dtolnay / request-for-implementation

Crates that don't exist, but should
613 stars 6 forks source link

Miniserde derive macro that supports enums #10

Closed dtolnay closed 4 years ago

dtolnay commented 5 years ago

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 commented 5 years ago

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

dtolnay commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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

eupn commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 4 years ago

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 commented 4 years ago

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

dtolnay commented 4 years ago

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

etwyniel commented 4 years ago

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 commented 4 years ago

@etwyniel good job!