xso: add support for post-deserialization callback

Jonas SchΓ€fer created

Change summary

parsers/src/util/macro_tests.rs | 137 +++++++++++++++++++++++++++++++++++
xso-proc/src/enums.rs           |  12 +++
xso-proc/src/field/child.rs     |   1 
xso-proc/src/meta.rs            |  14 +++
xso-proc/src/state.rs           |  18 ++++
xso-proc/src/structs.rs         |   8 ++
xso/ChangeLog                   |   1 
xso/src/from_xml_doc.md         |   3 
8 files changed, 193 insertions(+), 1 deletion(-)

Detailed changes

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

@@ -2299,3 +2299,140 @@ fn discard_text_absent_roundtrip() {
     };
     roundtrip_full::<DiscardText>("<foo xmlns='urn:example:ns1'/>");
 }
+
+fn transform_test_struct(
+    v: &mut DeserializeCallback,
+) -> ::core::result::Result<(), ::xso::error::Error> {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    use xso::error::Error;
+    if v.outcome == 0 {
+        return Err(Error::Other("saw outcome == 0"));
+    }
+    if v.outcome == 1 {
+        v.outcome = 0;
+    }
+    Ok(())
+}
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "foo", deserialize_callback = transform_test_struct)]
+struct DeserializeCallback {
+    #[xml(attribute)]
+    outcome: u32,
+}
+
+#[test]
+fn deserialize_callback_roundtrip() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<DeserializeCallback>("<foo xmlns='urn:example:ns1' outcome='2'/>");
+}
+
+#[test]
+fn deserialize_callback_can_mutate() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<DeserializeCallback>("<foo xmlns='urn:example:ns1' outcome='1'/>") {
+        Ok(DeserializeCallback { outcome }) => {
+            assert_eq!(outcome, 0);
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn deserialize_callback_can_fail() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<DeserializeCallback>("<foo xmlns='urn:example:ns1' outcome='0'/>") {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => {
+            assert_eq!(e, "saw outcome == 0");
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+fn transform_test_enum(
+    v: &mut DeserializeCallbackEnum,
+) -> ::core::result::Result<(), ::xso::error::Error> {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    use xso::error::Error;
+    match v {
+        DeserializeCallbackEnum::Foo { ref mut outcome } => {
+            if *outcome == 0 {
+                return Err(Error::Other("saw outcome == 0"));
+            }
+            if *outcome == 1 {
+                *outcome = 0;
+            }
+            Ok(())
+        }
+    }
+}
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, deserialize_callback = transform_test_enum)]
+enum DeserializeCallbackEnum {
+    #[xml(name = "foo")]
+    Foo {
+        #[xml(attribute)]
+        outcome: u32,
+    },
+}
+
+#[test]
+fn enum_deserialize_callback_roundtrip() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<DeserializeCallbackEnum>("<foo xmlns='urn:example:ns1' outcome='2'/>");
+}
+
+#[test]
+fn enum_deserialize_callback_can_mutate() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<DeserializeCallbackEnum>("<foo xmlns='urn:example:ns1' outcome='1'/>") {
+        Ok(DeserializeCallbackEnum::Foo { outcome }) => {
+            assert_eq!(outcome, 0);
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn enum_deserialize_callback_can_fail() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<DeserializeCallbackEnum>("<foo xmlns='urn:example:ns1' outcome='0'/>") {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => {
+            assert_eq!(e, "saw outcome == 0");
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}

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

@@ -50,6 +50,7 @@ impl NameVariant {
             on_unknown_child,
             transparent,
             discard,
+            deserialize_callback,
         } = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
 
         reject_key!(debug flag not on "enum variants" only on "enums and structs");
@@ -58,6 +59,7 @@ impl NameVariant {
         reject_key!(builder not on "enum variants" only on "enums and structs");
         reject_key!(iterator not on "enum variants" only on "enums and structs");
         reject_key!(transparent flag not on "named enum variants" only on "structs");
+        reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
 
         let Some(name) = name else {
             return Err(Error::new(meta_span, "`name` is required on enum variants"));
@@ -278,12 +280,14 @@ impl DynamicVariant {
             on_unknown_child: _,     // used by StructInner
             transparent: _,          // used by StructInner
             discard: _,              // used by StructInner
+            ref deserialize_callback,
         } = meta;
 
         reject_key!(debug flag not on "enum variants" only on "enums and structs");
         reject_key!(exhaustive flag not on "enum variants" only on "enums");
         reject_key!(builder not on "enum variants" only on "enums and structs");
         reject_key!(iterator not on "enum variants" only on "enums and structs");
+        reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
 
         let inner = StructInner::new(meta, &variant.fields)?;
         Ok(Self { ident, inner })
@@ -395,6 +399,7 @@ impl EnumInner {
             on_unknown_child,
             transparent,
             discard,
+            deserialize_callback,
         } = meta;
 
         // These must've been cleared by the caller. Because these being set
@@ -403,6 +408,7 @@ impl EnumInner {
         assert!(builder.is_none());
         assert!(iterator.is_none());
         assert!(!debug.is_set());
+        assert!(deserialize_callback.is_none());
 
         reject_key!(name not on "enums" only on "their variants");
         reject_key!(transparent flag not on "enums" only on "structs");
@@ -476,6 +482,9 @@ pub(crate) struct EnumDef {
 
     /// Flag whether debug mode is enabled.
     debug: bool,
+
+    /// Optional validator function to call.
+    deserialize_callback: Option<Path>,
 }
 
 impl EnumDef {
@@ -496,6 +505,7 @@ impl EnumDef {
         };
 
         let debug = meta.debug.take().is_set();
+        let deserialize_callback = meta.deserialize_callback.take();
 
         Ok(Self {
             inner: EnumInner::new(meta, variant_iter)?,
@@ -503,6 +513,7 @@ impl EnumDef {
             builder_ty_ident,
             item_iter_ty_ident,
             debug,
+            deserialize_callback,
         })
     }
 }
@@ -530,6 +541,7 @@ impl ItemDef for EnumDef {
                     path: target_ty_ident.clone().into(),
                 }
                 .into(),
+                self.deserialize_callback.as_ref(),
             )?;
 
         Ok(FromXmlParts {

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

@@ -303,6 +303,7 @@ impl ExtractDef {
             &from_xml_builder_ty_ident,
             &state_ty_ident,
             &self.parts.to_tuple_ty().into(),
+            None,
         )?;
         let from_xml_builder_ty = ty_from_ident(from_xml_builder_ty_ident.clone()).into();
 

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

@@ -447,6 +447,9 @@ pub(crate) struct XmlCompoundMeta {
 
     /// Items to discard.
     pub(crate) discard: Vec<DiscardSpec>,
+
+    /// The value assigned to `deserialize_callback` inside `#[xml(..)]`, if any.
+    pub(crate) deserialize_callback: Option<Path>,
 }
 
 impl XmlCompoundMeta {
@@ -464,6 +467,7 @@ impl XmlCompoundMeta {
         let mut exhaustive = Flag::Absent;
         let mut transparent = Flag::Absent;
         let mut discard = Vec::new();
+        let mut deserialize_callback = None;
 
         attr.parse_nested_meta(|meta| {
             if meta.path.is_ident("debug") {
@@ -520,6 +524,15 @@ impl XmlCompoundMeta {
                     Ok(())
                 })?;
                 Ok(())
+            } else if meta.path.is_ident("deserialize_callback") {
+                if deserialize_callback.is_some() {
+                    return Err(Error::new_spanned(
+                        meta.path,
+                        "duplicate `deserialize_callback` key",
+                    ));
+                }
+                deserialize_callback = Some(meta.value()?.parse()?);
+                Ok(())
             } else {
                 match qname.parse_incremental_from_meta(meta)? {
                     None => Ok(()),
@@ -539,6 +552,7 @@ impl XmlCompoundMeta {
             exhaustive,
             transparent,
             discard,
+            deserialize_callback,
         })
     }
 

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

@@ -456,6 +456,7 @@ impl FromEventsStateMachine {
         builder_ty_ident: &Ident,
         state_ty_ident: &Ident,
         output_ty: &Type,
+        validate_fn: Option<&Path>,
     ) -> Result<TokenStream> {
         let Self {
             defs,
@@ -488,6 +489,18 @@ impl FromEventsStateMachine {
 
         let docstr = format!("Build a {0} from XML events.\n\nThis type is generated using the [`macro@xso::FromXml`] derive macro and implements [`xso::FromEventsBuilder`] for {0}.", output_ty_ref);
 
+        let validate_call = match validate_fn {
+            None => quote! {
+                // needed to avoid unused_mut warning.
+                let _ = &mut value;
+            },
+            Some(validate_fn) => {
+                quote! {
+                    #validate_fn(&mut value)?;
+                }
+            }
+        };
+
         Ok(quote! {
             #defs
 
@@ -529,7 +542,10 @@ impl FromEventsStateMachine {
                 fn feed(&mut self, ev: ::xso::exports::rxml::Event) -> ::core::result::Result<::core::option::Option<Self::Output>, ::xso::error::Error> {
                     let inner = self.0.take().expect("feed called after completion");
                     match inner.advance(ev)? {
-                        ::core::ops::ControlFlow::Continue(value) => ::core::result::Result::Ok(::core::option::Option::Some(value)),
+                        ::core::ops::ControlFlow::Continue(mut value) => {
+                            #validate_call
+                            ::core::result::Result::Ok(::core::option::Option::Some(value))
+                        }
                         ::core::ops::ControlFlow::Break(st) => {
                             self.0 = ::core::option::Option::Some(st);
                             ::core::result::Result::Ok(::core::option::Option::None)

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

@@ -73,6 +73,7 @@ impl StructInner {
             on_unknown_child,
             transparent,
             discard,
+            deserialize_callback,
         } = meta;
 
         // These must've been cleared by the caller. Because these being set
@@ -81,6 +82,7 @@ impl StructInner {
         assert!(builder.is_none());
         assert!(iterator.is_none());
         assert!(!debug.is_set());
+        assert!(deserialize_callback.is_none());
 
         reject_key!(exhaustive flag not on "structs" only on "enums");
 
@@ -322,6 +324,9 @@ pub(crate) struct StructDef {
 
     /// The matching logic and contents of the struct.
     inner: StructInner,
+
+    /// Optional validator function to call.
+    deserialize_callback: Option<Path>,
 }
 
 impl StructDef {
@@ -338,6 +343,7 @@ impl StructDef {
         };
 
         let debug = meta.debug.take();
+        let deserialize_callback = meta.deserialize_callback.take();
 
         let inner = StructInner::new(meta, fields)?;
 
@@ -347,6 +353,7 @@ impl StructDef {
             builder_ty_ident,
             item_iter_ty_ident,
             debug: debug.is_set(),
+            deserialize_callback,
         })
     }
 }
@@ -379,6 +386,7 @@ impl ItemDef for StructDef {
                     path: target_ty_ident.clone().into(),
                 }
                 .into(),
+                self.deserialize_callback.as_ref(),
             )?;
 
         Ok(FromXmlParts {

xso/ChangeLog πŸ”—

@@ -40,6 +40,7 @@ Version NEXT:
         parsing (!552).
       - Implement `AsXml` and `FromXml` for serde_json::Value` behind
         `serde_json` feature.
+      - Support for a post-deserialization callback function call. (!553)
     * Changes
       - Generated AsXml iterator and FromXml builder types are now
         doc(hidden), to not clutter hand-written documentation with auto

xso/src/from_xml_doc.md πŸ”—

@@ -71,6 +71,7 @@ The following keys are defined on structs:
 | `on_unknown_attribute` | optional *ident* | Name of an [`UnknownAttributePolicy`] member, controlling how unknown attributes are handled. |
 | `on_unknown_child` | optional *ident* | Name of an [`UnknownChildPolicy`] member, controlling how unknown children are handled. |
 | `discard` | optional *nested* | Contains field specifications of content to ignore. See below for details. |
+| `deserialize_callback` | optional *path* | Path to a `fn(&mut T) -> Result<(), Error>` which is called on the deserialized struct after deserialization. |
 
 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
@@ -142,6 +143,7 @@ The following keys are defined on name-switched enums:
 | `iterator` | optional *ident* | The name to use for the generated iterator type. |
 | `exhaustive` | *flag* | If present, the enum considers itself authoritative for its namespace; unknown elements within the namespace are rejected instead of treated as mismatch. |
 | `discard` | optional *nested* | Contains field specifications of content to ignore. See the struct meta docs for details. |
+| `deserialize_callback` | optional *path* | Path to a `fn(&mut T) -> Result<(), Error>` which is called on the deserialized enum after deserialization. |
 
 All variants of a name-switched enum live within the same namespace and are
 distinguished exclusively by their XML name within that namespace. The
@@ -211,6 +213,7 @@ The following keys are defined on dynamic enums:
 | `builder` | optional *ident* | The name to use for the generated builder type. |
 | `iterator` | optional *ident* | The name to use for the generated iterator type. |
 | `discard` | optional *nested* | Contains field specifications of content to ignore. See the struct meta docs for details. |
+| `deserialize_callback` | optional *path* | Path to a `fn(&mut T) -> Result<(), Error>` which is called on the deserialized enum after deserialization. |
 
 For details on `builder` and `iterator`, see the [Struct meta](#struct-meta)
 documentation above.