Thermatix
Thermatix

Reputation: 2929

Using serde, is it possible to deserialize to a struct that implements a type?

I'm building a rogue-like, I've already gotten a data loader working and part of the ECS working (building from scratch). The data is stored in .yml files and is used to describe things in the game (in this instance mobs) and what features those things have, for example:

---
orc:
  feature_packs:
    - physical
    - basic_identifiers_mob
  features:
    - component: char
      initial_value: T
goblin:
  feature_packs:
    - physical
    - basic_identifiers_mob
  features:
    - component: char
      initial_value: t

As you can see, there are two mobs described, a goblin and an orc, they both possess two feature packs (groups of features) and also posses a char feature that's used to describe what they look like to the player.

The initial_value field can be a string, an integer, a floating point, a bool, a range, etc depending on what the component requires, this will indicate the value or possible values the component may have when the component is generated during entity building/creation.

The problem is that I don't know how to, when iterating over the features, select the struct based on the component's name, for example, select the Char struct for the "char" feature.

To better describe what I mean, I've written an example in a language I better understand, Ruby:

data_manager = function_that_loads_data('folder_path')

Entity_Manager.build(:mob, :orc, data_manager)

class Entity_Manager
  class << self
    attr_accessor :entities, :components
  end

  def self.build(entity_type, template_name, data_manager)
    template = data_manager[entity_type][template_name]
    entity_id = generate_unique_id
    entities[entity_id] = Entity.new(entity_id, components: template.components.keys)
    template.components.each do |component|
      components[component.name][entity_id] =
        Components.get(component.name).new(component.initial_value) # <= This part, how do I do the equivalent in rust, a function that will return or allow me to get or create a struct based on the value of a string variable 
    end
  end
end

Now serde is the only thing I know that seems to be able to read text data and transform it into data, so to that end

How can I use serde (or a more appropriate non-serde using solution) to take the names of the feature and retrieve the correct struct, all implementing a type?

Incidentally, the one solution I'm trying not to use is a giant match statement.

The repo of my work as it stands is here

What I'm trying to avoid is doing somthing like this:

pub fn get(comp_name: &String) -> impl Component {
    match comp_name.as_ref() {
        "kind"      => Kind,
        "location"  => Location,
        "name"      => Name,
        "position"  => Position,
        "char"      => Char,
    }
}

because it's not really maintainable, though a macro would help a lot, I'm not very good at those atm and it doesn't even work, rust keeps thinking I'm trying to initialize the types when I just want to return one of several possible types that all will implement Component

EDIT: Becuase it looks like I'm not clear enough:

pub enum InitialValue {
    Char(char),
    String(String),
    Int(i32),
    Float(f32),
    Bool(bool),
    Range(Range<i32>),
    Point((i32,i32))
}


impl InitialValue {

    pub fn unwrap_char(&self) -> &char {
        match &self {
            InitialValue::Char(val) => val,
            _ => panic!("Stored value does not match unwrap type")
        }
    }

    pub fn unwrap_string(&self) -> &String {
        match &self {
            InitialValue::String(val) => val,
            _ => panic!("Stored value does not match unwrap type")
        }
    }

    pub fn unwrap_int(&self) -> &i32 {
        match &self {
            InitialValue::Int(val) => val,
            _ => panic!("Stored value does not match unwrap type")
        }
    }

    pub fn unwrap_float(&self) -> &f32 {
        match &self {
            InitialValue::Float(val) => val,
            _ => panic!("Stored value does not match unwrap type")
        }
    }

    pub fn unwrap_bool(&self) -> &bool {
        match &self {
            InitialValue::Bool(val) => val,
            _ => panic!("Stored value does not match unwrap type")
        }
    }

    pub fn unwrap_range(&self) -> &Range<i32> {
        match &self {
            InitialValue::Range(val) => val,
            _ => panic!("Stored value does not match unwrap type")
        }
    }

    pub fn unwrap_point(&self) -> &(i32, i32) {
        match &self {
            InitialValue::Point(val) => val,
            _ => panic!("Stored value does not match unwrap type")
        }
    }
}

#[derive(Debug, Deserialize)]
pub struct Component {
    #[serde(rename="component")]
    name: String,
    #[serde(default)]
    initial_value: Option<InitialValue>,
}

#[derive(Debug, Deserialize)]
pub struct Template {
    pub feature_packs: Vec<String>,
    pub features: Vec<Component>,
}

Upvotes: 1

Views: 1996

Answers (1)

Yann Vernier
Yann Vernier

Reputation: 15877

Sounds like you want a tagged union, or sum type; Rust knows these as enumerations. Serde even supports using container internal tags. So here's my little experiment:

#[macro_use] extern crate serde_derive;
extern crate serde_yaml;

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag="component")]
enum Feature {
    Char { initial_value : char },
    Weight { kgs : u32 }
}

fn main() {
    let v = vec![
        Feature::Char{initial_value:'x'},
        Feature::Weight{kgs:12}
    ];
    println!("{}", serde_yaml::to_string(&v).unwrap());
}

This outputs:

---
- component: Char
  initial_value: x
- component: Weight
  kgs: 12

Probably the next step is to make dedicated structs for the variants.

Upvotes: 1

Related Questions