3

I'm using config as a means to load external data into my program which uses serde in the background to deserialize, but I want the ability that a specific field can be one of several types. Because I'm quite new to Rust, the documentation I found doesn't make a lot of sense.

How can I make it so that initial_value can be one of several types:

  • String
  • i32
  • bool
  • [i32,i32]

I get that I can use the attribute deserialize_with to direct to a custom decoder but beyond that I'm a bit at a loss.

Here's my code:

use std::collections::HashMap;
use std::fs;

use config_rs::{Config, ConfigError, File};

#[derive(Debug, Deserialize)]
enum InitialValues {
    String,
    i32,
    bool,
}

#[derive(Debug, Deserialize)]
struct Component {
    component: String,
    initial_value: InitialValues,
}

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

type Name = String;
type Type = String;

#[derive(Debug, Deserialize)]
pub struct Templates {
    templates: HashMap<Name, Template>,
}

impl Templates {
    pub fn new(file: &str) -> Result<Self, ConfigError> {
        let mut templates = Config::new();
        templates.merge(File::with_name(file)).unwrap();
        templates.try_into()
    }
}

#[derive(Debug)]
pub struct Manager {
    pub templates: HashMap<Type, Templates>,
}

impl Manager {
    pub fn new(dir: &str) -> Self {
        let mut manager = Self {
            templates: HashMap::new(),
        };
        'paths: for raw_path in fs::read_dir(dir).unwrap() {
            let path = raw_path.unwrap().path();
            let file_path = path.clone();
            let type_name = path.clone();
            let templates = match Templates::new(type_name.to_str().unwrap()) {
                Ok(templates) => templates,
                Err(message) => {
                    println!("TemplateManager: file({:?}), {}", &file_path, message);
                    continue 'paths;
                }
            };
            manager.templates.insert(
                file_path.file_stem().unwrap().to_str().unwrap().to_owned(),
                templates,
            );
        }
        manager
    }
}

An example config file:

---
templates:
  orc:
    feature_packs:
      - physical
      - basic_identifiers_mob
    components:
      - component: char
        initial_value: T
  goblin:
    feature_packs:
      - physical
      - basic_identifiers_mob
    components:
      - component: char
        initial_value: t
Thermatix
  • 2,757
  • 21
  • 51
  • Please review how to create a [MCVE] and then [edit] your question to include it. We cannot tell what crates and their versions are present in the code and there is [no crate called config_rs](https://crates.io/crates/config_rs). Try to produce something that reproduces your error on the [Rust Playground](https://play.rust-lang.org) or you can reproduce it in a brand new Cargo project. There are [Rust-specific MCVE tips](//stackoverflow.com/tags/rust/info) as well. – Shepmaster Sep 29 '18 at 12:58
  • 1
    See also [Deserialize TOML string to enum using config-rs](https://stackoverflow.com/q/47785720/155423); [How to deserialize into a enum variant based on a key name?](https://stackoverflow.com/q/45059538/155423); [Deserializing TOML into vector of enum with values](https://stackoverflow.com/q/48641541/155423); [How do I configure Serde to use an enum variant's discriminant rather than name?](https://stackoverflow.com/q/49604190/155423); etc. If it uses Serde, also check out the [thorough Serde docs](https://serde.rs/). – Shepmaster Sep 29 '18 at 13:01

2 Answers2

3

I would deserialize the example config you gave as follows.


const Y: &str = r#"
---
orc:
  feature_packs:
    - physical
    - basic_identifiers_mob
  components:
    - component: char
      initial_value: T
goblin:
  feature_packs:
    - physical
    - basic_identifiers_mob
  components:
    - component: char
      initial_value: t
"#;

#[macro_use]
extern crate serde_derive;

extern crate serde;
extern crate serde_yaml;

use std::collections::HashMap as Map;

type Templates = Map<String, Template>;

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

#[derive(Deserialize, Debug)]
#[serde(
    tag = "component",
    content = "initial_value",
    rename_all = "lowercase",
)]
enum Component {
    Char(char),
    String(String),
    Int(i32),
    Float(f32),
    Bool(bool),
    Range(Range<i32>),
}

#[derive(Deserialize, Debug)]
struct Range<T> {
    start: T,
    end: T,
}

fn main() {
    println!("{:#?}", serde_yaml::from_str::<Templates>(Y).unwrap());
}
dtolnay
  • 9,621
  • 5
  • 41
  • 62
1

Changing the InitialValues enum to be:

#[derive(Debug, Deserialize)]
#[serde(
    rename_all = "lowercase",
    untagged
)]
pub enum InitialValue {
    Char(char),
    String(String),
    Int(i32),
    Float(f32),
    Bool(bool),
    Range(Range<i32>)
}

Means that the following now works:

---
example:
  feature_packs:
    - example
    - example
  components:
    - component: char
      initial_value: T
    - component: string
      initial_value: 'asdasdasd'
    - component: int
      initial_value: 2
    - component: float
      initial_value: 3.2
    - component: bool
      initial_value: true
    - component: range
      initial_value:
        start: 0
        end: 9

It's a bit more verbose then I'd like but at least it's doable.


EDIT: Thanks to @dtolnay answer I was able to improve this question and now it reads the data exactly as I originally intended!

Code above has already been modified to reflect this.


Also, by #[serde(flatten)] to Templates like so:

#[derive(Debug, Deserialize)]
pub struct Templates {
    #[serde(flatten)]
    templates: HashMap<Name,Template>,
}

I was able to remove the need to add templates: at the start of the file.

Thermatix
  • 2,757
  • 21
  • 51