Rust, Enums And Magical Custom Values

As part of the Kanidm project every object has Attributes which we catch in a chonky Enum. But being an IDP used by humans, they want to be able to use custom attribues, which makes it hard when we’re trying to strictly type and catch typos. Enter… serde and its parsing magic.

The comments hopefully explain how it works, but basically we parse the string value and if it doesn’t match, then it gets put into a Custom(value) variant, which when serialized uses the inner value as the output. The container attributes documentation’s where you want to go for more information.

use serde::{Deserialize, Serialize};

#[derive(Serialize, Debug, Deserialize, Clone, Eq, PartialEq)]
// The from/into attributes do the real magic.
#[serde(rename_all = "lowercase", from = "&str", into = "String")]
enum CustomEnum {
    Foo,
    Bar,
    /// Catches everything not listed above, as part of the `From<&str>` implementation
    Custom(String),
}

impl std::fmt::Display for CustomEnum {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CustomEnum::Foo => "foo".fmt(f),
            CustomEnum::Bar => "bar".fmt(f),
            // Returns the inner value
            CustomEnum::Custom(val) => f.write_str(val),
        }
    }
}

/// This is used for `serde` things!
impl From<CustomEnum> for String {
    fn from(value: CustomEnum) -> Self {
        value.to_string()
    }
}

impl From<&str> for CustomEnum {
    fn from(value: &str) -> Self {
        match value {
            "foo" => Self::Foo,
            "bar" => Self::Bar,
            // if we don't find a match, shove it into a `Custom` variant.
            _ => Self::Custom(value.to_string()),
        }
    }
}

/// Let's make sure it works!
fn main() {
    for testval in [
        CustomEnum::Foo,
        CustomEnum::Bar,
        CustomEnum::Custom("hello".to_string()),
    ] {
        // Show what it looks like before hand...
        println!("Test value: {:?}", &testval);

        // Serialize it and show it...
        let as_string = serde_json::to_string(&testval).expect("Failed");
        println!("serialized: {}", as_string);

        // Take that value and deserialize it again, to check that path...
        let from_string: CustomEnum = serde_json::from_str(&as_string).expect("Failed");
        println!("deserialized: {:?}", from_string);
        // Now we check we're actually getting what we expect back...
        assert_eq!(from_string, testval);
        println!("\n-------\n");
    }
}

And here’s the output:

Test value: Foo
serialized: "foo"
deserialized: Foo

-------

Test value: Bar
serialized: "bar"
deserialized: Bar

-------

Test value: Custom("hello")
serialized: "hello"
deserialized: Custom("hello")

-------


#rust #serde #howto