Skip to content
This repository has been archived by the owner on Jul 9, 2023. It is now read-only.

Miniserde derive macro that supports enums #10

Closed
dtolnay opened this issue Jan 21, 2019 · 13 comments
Closed

Miniserde derive macro that supports enums #10

dtolnay opened this issue Jan 21, 2019 · 13 comments

Comments

@dtolnay
Copy link
Owner

dtolnay commented Jan 21, 2019

The Miniserde built in derive macros keep compile time down by only supporting structs. But for some use cases it is pretty important to be able to serialize and deserialize enums.

I would like a different crate to provide derive macros that implement Miniserde's traits for an enum.

#[derive(Serialize_enum, Deserialize_enum)]
enum E {
    Unit,
    Tuple(u8, u8),
    Struct { one: u8, two: u8 },
}
// generated code

impl miniserde::Serialize for E {
    /* ... */
}

impl miniserde::Deserialize for E {
    /* ... */
}

The representation could be whichever of Serde's enum representations is easiest to implement.

@eupn
Copy link

eupn commented Oct 15, 2019

I'm on it! 🙂 WIP here: https://github.com/eupn/miniserde-derive-enum

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 15, 2019

Nice! Let me know if you have questions. The miniserde serialization and deserialization APIs are unfortunately (even) more confusing than the ones in serde because recursion is such a natural way to express serializing and deserializing nested types and miniserde needs to work without recursion, but the existing miniserde struct derive code and using cargo expand to look at the generated code should be helpful.

@eupn
Copy link

eupn commented Oct 15, 2019

@dtolnay

I've started from serialization part and have simple enums serializing with "externally-tagged" strategy working:

#[derive(Serialize_enum)]
enum E {
    Struct { a: u8, b: u8 },
}

let e = E::Struct { a: 0u8, b: 1u8 };
println!("{}", json::to_string(&e));

Prints: {"Struct":{"a":0,"b":1}}

For those I generate a structs with fields as references from currently serializing variant:

#[derive(Serialize)]
struct __E_Struct_VARIANT_STRUCT<'a> {
    a: &'a u8,
    b: &'a u8,
}

And use already implemented miniserde machinery for structs to serialize that under the tag:

Some((miniserde::export::Cow::Borrowed("Struct"), &self.data)),

To fill those structs, I destructure enum variants via pattern-matching by-reference in generated code:

match self {
    E::Struct { ref a, ref b } => {
        miniserde::ser::Fragment::Map(Box::new(__E_Struct_VARIANT_STREAM {
            data: __E_Struct_VARIANT_STRUCT { a, b },
            state: 0,
        }))
    }
}

Using references here and there allowed me to avoid copying, but generics and lifetimes will probably complicate everything or won't work that way. Is my approach sustainable or should I consider doing that differently?

And also unit variants support is on the way:

#[derive(Serialize_enum)]
enum E {
    Unit,
}

let e = E::Unit;
println!("{}", json::to_string(&e));

Prints: {"Unit":null}

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 15, 2019

That seems good to me! I think the generics and lifetimes concerns would equally complicated with any other approach too.

@eupn
Copy link

eupn commented Oct 15, 2019

Simple serialization working 🎉:

#[derive(Serialize_enum)]
enum E {
    Unit,
    Struct { a: u8, b: u8 },
    Tuple(u8, String),
}

let s = E::Struct { a: 0u8, b: 1u8 };
let u = E::Unit;
let t = E::Tuple(0u8, "Hello".to_owned());

println!(
    "{}\n{}\n{}",
    json::to_string(&s),
    json::to_string(&u),
    json::to_string(&t)
);

Produces:

{"Struct":{"a":0,"b":1}}
{"Unit":null}
{"Tuple":{"_0":0,"_1":"Hello"}}

@eupn
Copy link

eupn commented Oct 16, 2019

Implemented struct enum deserialization 🎉! Now we can do roundtrip ser/de test:

    #[derive(Debug, Serialize_enum, Deserialize_enum)]
    enum E {
        A { a: u8, b: Box<E> },
        B { b: u8 },
    }

    let s_a = E::A {
        a: 0,
        b: Box::new(E::B { b: 1 }), // Nesting is also supported
    };
    let s_b = E::B { b: 1 };

    let json_a = json::to_string(&s_a);
    println!("{}", json_a);

    let json_b = json::to_string(&s_b);
    println!("{}", json_b);

    let s_a: E = json::from_str(&json_a).unwrap();
    let s_b: E = json::from_str(&json_b).unwrap();

    dbg!(s_a, s_b);

Will print:

{"A":{"a":0,"b":{"B":{"b":1}}}}
{"B":{"b":1}}

