xso: add support for overriding names of generated types

Jonas Schäfer created

In 1265f4b, we introduced a change which may cause a conflict of type
names when deriving the traits on two different types. While a
workaround existed (use `mod`s to isolate the implementation), that is
ugly.

This commit allows overriding the choice of type names.

Change summary

parsers/src/util/macro_tests.rs | 23 +++++++++++++++++++++++
xso-proc/src/meta.rs            | 22 ++++++++++++++++++++++
xso-proc/src/structs.rs         | 14 ++++++++++++--
xso/ChangeLog                   | 16 ++++++----------
xso/src/from_xml_doc.md         | 14 ++++++++++++++
5 files changed, 77 insertions(+), 12 deletions(-)

Detailed changes

parsers/src/util/macro_tests.rs 🔗

@@ -587,3 +587,26 @@ fn boxed_child_roundtrip_nested_2() {
     };
     roundtrip_full::<BoxedChild>("<elem xmlns='urn:example:ns1'><elem><elem/></elem></elem>")
 }
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "elem", builder = RenamedBuilder, iterator = RenamedIter)]
+struct RenamedTypes;
+
+#[test]
+fn renamed_types_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<RenamedTypes>("<elem xmlns='urn:example:ns1'/>")
+}
+
+#[test]
+#[allow(unused_comparisons)]
+fn renamed_types_get_renamed() {
+    // these merely serve as a test that the types are declared with the names
+    // given in the attributes.
+    assert!(std::mem::size_of::<RenamedBuilder>() >= 0);
+    assert!(std::mem::size_of::<RenamedIter>() >= 0);
+}

xso-proc/src/meta.rs 🔗

@@ -157,6 +157,12 @@ pub(crate) struct XmlCompoundMeta {
 
     /// The debug flag.
     pub(crate) debug: Flag,
+
+    /// The value assigned to `builder` inside `#[xml(..)]`, if any.
+    pub(crate) builder: Option<Ident>,
+
+    /// The value assigned to `iterator` inside `#[xml(..)]`, if any.
+    pub(crate) iterator: Option<Ident>,
 }
 
 impl XmlCompoundMeta {
@@ -167,6 +173,8 @@ impl XmlCompoundMeta {
     fn parse_from_attribute(attr: &Attribute) -> Result<Self> {
         let mut namespace = None;
         let mut name = None;
+        let mut builder = None;
+        let mut iterator = None;
         let mut debug = Flag::Absent;
 
         attr.parse_nested_meta(|meta| {
@@ -188,6 +196,18 @@ impl XmlCompoundMeta {
                 }
                 debug = (&meta.path).into();
                 Ok(())
+            } else if meta.path.is_ident("builder") {
+                if builder.is_some() {
+                    return Err(Error::new_spanned(meta.path, "duplicate `builder` key"));
+                }
+                builder = Some(meta.value()?.parse()?);
+                Ok(())
+            } else if meta.path.is_ident("iterator") {
+                if iterator.is_some() {
+                    return Err(Error::new_spanned(meta.path, "duplicate `iterator` key"));
+                }
+                iterator = Some(meta.value()?.parse()?);
+                Ok(())
             } else {
                 Err(Error::new_spanned(meta.path, "unsupported key"))
             }
@@ -198,6 +218,8 @@ impl XmlCompoundMeta {
             namespace,
             name,
             debug,
+            builder,
+            iterator,
         })
     }
 

xso-proc/src/structs.rs 🔗

@@ -85,13 +85,23 @@ impl StructDef {
             return Err(Error::new(meta.span, "`name` is required on structs"));
         };
 
+        let builder_ty_ident = match meta.builder {
+            Some(v) => v,
+            None => concat_camel_case(ident, "FromXmlBuilder"),
+        };
+
+        let item_iter_ty_ident = match meta.iterator {
+            Some(v) => v,
+            None => concat_camel_case(ident, "AsXmlIterator"),
+        };
+
         Ok(Self {
             namespace,
             name,
             inner: Compound::from_fields(fields)?,
             target_ty_ident: ident.clone(),
-            builder_ty_ident: concat_camel_case(ident, "FromXmlBuilder"),
-            item_iter_ty_ident: concat_camel_case(ident, "AsXmlIterator"),
+            builder_ty_ident,
+            item_iter_ty_ident,
             debug: meta.debug.is_set(),
         })
     }

xso/ChangeLog 🔗

@@ -2,20 +2,16 @@ Version NEXT:
 0000-00-00 Jonas Schäfer <jonas@zombofant.net>
     * Breaking
       - We now strip trailing underscores from identifiers before constructing
-        any type names we declare from derive macros. That means that it is
-        not possible to derive the traits on `Foo` and `Foo_` if both live
-        in the same scope.
+        any type names we declare from derive macros.
 
-        As a workaround, you can put either of these types into a `mod` and
-        reexport them from the outer module. The types generated by the derive
-        macro will then be scoped inside the `mod` and cannot conflict with
-        the derived types on the other type.
-
-        All this is to avoid triggering the camel case lint on the types we
-        generate.
+        If you previously derived any of the macros on e.g. `Foo` and `Foo_`
+        within the same scope, you can use the newly added `builder` and
+        `iterator` meta keys to override the generated type names.
     * Added
       - Support for child elements in derive macros. Child elements may also
         be wrapped in Option or Box.
+      - Support for overriding the names of the types generated by the derive
+        macros.
 
 Version 0.1.2:
 2024-07-26 Jonas Schäfer <jonas@zombofant.net>

xso/src/from_xml_doc.md 🔗

@@ -35,6 +35,7 @@ such:
   is also a path.
 - *string literal*: A string literal, like `"hello world!"`.
 - *type*: A Rust type.
+- *ident*: A Rust identifier.
 - flag: Has no value. The key's mere presence has relevance and it must not be
   followed by a `=` sign.
 
@@ -46,6 +47,8 @@ The following keys are defined on structs:
 | --- | --- | --- |
 | `namespace` | *string literal* or *path* | The XML element namespace to match. If it is a *path*, it must point at a `&'static str`. |
 | `name` | *string literal* or *path* | The XML element name to match. If it is a *path*, it must point at a `&'static NcNameStr`. |
+| `builder` | optional *ident* | The name to use for the generated builder type. |
+| `iterator` | optional *ident* | The name to use for the generated iterator type. |
 
 Note that the `name` value must be a valid XML element name, without colons.
 The namespace prefix, if any, is assigned automatically at serialisation time
@@ -58,6 +61,17 @@ and cannot be overridden. The following will thus not compile:
 struct Foo;
 ```
 
+If `builder` or `iterator` are given, the respective generated types will use
+the given names instead of names chosen by the derive macro implementation.
+Helper types will use these names as prefix. The exact names of helper types
+are implementation defined, which is why any type name starting with the
+identifiers passed to either of these keys is considered reserved.
+
+By default, the builder type uses the type's name suffixed with
+`FromXmlBuilder` and the iterator type uses the type's name suffixed with
+`AsXmlIterator`. If the target type has any trailing underscores, they are
+removed before making the type name.
+
 ### Field meta
 
 For fields, the *meta* consists of a nested meta inside the `#[xml(..)]` meta,