Closed dtolnay closed 4 years ago
I'm on it! 🙂 WIP here: https://github.com/eupn/miniserde-derive-enum
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.
@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 struct
s 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}
That seems good to me! I think the generics and lifetimes concerns would equally complicated with any other approach too.
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"}}
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.
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.
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"]"#;
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.
@dtolnay crate is published: miniserde-derive-enum.
Terrific! I have added a link from the readme. Thanks for all your work on this library!
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).
@etwyniel good job!
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.
The representation could be whichever of Serde's enum representations is easiest to implement.