xso: allow omission of namespace and name on extracts

Jonas SchΓ€fer created

This is a quality-of-life improvement, as it may save lots of typing in
the common case (see the diff in parsers).

Change summary

parsers/src/data_forms.rs       |  2 
parsers/src/mix.rs              | 12 ++--
parsers/src/util/macro_tests.rs | 57 ++++++++++++++++++++++
xso-proc/src/compound.rs        |  8 ++
xso-proc/src/enums.rs           |  6 +-
xso-proc/src/field.rs           | 88 +++++++++++++++++++---------------
xso-proc/src/meta.rs            |  2 
xso-proc/src/structs.rs         |  2 
xso/src/from_xml_doc.md         |  6 ++
9 files changed, 129 insertions(+), 54 deletions(-)

Detailed changes

parsers/src/data_forms.rs πŸ”—

@@ -24,7 +24,7 @@ pub struct Option_ {
     pub label: Option<String>,
 
     /// The value returned to the server when selecting this option.
-    #[xml(extract(namespace = ns::DATA_FORMS, name = "value", fields(text)))]
+    #[xml(extract(fields(text)))]
     pub value: String,
 }
 

parsers/src/mix.rs πŸ”—

@@ -38,11 +38,11 @@ generate_id!(
 #[xml(namespace = ns::MIX_CORE, name = "participant")]
 pub struct Participant {
     /// The nick of this participant.
-    #[xml(extract(namespace = ns::MIX_CORE, name = "nick", fields(text)))]
+    #[xml(extract(fields(text)))]
     pub nick: String,
 
     /// The bare JID of this participant.
-    #[xml(extract(namespace = ns::MIX_CORE, name = "jid", fields(text)))]
+    #[xml(extract(fields(text)))]
     pub jid: BareJid,
 }
 
@@ -85,7 +85,7 @@ pub struct Join {
     pub id: Option<ParticipantId>,
 
     /// The nick requested by the user or set by the service.
-    #[xml(extract(namespace = ns::MIX_CORE, name = "nick", fields(text)))]
+    #[xml(extract(fields(text)))]
     pub nick: String,
 
     /// Which MIX nodes to subscribe to.
@@ -164,7 +164,7 @@ impl IqResultPayload for Leave {}
 #[xml(namespace = ns::MIX_CORE, name = "setnick")]
 pub struct SetNick {
     /// The new requested nick.
-    #[xml(extract(namespace = ns::MIX_CORE, name = "nick", fields(text)))]
+    #[xml(extract(fields(text)))]
     pub nick: String,
 }
 
@@ -184,11 +184,11 @@ impl SetNick {
 #[xml(namespace = ns::MIX_CORE, name = "mix")]
 pub struct Mix {
     /// The nick of the user who said something.
-    #[xml(extract(namespace = ns::MIX_CORE, name = "nick", fields(text)))]
+    #[xml(extract(fields(text)))]
     pub nick: String,
 
     /// The JID of the user who said something.
-    #[xml(extract(namespace = ns::MIX_CORE, name = "jid", fields(text)))]
+    #[xml(extract(fields(text)))]
     pub jid: BareJid,
 }
 

parsers/src/util/macro_tests.rs πŸ”—

@@ -1124,3 +1124,60 @@ fn nested_extract_roundtrip() {
     };
     roundtrip_full::<NestedExtract>("<parent xmlns='urn:example:ns1'><child><grandchild>hello world</grandchild></child></parent>")
 }
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct ExtractOmitNamespace {
+    #[xml(extract(name = "child", fields(text)))]
+    contents: String,
+}
+
+#[test]
+fn extract_omit_namespace_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<ExtractOmitNamespace>(
+        "<parent xmlns='urn:example:ns1'><child>hello world!</child></parent>",
+    )
+}
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct ExtractOmitName {
+    #[xml(extract(namespace = NS1, fields(text)))]
+    contents: String,
+}
+
+#[test]
+fn extract_omit_name_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<ExtractOmitName>(
+        "<parent xmlns='urn:example:ns1'><contents>hello world!</contents></parent>",
+    )
+}
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct ExtractOmitNameAndNamespace {
+    #[xml(extract(fields(text)))]
+    contents: String,
+}
+
+#[test]
+fn extract_omit_name_and_namespace_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<ExtractOmitNameAndNamespace>(
+        "<parent xmlns='urn:example:ns1'><contents>hello world!</contents></parent>",
+    )
+}