[src/main.rs] s_a = A {
    a: 0,
    b: B {
        b: 1,
    },
}
[src/main.rs] s_b = B {
    b: 1,
}

The approach to deserialization is similar to that during serialization. Struct enum variants are represented as structures with #[derive(Deserialize) applied to them. The enum deserializer is a top-level builder that have optional fields in it for each variant structure and holds a string key (tag) for the variant that is actually being deserialized. During finalization, we match against the previously saved key to choose from one of the optional variant fields, then take the chosen field and construct an enum variant filled with data from it.

@eupn
Copy link

eupn commented Oct 16, 2019

Both serialization and deserialization are implemented 🎉 and following code compiles:

#[derive(Debug, Serialize_enum, Deserialize_enum)]
enum E {
    UnitA,
    UnitB,
    Struct { a: u8, b: Box<E>, c: Box<E> },
    Tuple(u8, String),
}

let ua = E::UnitA;
let ub = E::UnitB;
let t = E::Tuple(0, "Hello".to_owned());
let s = E::Struct {
    a: 0,
    b: Box::new(E::Struct {
        a: 42,
        b: Box::new(E::UnitA),
        c: Box::new(E::Tuple(0, "Test".to_owned())),
    }),
    c: Box::new(E::UnitB),
};

let json_s = json::to_string(&s);
let json_t = json::to_string(&t);
let json_ua = json::to_string(&ua);
let json_ub = json::to_string(&ub);

println!("{}", json_ua);
println!("{}", json_ub);
println!("{}", json_s);
println!("{}", json_t);

let ua: E = json::from_str(&json_ua).unwrap();
let ub: E = json::from_str(&json_ub).unwrap();
let s: E = json::from_str(&json_s).unwrap();
let t: E = json::from_str(&json_t).unwrap();

dbg!(ua, ub, s, t);

and prints when run:

{"UnitA":null}
{"UnitB":null}
{"Struct":{"a":0,"b":{"Struct":{"a":42,"b":{"UnitA":null},"c":{"Tuple":{"_0":0,"_1":"Test"}}}},"c":{"UnitB":null}}}
{"Tuple":{"_0":0,"_1":"Hello"}}
[src/main.rs:41] ua = UnitA
[src/main.rs:41] ub = UnitB
[src/main.rs:41] s = Struct {
    a: 0,
    b: Struct {
        a: 42,
        b: UnitA,
        c: Tuple(
            0,
            "Test",
        ),
    },
    c: UnitB,
}
[src/main.rs:41] t = Tuple(
    0,
    "Hello",
)

Next thing will be the support for generics.

@etwyniel
Copy link

I actually started working on this as well a few days ago at https://github.com/etwyniel/miniserde-enum.

I only support serialization for now, but in a way that more closely resembles SerDe's behavior, i.e. tuple variants serialize to arrays (or a single value), unit variants serialize to strings.
I also support different enum representations. I'm only missing adjacently tagged enums.
Generics should work, although they haven't been extensively tested.

Here are a few examples from my tests:

#[serde(tag = "type")]
#[derive(Serialize_enum)]
enum Internal {
    A,
    #[serde(rename = "renamedB")]
    B,
    C {
        x: i32,
    },
}
use Internal::*;
let example = [A, B, C { x: 2 }];
let expected = r#"[{"type":"A"},{"type":"renamedB"},{"type":"C","x":2}]"#;
#[derive(Serialize_enum)]
enum External {
    A(i32),
    #[serde(rename = "renamedB")]
    B(i32, String),
    C {
        x: i32,
    },
    D,
}
use External::*;
let example = [A(21), B(42, "everything".to_string()), C { x: 2 }, D];
let expected = r#"[{"A":21},{"renamedB":[42,"everything"]},{"C":{"x":2}},"D"]"#;
#[serde(untagged)]
#[derive(Serialize_enum)]
enum Untagged {
    A(i32),
    #[serde(rename = "renamedB")]
    B(i32, String),
    C {
        x: i32,
    },
    D,
}
use Untagged::*;
let example = [A(21), B(42, "everything".to_string()), C { x: 2 }, D];
let expected = r#"[21,[42,"everything"],{"x":2},"D"]"#;

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 20, 2019

Wow that's great progress! I filed a small suggestion in both. Once the crates are documented and released to crates.io, I will close out this issue.

@eupn
Copy link

eupn commented Oct 27, 2019

@dtolnay crate is published: miniserde-derive-enum.

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 27, 2019

Terrific! I have added a link from the readme. Thanks for all your work on this library!

@etwyniel
Copy link

For whatever it's worth, I finally got around to publishing my crate, after adding deserialization for most enum representations (haven't quite figured out how to deserialize untagged enums yet).

@eupn
Copy link

eupn commented Nov 19, 2019

@etwyniel good job!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants