dtolnay / typetag

Serde serializable and deserializable trait objects
Apache License 2.0
1.19k stars 38 forks source link

Deserializing an object of externally tagged typetag trait objects into a Vec of trait objects #27

Closed RDambrosio016 closed 2 years ago

RDambrosio016 commented 4 years ago

Hello!

I have structs similar to

#[derive(Deserialize, Serialize])
struct Config {
  pub rules: Option<RuleConf>
}

#[derive(Deserialize, Serialize])
struct RuleConf {
  pub errors: Vec<Box<dyn CstRule>>,
  pub warnings: Vec<Box<dyn CstRule>>
}

where CstRule is typetagged as externally typed. what my goal is is to be able to parse the config from this:

[rules.errors]
something_here = { some_prop = "foo" }

[rules.warnings]
something_else = {}

where something_here and something_else are the "tags" i set for the typetag, like #[typetag::serde(name = "something_else")]. This structure of vectors does not currently work, because it ends up being a vector of objects, with each object having a single key being the name of the typetag. What i basically want to do is flatten that into an object with those keys. I also tried a HashMap<String, Box<dyn CstRule>> and flattened it, but that fails to deserialize for more than one item.

I would also like to make the deserialization case insensitive, e.g. something_else and SomethingElse both work. I tried using serde-aux's attribute for this but that does not work. And i cant exactly apply it to the enum which would be generated by typetag.

Id love to know if there is a way to do this without manually implementing deserialize 🙂

dtolnay commented 4 years ago

You could do something like this:

// [dependencies]
// heck = "0.3"
// indoc = "1.0"
// serde = { version = "1.0", features = ["derive"] }
// toml = "0.5"
// typetag = "0.1"

use heck::CamelCase;
use indoc::indoc;
use serde::de::value::MapAccessDeserializer;
use serde::de::{DeserializeSeed, Deserializer, IntoDeserializer, MapAccess, Visitor};
use serde::Deserialize;
use std::fmt::{self, Debug};
use std::marker::PhantomData;

#[typetag::deserialize]
trait CstRule: Debug {}

#[derive(Deserialize, Debug)]
struct SomethingHere {
    some_prop: String,
}

#[typetag::deserialize]
impl CstRule for SomethingHere {}

#[derive(Deserialize, Debug)]
struct SomethingElse {}

#[typetag::deserialize]
impl CstRule for SomethingElse {}

#[derive(Deserialize, Debug)]
struct Config {
    rules: Option<RuleConf>,
}

#[derive(Deserialize, Debug)]
struct RuleConf {
    #[serde(deserialize_with = "from_typetag_objects")]
    errors: Vec<Box<dyn CstRule>>,
    #[serde(deserialize_with = "from_typetag_objects")]
    warnings: Vec<Box<dyn CstRule>>,
}

fn from_typetag_objects<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    struct TypetagObjects<T> {
        _type: PhantomData<T>,
    }

    impl<'de, T> Visitor<'de> for TypetagObjects<T>
    where
        T: Deserialize<'de>,
    {
        type Value = Vec<T>;

        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("zero or more typename-to-value pairs")
        }

        fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
        where
            M: MapAccess<'de>,
        {
            let mut vec = Vec::new();
            while let Some(key) = map.next_key::<String>()? {
                let de = MapAccessDeserializer::new(Entry {
                    key: Some(key.to_camel_case().into_deserializer()),
                    value: &mut map,
                });
                vec.push(T::deserialize(de)?);
            }
            Ok(vec)
        }
    }

    struct Entry<K, V> {
        key: Option<K>,
        value: V,
    }

    impl<'de, K, V> MapAccess<'de> for Entry<K, V>
    where
        K: Deserializer<'de, Error = V::Error>,
        V: MapAccess<'de>,
    {
        type Error = V::Error;

        fn next_key_seed<S>(&mut self, seed: S) -> Result<Option<S::Value>, Self::Error>
        where
            S: DeserializeSeed<'de>,
        {
            self.key.take().map(|key| seed.deserialize(key)).transpose()
        }

        fn next_value_seed<S>(&mut self, seed: S) -> Result<S::Value, Self::Error>
        where
            S: DeserializeSeed<'de>,
        {
            self.value.next_value_seed(seed)
        }
    }

    deserializer.deserialize_map(TypetagObjects { _type: PhantomData })
}

fn main() {
    let input = indoc! {r#"
        [rules.errors]
        something_here = { some_prop = "foo" }

        [rules.warnings]
        something_else = {}
        something_else = {}
    "#};

    println!("{:#?}", toml::from_str::<Config>(input).unwrap());
}
RDambrosio016 commented 4 years ago

That works! thank you 🙂

Any idea on how i could maybe parse special keys as properties? id like to have an error property which is a boolean based on whether error = {} is there.

dtolnay commented 2 years ago

Sorry that no one was able to provide further guidance here. If this is still an issue, you could try taking this question to any of the resources shown in https://www.rust-lang.org/community.