xso-proc/src/compound.rs πŸ”—

@@ -12,6 +12,7 @@ use syn::{spanned::Spanned, *};
 
 use crate::error_message::ParentRef;
 use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit};
+use crate::meta::NamespaceRef;
 use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
 use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
 use crate::types::{feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty};
@@ -55,7 +56,10 @@ impl Compound {
     }
 
     /// Construct a compound from fields.
-    pub(crate) fn from_fields(compound_fields: &Fields) -> Result<Self> {
+    pub(crate) fn from_fields(
+        compound_fields: &Fields,
+        container_namespace: &NamespaceRef,
+    ) -> Result<Self> {
         Self::from_field_defs(compound_fields.iter().enumerate().map(|(i, field)| {
             let index = match i.try_into() {
                 Ok(v) => v,
@@ -68,7 +72,7 @@ impl Compound {
                     ))
                 }
             };
-            FieldDef::from_field(field, index)
+            FieldDef::from_field(field, index, container_namespace)
         }))
     }
 

xso-proc/src/enums.rs πŸ”—

@@ -33,7 +33,7 @@ struct NameVariant {
 
 impl NameVariant {
     /// Construct a new name-selected variant from its declaration.
-    fn new(decl: &Variant) -> Result<Self> {
+    fn new(decl: &Variant, enum_namespace: &NamespaceRef) -> Result<Self> {
         // We destructure here so that we get informed when new fields are
         // added and can handle them, either by processing them or raising
         // an error if they are present.
@@ -59,7 +59,7 @@ impl NameVariant {
         Ok(Self {
             name,
             ident: decl.ident.clone(),
-            inner: Compound::from_fields(&decl.fields)?,
+            inner: Compound::from_fields(&decl.fields, enum_namespace)?,
         })
     }
 
@@ -190,7 +190,7 @@ impl EnumDef {
         let mut variants = Vec::new();
         let mut seen_names = HashMap::new();
         for variant in variant_iter {
-            let variant = NameVariant::new(variant)?;
+            let variant = NameVariant::new(variant, &namespace)?;
             if let Some(other) = seen_names.get(&variant.name) {
                 return Err(Error::new_spanned(
                     variant.name,

xso-proc/src/field.rs πŸ”—

@@ -192,6 +192,28 @@ enum FieldKind {
     },
 }
 
+fn default_name(span: Span, name: Option<NameRef>, field_ident: Option<&Ident>) -> Result<NameRef> {
+    match name {
+        Some(v) => Ok(v),
+        None => match field_ident {
+            None => Err(Error::new(
+                span,
+                "name must be explicitly specified with the `name` key on unnamed fields",
+            )),
+            Some(field_ident) => match NcName::try_from(field_ident.to_string()) {
+                Ok(value) => Ok(NameRef::Literal {
+                    span: field_ident.span(),
+                    value,
+                }),
+                Err(e) => Err(Error::new(
+                    field_ident.span(),
+                    format!("invalid XML name: {}", e),
+                )),
+            },
+        },
+    }
+}
+
 impl FieldKind {
     /// Construct a new field implementation from the meta attributes.
     ///
@@ -199,34 +221,22 @@ impl FieldKind {
     /// it is not specified explicitly.
     ///
     /// `field_ty` is needed for type inferrence on extracted fields.
-    fn from_meta(meta: XmlFieldMeta, field_ident: Option<&Ident>, field_ty: &Type) -> Result<Self> {
+    ///
+    /// `container_namespace` is used in some cases to insert a default
+    /// namespace.
+    fn from_meta(
+        meta: XmlFieldMeta,
+        field_ident: Option<&Ident>,
+        field_ty: &Type,
+        container_namespace: &NamespaceRef,
+    ) -> Result<Self> {
         match meta {
             XmlFieldMeta::Attribute {
                 span,
                 qname: QNameRef { namespace, name },
                 default_,
             } => {
-                let xml_name = match name {
-                    Some(v) => v,
-                    None => match field_ident {
-                        None => return Err(Error::new(
-                            span,
-                            "attribute name must be explicitly specified using `#[xml(attribute = ..)] on unnamed fields",
-                        )),
-                        Some(field_ident) => match NcName::try_from(field_ident.to_string()) {
-                            Ok(value) => NameRef::Literal {
-                                span: field_ident.span(),
-                                value,
-                            },
-                            Err(e) => {
-                                return Err(Error::new(
-                                    field_ident.span(),
-                                    format!("invalid XML attribute name: {}", e),
-                                ))
-                            }
-                        },
-                    }
-                };
+                let xml_name = default_name(span, name, field_ident)?;
 
                 Ok(Self::Attribute {
                     xml_name,
@@ -267,19 +277,8 @@ impl FieldKind {
                 qname: QNameRef { namespace, name },
                 fields,
             } => {
-                let Some(xml_namespace) = namespace else {
-                    return Err(Error::new(
-                        span,
-                        "`#[xml(extract(..))]` must contain a `namespace` key.",
-                    ));
-                };
-
-                let Some(xml_name) = name else {
-                    return Err(Error::new(
-                        span,
-                        "`#[xml(extract(..))]` must contain a `name` key.",
-                    ));
-                };
+                let xml_namespace = namespace.unwrap_or_else(|| container_namespace.clone());
+                let xml_name = default_name(span, name, field_ident)?;
 
                 let field = {
                     let mut fields = fields.into_iter();
@@ -301,7 +300,7 @@ impl FieldKind {
                 };
 
                 let parts = Compound::from_field_defs(
-                    [FieldDef::from_extract(field, 0, field_ty)].into_iter(),
+                    [FieldDef::from_extract(field, 0, field_ty, &xml_namespace)].into_iter(),
                 )?;
 
                 Ok(Self::Extract {
@@ -334,7 +333,11 @@ impl FieldDef {
     ///
     /// The `index` must be the zero-based index of the field even for named
     /// fields.
-    pub(crate) fn from_field(field: &syn::Field, index: u32) -> Result<Self> {
+    pub(crate) fn from_field(
+        field: &syn::Field,
+        index: u32,
+        container_namespace: &NamespaceRef,
+    ) -> Result<Self> {
         let field_span = field.span();
         let meta = XmlFieldMeta::parse_from_attributes(&field.attrs, &field_span)?;
 
@@ -352,7 +355,7 @@ impl FieldDef {
         let ty = field.ty.clone();
 
         Ok(Self {
-            kind: FieldKind::from_meta(meta, ident, &ty)?,
+            kind: FieldKind::from_meta(meta, ident, &ty, container_namespace)?,
             member,
             ty,
         })
@@ -362,12 +365,17 @@ impl FieldDef {
     ///
     /// The `index` must be the zero-based index of the field even for named
     /// fields.
-    pub(crate) fn from_extract(meta: XmlFieldMeta, index: u32, ty: &Type) -> Result<Self> {
+    pub(crate) fn from_extract(
+        meta: XmlFieldMeta,
+        index: u32,
+        ty: &Type,
+        container_namespace: &NamespaceRef,
+    ) -> Result<Self> {
         let span = meta.span();
         Ok(Self {
             member: Member::Unnamed(Index { index, span }),
             ty: ty.clone(),
-            kind: FieldKind::from_meta(meta, None, ty)?,
+            kind: FieldKind::from_meta(meta, None, ty, container_namespace)?,
         })
     }
 

xso-proc/src/meta.rs πŸ”—

@@ -61,7 +61,7 @@ macro_rules! reject_key {
 pub(crate) use reject_key;
 
 /// Value for the `#[xml(namespace = ..)]` attribute.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) enum NamespaceRef {
     /// The XML namespace is specified as a string literal.
     LitStr(LitStr),

xso-proc/src/structs.rs πŸ”—

@@ -75,9 +75,9 @@ impl StructDef {
         };
 
         Ok(Self {
+            inner: Compound::from_fields(fields, &namespace)?,
             namespace,
             name,
-            inner: Compound::from_fields(fields)?,
             target_ty_ident: ident.clone(),
             builder_ty_ident,
             item_iter_ty_ident,

xso/src/from_xml_doc.md πŸ”—

@@ -316,6 +316,12 @@ expanded to the corresponding namespace URI and the value for the `namespace`
 key is implied. Mixing a prefixed name with an explicit `namespace` key is
 not allowed.
 
+Both `namespace` and `name` may be omitted. If `namespace` is omitted, it
+defaults to the namespace of the surrounding container. If `name` is omitted
+and the `extract` meta is being used on a named field, that field's name is
+used. If `name` is omitted and `extract` is not used on a named field, an
+error is emitted.
+
 The sequence of field meta inside `fields` can be thought of as a nameless
 tuple-style struct. The macro generates serialisation/deserialisation code
 for that nameless tuple-style struct and uses it to serialise/deserialise