diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 7c44ca384f9349d513a99fbb183bc80f1ae40993..53fd71a7f0185b71f8f2acc047fc079e3c6f1616 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -2299,3 +2299,140 @@ fn discard_text_absent_roundtrip() { }; roundtrip_full::(""); } + +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::(""); +} + +#[test] +fn deserialize_callback_can_mutate() { + #[allow(unused_imports)] + use core::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::("") { + 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::("") { + 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::(""); +} + +#[test] +fn enum_deserialize_callback_can_mutate() { + #[allow(unused_imports)] + use core::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::("") { + 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::("") { + Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => { + assert_eq!(e, "saw outcome == 0"); + } + other => panic!("unexpected result: {:?}", other), + } +} diff --git a/xso-proc/src/enums.rs b/xso-proc/src/enums.rs index 2638cb57b7228f6b68e2a7aa1fbba7d789a2717c..b10c392a6499f6e14c362e942e1cef86059080ac 100644 --- a/xso-proc/src/enums.rs +++ b/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, } 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 { diff --git a/xso-proc/src/field/child.rs b/xso-proc/src/field/child.rs index 268a59f901f2409a306377224b146f6071222d46..79fb4de3e2979947645965f336acd9196b031b7e 100644 --- a/xso-proc/src/field/child.rs +++ b/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(); diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 9ac7fab3b0a2bc338f1f6c778dcd14168ae6bbc0..c2f0951e9009822e2e6506609258b27a82bf80ec 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -447,6 +447,9 @@ pub(crate) struct XmlCompoundMeta { /// Items to discard. pub(crate) discard: Vec, + + /// The value assigned to `deserialize_callback` inside `#[xml(..)]`, if any. + pub(crate) deserialize_callback: Option, } 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, }) } diff --git a/xso-proc/src/state.rs b/xso-proc/src/state.rs index b32fc61b95262928adf51b36f0e3c6f6efc890a6..0c92baa05db9d35576369b2dd6e6461e7d4f37e5 100644 --- a/xso-proc/src/state.rs +++ b/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 { 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, ::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) diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index 0b6de78bce1306e9b26aa8711d25fcd9e03fc205..9e2e8d5edd4c700dfd675a7715f9fcf4262f7ae3 100644 --- a/xso-proc/src/structs.rs +++ b/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, } 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 { diff --git a/xso/ChangeLog b/xso/ChangeLog index 2e5dd300688ba2523115f0bbdbec97172b55567a..edb7e18ba7d57bb6446b44e0bbc0c7b6ef9d295d 100644 --- a/xso/ChangeLog +++ b/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 diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index c5b64d15035882912616e5f3750203154571434a..7744bfd002ac025366e8cb58be111e79ebaad238 100644 --- a/xso/src/from_xml_doc.md +++ b/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.