thunderbird / xml-struct-rs

Mozilla Public License 2.0
13 stars 0 forks source link

Allow defining element attributes via nested structures #9

Open babolivier opened 2 weeks ago

babolivier commented 2 weeks ago

Consider:

enum MyEnum
    Foo {
        #[xml_struct(attribute)]
        a_field: String,

        #[xml_struct(attribute)]
        another_field: String,
    },
    Bar,
}

MyEnum::Foo{..} serializes into something like <someTagName AField="..." AnotherField="..." />.

Now consider that we want to extract MyEnum::Foo's inner structure into a struct so it can be used directly by something else:

struct Foo {
    #[xml_struct(attribute)]
    a_field: String,

    #[xml_struct(attribute)]
    another_field: String,
}

enum MyEnum
    Foo(Foo),
    Bar,
}

Now MyEnum::Foo(..) serializes into something like <someTagName />.

A concrete example can be found in ews-rs. PathToElement is used to specify the identifier to a property to e.g. request from the EWS server. One of its variants is ExtendedFieldURI, which represents the identifier to an extended MAPI property.

However, EWS also defines the ExtendedProperty element, which specifically expects an ExtendedFieldURI child element. In order to define this child element in Rust, without breaking the existing consumers of PathToElement, I can only see two non-ideal solutions:

leftmostcat commented 1 week ago

I agree that what we have right now doesn't feel great or behave very intuitively.

This behavior was an intentional decision so that we behave consistently for all tuples. If you declared MyEnum::Foo as a 2-tuple (e.g. Foo(Foo, Baz)), I'm not sure that there's an intuitive way to serialize that consistently with how we'd treat a single-element tuple as a newtype.

That said, we currently have no way to treat a single-element tuple as a newtype, and that comes up much more frequently than I expect multi-element tuple variants to appear. I agree that we should solve this problem, as it doesn't match intuition and it makes it impossible to use a common Rust pattern. Suggestions welcome.

leftmostcat commented 1 week ago

Current behavior

The primary pattern we use at present deals with structs with named fields, e.g.:

struct Foo {
    field: String,
    other_field: String,
}

In this case, the name of each field is used as the basis for the name of an XML element which encloses the contents of the field. For example, the following Rust value would be serialized as the subsequent XML:

let foo = Foo {
    field: String::from("some text"),
    other_field: String::from("other text"),
};

foo.serialize_as_element(&mut writer, "Foo")?;
<Foo>
    <Field>some text</Field>
    <OtherField>other text</OtherField>
</Foo>

It is possible to declare that certain fields should be serialized as attributes on the enclosing element:

struct Bar {
    field: String,

    #[xml_struct(attribute)]
    other_field: String,
}

let bar = Bar {
    field: String::from("some text"),
    other_field: String::from("other text"),
};

bar.serialize_as_element(&mut writer, "Bar")?;
<Bar OtherField="other text">
    <Field>some text</Field>
</Bar>

When a struct's fields are not named, we do not enclose the contents of those fields in XML elements:

struct Baz(String, String);

let baz = Baz(String::from("some text"), String::from("other text"));

baz.serialize_as_element(&mut writer, "Baz")?;
<Baz>some textother text</Baz>

Problem

The newtype pattern, wherein a type is enclosed in a struct with a single unnamed field in order to give the resulting type special behavior, is common in Rust and would be particularly useful for us in reusing structures which appear in enums.

This generally works as expected when the enclosed type does not include any attribute fields. However, if a struct with attribute fields is enclosed in a tuple, its attribute fields are ignored. The field does not have its own enclosing element, and attempting to resolve attribute fields between multiple types could create conflicts or runtime errors.

In the end, the combination of these behaviors creates a conflict of intuition, in which attempting to newtype a single struct with attribute fields causes those fields to be silently ignored, whereas we might generally expect them to be included in the surrounding element. Unfortunately, a derive macro cannot reliably introspect the types of fields, so there is no plausible means of allowing attribute fields on a single-element tuple but giving a compile-time error when they are used with multi-element tuples.