dtolnay / serde-yaml

Strongly typed YAML library for Rust
Apache License 2.0
964 stars 164 forks source link

Fallback on default enum variant when no tag is present #338

Closed NikolaLohinski closed 2 years ago

NikolaLohinski commented 2 years ago

Hi and thanks for the lib, especially the latest work on YAML tags which it makes it really useable!

I am trying to figure out if there is a way to have a « default » tag applied when none is defined but the expected target type is an enum wanting a tag to be defined.

In code, is there anyway to have this passing ?

use serde_yaml;
use serde::Deserialize;

fn main() {
   #[derive(Deserialize, Debug, PartialEq)]
   enum Enum<'a> {
       Variant(&'a str),
       Other(u32),
       Default(&'a str),
   }
   let file = r#"
   - !Variant test
   - !Other 42
   - !Default foo
   - bar # will default to !Default since no tag has been supplied
   "#;
   let result: Vec<Enum> = serde_yaml::from_str(file).unwrap();
   assert_eq!(
       result,
       vec![
           Enum::Variant("test"),
           Enum::Other(42),
           Enum::Default("foo"),
           Enum::Default("bar"),
       ]
   );
}

I basically want the YAML to be deserialized into an enum, where only some tags are specified, and the others “default” to one of the variants.

Thanks in advance !

dtolnay commented 2 years ago

Here are 2 possible implementations. A conceptually simpler one where we just insert a Tag if there isn't one already present, but it can't deserialize borrowed values this way:

// [dependencies]
// serde = { version = "1.0.147", features = ["derive"] }
// serde_yaml = "0.9.14"

use serde::{Deserialize, Deserializer};

#[derive(Deserialize, Debug, PartialEq)]
#[serde(remote = "Self")]
enum Enum {
    Variant(String),
    Other(u32),
    Default(String),
}

impl<'de> Deserialize<'de> for Enum {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let tagged_value = match serde_yaml::Value::deserialize(deserializer)? {
            serde_yaml::Value::Tagged(tagged_value) => *tagged_value,
            value => serde_yaml::value::TaggedValue {
                tag: serde_yaml::value::Tag::new("Default"),
                value,
            },
        };
        Enum::deserialize(tagged_value).map_err(serde::de::Error::custom)
    }
}

or a slightly more verbose one that avoids allocation and supports borrowing:

use serde::de::value::{BorrowedStrDeserializer, EnumAccessDeserializer};
use serde::de::{Deserializer, EnumAccess, Visitor};
use serde::Deserialize;
use std::fmt;
use std::marker::PhantomData;

#[derive(Deserialize, Debug, PartialEq)]
#[serde(remote = "Self")]
enum Enum<'a> {
    Variant(&'a str),
    Other(u32),
    Default(&'a str),
}

impl<'de: 'a, 'a> Deserialize<'de> for Enum<'a> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct EnumVisitor<'de>(PhantomData<fn() -> Enum<'de>>);

        impl<'de> Visitor<'de> for EnumVisitor<'de> {
            type Value = Enum<'de>;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("enum Enum")
            }

            fn visit_borrowed_str<E>(self, s: &'de str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                let de = BorrowedStrDeserializer::new(s);
                Deserialize::deserialize(de).map(Enum::Default)
            }

            fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
            where
                A: EnumAccess<'de>,
            {
                let de = EnumAccessDeserializer::new(data);
                Enum::deserialize(de)
            }
        }

        deserializer.deserialize_any(EnumVisitor(PhantomData))
    }
}
NikolaLohinski commented 2 years ago

Thanks a lot, I got it working just fine for my use case with your instructions!

As I needed to do serialization as well I had to add to explicitly re-implement Serialize as follows

impl Serialize for Enum {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
      S: serde::Serializer
  {
      Enum::serialize(self, serializer)
  }
}

since adding #[serde(remote = "Self")] requires defining it in order to get back the default serialization behaviour.

Anyhow, thanks for the help ! 🙏