tafia / quick-xml

Rust high performance xml reader and writer
MIT License
1.2k stars 236 forks source link

Change the representation of Enums #717

Open dishmaker opened 8 months ago

dishmaker commented 8 months ago
serde_json supports enums: Kind Enum field
Unit "field": "Unit",
Newtype "field": { "Newtype": 42 },
Tuple "field": { "Tuple": [42, "answer"] },
Struct "field": { "Struct": {"q": 42, "a":"answer"} },
enum Kinds {             // Enum name as:
    Unit,                 // Value
    Newtype(i32),         // Key
    Tuple((i32, String)), // Key
    Struct(MyStruct),     // Key
}

https://docs.rs/quick-xml/latest/quick_xml/de/index.html#normal-enum-variant

so quick-xml should too: Kind Enum field
Unit <field>Unit</field>
Newtype <field><Newtype>42</Newtype></field>
Tuple <field><Tuple>42</Tuple><Tuple>answer</Tuple></field>
Struct <field><Struct><q>42</q><a>answer</a></Struct></field>
enum Kinds {             // Enum name as:
    Unit,                 // Value
    Newtype(i32),         // Tag
    Tuple((i32, String)), // Tag
    Struct(MyStruct),     // Tag
}

Currently the above XML won't be generated (gives an error) while JSON would even parse back into Rust enum. Unsupported operation: cannot serialize enum newtype variant

Serialization of enum - crate comparison:

// [package]
// name = "xml_vs_json"
// version = "0.1.0"
// edition = "2021"

// [dependencies]
// eyre = "0.6.12"
// quick-xml = { version = "0.31.0", features = ["serde", "serialize"] }
// rmp-serde = "1.1.2"
// serde = { version = "1.0.197", features = ["derive"] }
// serde_json = "1.0.114"
// serde_yaml = "0.9.32"
// toml = "0.8.10"

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct MyStruct {
    pub i8_value: i8,
    pub choice_value: MyChoice,
    pub choice_list: Vec<MyChoice>,
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub enum MyChoice {
    ChoiceA(MyChoiceA),
    ChoiceB(MyChoiceB),
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct MyChoiceA {
    pub choice_i32: i32,
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct MyChoiceB {
    pub choice_i64: i64,
    pub choice_text: String,
}

fn main() {
    let result = run();
    if let Err(err) = result {
        println!("err: {err}");
    }
}

fn run() -> eyre::Result<()> {
    let a = MyStruct {
        i8_value: 8,
        choice_value: MyChoice::ChoiceA(MyChoiceA { choice_i32: 32 }),

        choice_list: vec![
            MyChoice::ChoiceA(MyChoiceA { choice_i32: 32 }),
            MyChoice::ChoiceB(MyChoiceB {
                choice_i64: 64,
                choice_text: "my text".to_string(),
            }),
            MyChoice::ChoiceA(MyChoiceA { choice_i32: 32 }),
        ],
    };

    try_reencode_all(&a)?;

    Ok(())
}

fn try_reencode_all(a: &MyStruct) -> eyre::Result<()> {
    // pass
    let a_json = reencode_json(&a)?;
    assert_eq!(a, &a_json);

    // pass
    let a_toml = reencode_toml(&a)?;
    assert_eq!(a, &a_toml);

    // pass
    let a_yaml = reencode_yaml(&a)?;
    assert_eq!(a, &a_yaml);

    // pass
    let a_msgpack = reencode_msgpack(&a)?;
    assert_eq!(a, &a_msgpack);

    // fail: Unsupported operation: cannot serialize enum newtype variant `MyChoice::ChoiceA`
    let a_qxml = reencode_quick_xml(&a)?;
    assert_eq!(a, &a_qxml);
    Ok(())
}

fn reencode_json(input: &MyStruct) -> eyre::Result<MyStruct> {
    let json_str: String = serde_json::to_string(&input)?;
    println!("serde_json: {json_str}");
    let output = serde_json::from_str(&json_str)?;
    Ok(output)
}

fn reencode_toml(input: &MyStruct) -> eyre::Result<MyStruct> {
    let toml_str: String = toml::to_string(&input)?;
    println!("toml: {toml_str}");
    let output = toml::from_str(&toml_str)?;
    Ok(output)
}

fn reencode_yaml(input: &MyStruct) -> eyre::Result<MyStruct> {
    let yaml_str: String = serde_yaml::to_string(&input)?;
    println!("yaml: {yaml_str}");
    let output = serde_yaml::from_str(&yaml_str)?;
    Ok(output)
}

fn reencode_msgpack(input: &MyStruct) -> eyre::Result<MyStruct> {
    let msgpack_bytes: Vec<u8> = rmp_serde::to_vec(&input)?;
    println!("rmp_serde: {msgpack_bytes:?}");
    let output = rmp_serde::from_slice(&msgpack_bytes)?;
    Ok(output)
}

fn reencode_quick_xml(input: &MyStruct) -> eyre::Result<MyStruct> {
    // serialization does not work
    let qxml_str: String = quick_xml::se::to_string(&input)?;
    println!("quick_xml::se: {qxml_str}");
    let output = quick_xml::de::from_str(&qxml_str)?;
    Ok(output)
}
Mingun commented 8 months ago

You can find answer in this chapter about mapping of Rust types to XML. TL;DR: your enum variant is not a unit variant and it cannot be serialized in arbitrary-named field of struct.

dishmaker commented 8 months ago

Then why does it work for toml, yaml, msgpack and der but does not for quick-xml?

Why do I have to compile different binary just for your library? It forces me to use a feature flag in compile time.

Is there any way to show serde(rename) exclusively to this crate?


struct AnyName {
  #[serde(rename = "$value")]
  any_name: Choice,
}
dishmaker commented 8 months ago

But still, even when using #[serde(rename = "$value")] it does not serialize <any_name> ... </any_name> but only the inside variant.

Mingun commented 8 months ago

It is hard to answer to your questions because you do not provide your expectations. The mentioned piece of documentation shows how quick-xml performs mapping in a consistent manner. If you have concrete suggestions, please describe them and even better open a PR with them!

dishmaker commented 8 months ago

My expectations:

Kind    In normal field
Unit    <field>Unit</field>
Newtype <field><Newtype>42</Newtype></field>
Tuple   <field><Tuple>42</Tuple><Tuple>answer</Tuple></field>
Struct  <field><Struct><q>42</q><a>answer</a></Struct></field>

Just like JSON:

Unit       "field": "Unit",
Newtype    "field": { "Newtype": 42 },
Tuple      "field": [42, "answer"],
Struct     "field": { "Struct": {"q": 42, "a":"answer"} },
dishmaker commented 7 months ago

Issue should be reopened - other crates support enum lists. Only quick-xml is a black sheep here.

Mingun commented 7 months ago

You feel free to submit PR that would implement the desired behavior and make it in the consistent way. Probably this is possible. We also should keep the ability to use tag name as enum discriminator, because this is natural way how xs:choice in XML is represented. It must be representable with Rust enum.