From 01a0c51a2f67a1b4186dff4566aa4506c031d949 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Sat, 25 Jan 2025 16:21:22 +0100 Subject: [PATCH] xso-proc: Add support for the codec field on attribute meta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows a custom TextCodec to be used for encoding and decoding the attribute’s value, instead of FromXmlText and AsOptionalXmlText. --- parsers/src/util/macro_tests.rs | 51 +++++++++++++++++++++++++++++++++ xso-proc/src/field/attribute.rs | 36 +++++++++++++++++++---- xso-proc/src/field/mod.rs | 2 ++ xso-proc/src/meta.rs | 24 ++++++++++++++++ xso/ChangeLog | 2 ++ xso/src/from_xml_doc.md | 4 +++ 6 files changed, 114 insertions(+), 5 deletions(-) diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 1d8eb64da1d296c52a9ca7157ccf6884b4cfd73c..98bc0a8ea199f510f88ac30430f58d7c519517ac 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -388,6 +388,57 @@ fn default_attribute_roundtrip_pp() { roundtrip_full::(""); } +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = NS1, name = "attr")] +struct AttributeWithCodec { + #[xml(attribute(default, codec = xso::text::EmptyAsNone))] + foo: core::option::Option, +} + +#[test] +fn attribute_with_codec_is_none() { + #[allow(unused_imports)] + use core::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + let el = parse_str::("").unwrap(); + assert_eq!(el.foo, None); + let el = parse_str::("").unwrap(); + assert_eq!(el.foo, None); + let el = parse_str::("").unwrap(); + assert_eq!(el.foo, Some(String::from("bar"))); +} + +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = NS1, name = "attr")] +struct AttributeWithBase64Codec { + #[xml(attribute(codec = xso::text::Base64))] + foo: Vec, +} + +#[test] +fn attribute_with_base64_codec_roundtrip() { + #[allow(unused_imports)] + use core::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} + +#[test] +fn attribute_with_base64_codec_decodes() { + #[allow(unused_imports)] + use core::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + let el = parse_str::("") + .unwrap(); + assert_eq!(el.foo, [0, 0, 0]); +} + #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "text")] struct TextString { diff --git a/xso-proc/src/field/attribute.rs b/xso-proc/src/field/attribute.rs index 950d9d6c2d767ee79999fba3e4d6d6bb7602a2e9..f9d15456540527d1a06654409fb9fcfce58522cb 100644 --- a/xso-proc/src/field/attribute.rs +++ b/xso-proc/src/field/attribute.rs @@ -14,7 +14,10 @@ use syn::*; use crate::error_message::{self, ParentRef}; use crate::meta::{Flag, NameRef, NamespaceRef}; use crate::scope::{AsItemsScope, FromEventsScope}; -use crate::types::{as_optional_xml_text_fn, default_fn, from_xml_text_fn}; +use crate::types::{ + as_optional_xml_text_fn, default_fn, from_xml_text_fn, text_codec_decode_fn, + text_codec_encode_fn, +}; use super::{Field, FieldBuilderPart, FieldIteratorPart, FieldTempInit}; @@ -29,6 +32,9 @@ pub(super) struct AttributeField { /// Flag indicating whether the value should be defaulted if the /// attribute is absent. pub(super) default_: Flag, + + /// Optional codec to use. + pub(super) codec: Option, } impl Field for AttributeField { @@ -53,7 +59,18 @@ impl Field for AttributeField { }, }; - let from_xml_text = from_xml_text_fn(ty.clone()); + let finalize = match self.codec { + Some(ref codec) => { + let decode = text_codec_decode_fn(ty.clone()); + quote! { + |value| #decode(&#codec, value) + } + } + None => { + let from_xml_text = from_xml_text_fn(ty.clone()); + quote! { #from_xml_text } + } + }; let on_absent = match self.default_ { Flag::Absent => quote! { @@ -70,7 +87,7 @@ impl Field for AttributeField { Ok(FieldBuilderPart::Init { value: FieldTempInit { init: quote! { - match #attrs.remove(#xml_namespace, #xml_name).map(#from_xml_text).transpose()? { + match #attrs.remove(#xml_namespace, #xml_name).map(#finalize).transpose()? { ::core::option::Option::Some(v) => v, ::core::option::Option::None => #on_absent, } @@ -96,11 +113,20 @@ impl Field for AttributeField { }; let xml_name = &self.xml_name; - let as_optional_xml_text = as_optional_xml_text_fn(ty.clone()); + let generator = match self.codec { + Some(ref codec) => { + let encode = text_codec_encode_fn(ty.clone()); + quote! { #encode(&#codec, #bound_name)? } + } + None => { + let as_optional_xml_text = as_optional_xml_text_fn(ty.clone()); + quote! { #as_optional_xml_text(#bound_name)? } + } + }; Ok(FieldIteratorPart::Header { generator: quote! { - #as_optional_xml_text(#bound_name)?.map(|#bound_name| ::xso::Item::Attribute( + #generator.map(|#bound_name| ::xso::Item::Attribute( #xml_namespace, ::std::borrow::Cow::Borrowed(#xml_name), #bound_name, diff --git a/xso-proc/src/field/mod.rs b/xso-proc/src/field/mod.rs index 069a7fe40b57988adb7c0c9377a360662f80f349..9b892564d3d8560fd199416454f7e268db964b41 100644 --- a/xso-proc/src/field/mod.rs +++ b/xso-proc/src/field/mod.rs @@ -253,6 +253,7 @@ fn new_field( qname: QNameRef { namespace, name }, default_, type_, + codec, } => { let xml_name = default_name(span, name, field_ident)?; @@ -270,6 +271,7 @@ fn new_field( xml_name, xml_namespace: namespace, default_, + codec, })) } diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 849ea61a605e9768a6c409a2a3a6342f2636acf6..43e7af5489a6bd4acba5e7c07f12ea6e5f40afe5 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -689,6 +689,9 @@ pub(crate) enum XmlFieldMeta { /// An explicit type override, only usable within extracts. type_: Option, + + /// The path to the optional codec type. + codec: Option, }, /// `#[xml(text)]` @@ -787,12 +790,14 @@ impl XmlFieldMeta { }, default_: Flag::Absent, type_: None, + codec: None, }) } else if meta.input.peek(syn::token::Paren) { // full syntax let mut qname = QNameRef::default(); let mut default_ = Flag::Absent; let mut type_ = None; + let mut codec = None; meta.parse_nested_meta(|meta| { if meta.path.is_ident("default") { if default_.is_set() { @@ -806,6 +811,23 @@ impl XmlFieldMeta { } type_ = Some(meta.value()?.parse()?); Ok(()) + } else if meta.path.is_ident("codec") { + if codec.is_some() { + return Err(Error::new_spanned(meta.path, "duplicate `codec` key")); + } + let (new_codec, helpful_error) = parse_codec_expr(meta.value()?)?; + // See the comment at the top of text_from_meta() below for why we + // do this. + let lookahead = meta.input.lookahead1(); + if !lookahead.peek(Token![,]) && !meta.input.is_empty() { + if let Some(helpful_error) = helpful_error { + let mut e = lookahead.error(); + e.combine(helpful_error); + return Err(e); + } + } + codec = Some(new_codec); + Ok(()) } else { match qname.parse_incremental_from_meta(meta)? { None => Ok(()), @@ -818,6 +840,7 @@ impl XmlFieldMeta { qname, default_, type_, + codec, }) } else { // argument-less syntax @@ -826,6 +849,7 @@ impl XmlFieldMeta { qname: QNameRef::default(), default_: Flag::Absent, type_: None, + codec: None, }) } } diff --git a/xso/ChangeLog b/xso/ChangeLog index 796bee953c755ad92e3a18f196ed3758d9d5182a..a72c16aa24c028454997e2a15b7f299062e78a1f 100644 --- a/xso/ChangeLog +++ b/xso/ChangeLog @@ -29,6 +29,8 @@ Version NEXT: translated into a boolean (`#[xml(flag)]`). - Generic TextCodec implementation for all base64 engines provided by the base64 crate (if the `base64` feature is enabled). + - New `codec` field on `attribute` meta, to support decoding and + encoding using any `TextCodec`. Version 0.1.2: 2024-07-26 Jonas Schäfer diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index 5dd4c5f8a870e06aae888bc8a516f40e57d0b181..4dbd1800288d67027d8edd2854351a6dc3cc1283 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -263,6 +263,7 @@ The following keys can be used inside the `#[xml(attribute(..))]` meta: | `name` | optional *string literal* or *path* | The name of the XML attribute to match. If it is a *path*, it must point at a `&'static NcNameStr`. | | `default` | *flag* | If present, an absent attribute will substitute the default value instead of raising an error. | | `type_` | *type* | Optional explicit type specification. Only allowed within `#[xml(extract(fields(..)))]`. | +| `codec` | optional *expression* | [`TextCodec`] implementation which is used to encode or decode the field. | If the `name` key contains a namespace prefix, it must be one of the prefixes defined as built-in in the XML specifications. That prefix will then be @@ -284,6 +285,9 @@ If `type_` is specified and the `text` meta is used within an `#[xml(extract(fields(..)))]` meta, the specified type is used instead of the field type on which the `extract` is declared. +If `codec` is given, the given `codec` value must implement +[`TextCodec`][`TextCodec`] where `T` is the type of the field. + #### Example ```rust