xso-proc: Add support for the codec field on attribute meta
This allows a custom TextCodec to be used for encoding and decoding the attribute’s value, instead of FromXmlText and AsOptionalXmlText.
This commit is contained in:
parent
763a603e49
commit
01a0c51a2f
6 changed files with 114 additions and 5 deletions
|
|
@ -388,6 +388,57 @@ fn default_attribute_roundtrip_pp() {
|
||||||
roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1' foo='xyz' bar='16'/>");
|
roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1' foo='xyz' bar='16'/>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||||
|
#[xml(namespace = NS1, name = "attr")]
|
||||||
|
struct AttributeWithCodec {
|
||||||
|
#[xml(attribute(default, codec = xso::text::EmptyAsNone))]
|
||||||
|
foo: core::option::Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_with_codec_is_none() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
let el = parse_str::<AttributeWithCodec>("<attr xmlns='urn:example:ns1'/>").unwrap();
|
||||||
|
assert_eq!(el.foo, None);
|
||||||
|
let el = parse_str::<AttributeWithCodec>("<attr xmlns='urn:example:ns1' foo=''/>").unwrap();
|
||||||
|
assert_eq!(el.foo, None);
|
||||||
|
let el = parse_str::<AttributeWithCodec>("<attr xmlns='urn:example:ns1' foo='bar'/>").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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_with_base64_codec_roundtrip() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
roundtrip_full::<AttributeWithBase64Codec>("<attr xmlns='urn:example:ns1' foo='AAAA'/>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_with_base64_codec_decodes() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
let el = parse_str::<AttributeWithBase64Codec>("<attr xmlns='urn:example:ns1' foo='AAAA'/>")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(el.foo, [0, 0, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||||
#[xml(namespace = NS1, name = "text")]
|
#[xml(namespace = NS1, name = "text")]
|
||||||
struct TextString {
|
struct TextString {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@ use syn::*;
|
||||||
use crate::error_message::{self, ParentRef};
|
use crate::error_message::{self, ParentRef};
|
||||||
use crate::meta::{Flag, NameRef, NamespaceRef};
|
use crate::meta::{Flag, NameRef, NamespaceRef};
|
||||||
use crate::scope::{AsItemsScope, FromEventsScope};
|
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};
|
use super::{Field, FieldBuilderPart, FieldIteratorPart, FieldTempInit};
|
||||||
|
|
||||||
|
|
@ -29,6 +32,9 @@ pub(super) struct AttributeField {
|
||||||
/// Flag indicating whether the value should be defaulted if the
|
/// Flag indicating whether the value should be defaulted if the
|
||||||
/// attribute is absent.
|
/// attribute is absent.
|
||||||
pub(super) default_: Flag,
|
pub(super) default_: Flag,
|
||||||
|
|
||||||
|
/// Optional codec to use.
|
||||||
|
pub(super) codec: Option<Expr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Field for AttributeField {
|
impl Field for AttributeField {
|
||||||
|
|
@ -53,7 +59,18 @@ impl Field for AttributeField {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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());
|
let from_xml_text = from_xml_text_fn(ty.clone());
|
||||||
|
quote! { #from_xml_text }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let on_absent = match self.default_ {
|
let on_absent = match self.default_ {
|
||||||
Flag::Absent => quote! {
|
Flag::Absent => quote! {
|
||||||
|
|
@ -70,7 +87,7 @@ impl Field for AttributeField {
|
||||||
Ok(FieldBuilderPart::Init {
|
Ok(FieldBuilderPart::Init {
|
||||||
value: FieldTempInit {
|
value: FieldTempInit {
|
||||||
init: quote! {
|
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::Some(v) => v,
|
||||||
::core::option::Option::None => #on_absent,
|
::core::option::Option::None => #on_absent,
|
||||||
}
|
}
|
||||||
|
|
@ -96,11 +113,20 @@ impl Field for AttributeField {
|
||||||
};
|
};
|
||||||
let xml_name = &self.xml_name;
|
let xml_name = &self.xml_name;
|
||||||
|
|
||||||
|
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());
|
let as_optional_xml_text = as_optional_xml_text_fn(ty.clone());
|
||||||
|
quote! { #as_optional_xml_text(#bound_name)? }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(FieldIteratorPart::Header {
|
Ok(FieldIteratorPart::Header {
|
||||||
generator: quote! {
|
generator: quote! {
|
||||||
#as_optional_xml_text(#bound_name)?.map(|#bound_name| ::xso::Item::Attribute(
|
#generator.map(|#bound_name| ::xso::Item::Attribute(
|
||||||
#xml_namespace,
|
#xml_namespace,
|
||||||
::std::borrow::Cow::Borrowed(#xml_name),
|
::std::borrow::Cow::Borrowed(#xml_name),
|
||||||
#bound_name,
|
#bound_name,
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,7 @@ fn new_field(
|
||||||
qname: QNameRef { namespace, name },
|
qname: QNameRef { namespace, name },
|
||||||
default_,
|
default_,
|
||||||
type_,
|
type_,
|
||||||
|
codec,
|
||||||
} => {
|
} => {
|
||||||
let xml_name = default_name(span, name, field_ident)?;
|
let xml_name = default_name(span, name, field_ident)?;
|
||||||
|
|
||||||
|
|
@ -270,6 +271,7 @@ fn new_field(
|
||||||
xml_name,
|
xml_name,
|
||||||
xml_namespace: namespace,
|
xml_namespace: namespace,
|
||||||
default_,
|
default_,
|
||||||
|
codec,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -689,6 +689,9 @@ pub(crate) enum XmlFieldMeta {
|
||||||
|
|
||||||
/// An explicit type override, only usable within extracts.
|
/// An explicit type override, only usable within extracts.
|
||||||
type_: Option<Type>,
|
type_: Option<Type>,
|
||||||
|
|
||||||
|
/// The path to the optional codec type.
|
||||||
|
codec: Option<Expr>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// `#[xml(text)]`
|
/// `#[xml(text)]`
|
||||||
|
|
@ -787,12 +790,14 @@ impl XmlFieldMeta {
|
||||||
},
|
},
|
||||||
default_: Flag::Absent,
|
default_: Flag::Absent,
|
||||||
type_: None,
|
type_: None,
|
||||||
|
codec: None,
|
||||||
})
|
})
|
||||||
} else if meta.input.peek(syn::token::Paren) {
|
} else if meta.input.peek(syn::token::Paren) {
|
||||||
// full syntax
|
// full syntax
|
||||||
let mut qname = QNameRef::default();
|
let mut qname = QNameRef::default();
|
||||||
let mut default_ = Flag::Absent;
|
let mut default_ = Flag::Absent;
|
||||||
let mut type_ = None;
|
let mut type_ = None;
|
||||||
|
let mut codec = None;
|
||||||
meta.parse_nested_meta(|meta| {
|
meta.parse_nested_meta(|meta| {
|
||||||
if meta.path.is_ident("default") {
|
if meta.path.is_ident("default") {
|
||||||
if default_.is_set() {
|
if default_.is_set() {
|
||||||
|
|
@ -806,6 +811,23 @@ impl XmlFieldMeta {
|
||||||
}
|
}
|
||||||
type_ = Some(meta.value()?.parse()?);
|
type_ = Some(meta.value()?.parse()?);
|
||||||
Ok(())
|
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 {
|
} else {
|
||||||
match qname.parse_incremental_from_meta(meta)? {
|
match qname.parse_incremental_from_meta(meta)? {
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
|
|
@ -818,6 +840,7 @@ impl XmlFieldMeta {
|
||||||
qname,
|
qname,
|
||||||
default_,
|
default_,
|
||||||
type_,
|
type_,
|
||||||
|
codec,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// argument-less syntax
|
// argument-less syntax
|
||||||
|
|
@ -826,6 +849,7 @@ impl XmlFieldMeta {
|
||||||
qname: QNameRef::default(),
|
qname: QNameRef::default(),
|
||||||
default_: Flag::Absent,
|
default_: Flag::Absent,
|
||||||
type_: None,
|
type_: None,
|
||||||
|
codec: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ Version NEXT:
|
||||||
translated into a boolean (`#[xml(flag)]`).
|
translated into a boolean (`#[xml(flag)]`).
|
||||||
- Generic TextCodec implementation for all base64 engines provided by
|
- Generic TextCodec implementation for all base64 engines provided by
|
||||||
the base64 crate (if the `base64` feature is enabled).
|
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:
|
Version 0.1.2:
|
||||||
2024-07-26 Jonas Schäfer <jonas@zombofant.net>
|
2024-07-26 Jonas Schäfer <jonas@zombofant.net>
|
||||||
|
|
|
||||||
|
|
@ -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`. |
|
| `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. |
|
| `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(..)))]`. |
|
| `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
|
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
|
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
|
`#[xml(extract(fields(..)))]` meta, the specified type is used instead of the
|
||||||
field type on which the `extract` is declared.
|
field type on which the `extract` is declared.
|
||||||
|
|
||||||
|
If `codec` is given, the given `codec` value must implement
|
||||||
|
[`TextCodec<T>`][`TextCodec`] where `T` is the type of the field.
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue