xso: implement support for extracting data from child elements
This commit is contained in:
parent
5efaabc74b
commit
2c5f1f096b
12 changed files with 728 additions and 52 deletions
|
|
@ -827,3 +827,130 @@ fn children_roundtrip() {
|
||||||
"<parent xmlns='urn:example:ns1'><attr foo='X'/><attr foo='Y'/><attr foo='Z'/></parent>",
|
"<parent xmlns='urn:example:ns1'><attr foo='X'/><attr foo='Y'/><attr foo='Z'/></parent>",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||||
|
#[xml(namespace = NS1, name = "parent")]
|
||||||
|
struct TextExtract {
|
||||||
|
#[xml(extract(namespace = NS1, name = "child", fields(text)))]
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_extract_positive() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<TextExtract>(
|
||||||
|
"<parent xmlns='urn:example:ns1'><child>hello world</child></parent>",
|
||||||
|
) {
|
||||||
|
Ok(TextExtract { contents }) => {
|
||||||
|
assert_eq!(contents, "hello world");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_extract_roundtrip() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
roundtrip_full::<TextExtract>(
|
||||||
|
"<parent xmlns='urn:example:ns1'><child>hello world!</child></parent>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||||
|
#[xml(namespace = NS1, name = "parent")]
|
||||||
|
struct AttributeExtract {
|
||||||
|
#[xml(extract(namespace = NS1, name = "child", fields(attribute = "foo")))]
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_extract_positive() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<AttributeExtract>(
|
||||||
|
"<parent xmlns='urn:example:ns1'><child foo='hello world'/></parent>",
|
||||||
|
) {
|
||||||
|
Ok(AttributeExtract { contents }) => {
|
||||||
|
assert_eq!(contents, "hello world");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_extract_roundtrip() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
roundtrip_full::<AttributeExtract>(
|
||||||
|
"<parent xmlns='urn:example:ns1'><child foo='hello world'/></parent>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||||
|
#[xml(namespace = NS1, name = "parent")]
|
||||||
|
struct ChildExtract {
|
||||||
|
#[xml(extract(namespace = NS1, name = "child", fields(child)))]
|
||||||
|
contents: RequiredAttribute,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn child_extract_roundtrip() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
roundtrip_full::<ChildExtract>(
|
||||||
|
"<parent xmlns='urn:example:ns1'><child><attr foo='hello world!'/></child></parent>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||||
|
#[xml(namespace = NS1, name = "parent")]
|
||||||
|
struct NestedExtract {
|
||||||
|
#[xml(extract(namespace = NS1, name = "child", fields(
|
||||||
|
extract(namespace = NS1, name = "grandchild", fields(text))
|
||||||
|
)))]
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_extract_positive() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<NestedExtract>(
|
||||||
|
"<parent xmlns='urn:example:ns1'><child><grandchild>hello world</grandchild></child></parent>",
|
||||||
|
) {
|
||||||
|
Ok(NestedExtract { contents }) => {
|
||||||
|
assert_eq!(contents, "hello world");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_extract_roundtrip() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
roundtrip_full::<NestedExtract>("<parent xmlns='urn:example:ns1'><child><grandchild>hello world</grandchild></child></parent>")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ impl Compound {
|
||||||
output_name: &ParentRef,
|
output_name: &ParentRef,
|
||||||
state_prefix: &str,
|
state_prefix: &str,
|
||||||
) -> Result<FromEventsSubmachine> {
|
) -> Result<FromEventsSubmachine> {
|
||||||
let scope = FromEventsScope::new();
|
let scope = FromEventsScope::new(state_ty_ident.clone());
|
||||||
let FromEventsScope {
|
let FromEventsScope {
|
||||||
ref attrs,
|
ref attrs,
|
||||||
ref builder_data_ident,
|
ref builder_data_ident,
|
||||||
|
|
@ -106,6 +106,8 @@ impl Compound {
|
||||||
let mut output_cons = TokenStream::default();
|
let mut output_cons = TokenStream::default();
|
||||||
let mut child_matchers = TokenStream::default();
|
let mut child_matchers = TokenStream::default();
|
||||||
let mut text_handler = None;
|
let mut text_handler = None;
|
||||||
|
let mut extra_defs = TokenStream::default();
|
||||||
|
let is_tuple = !output_name.is_path();
|
||||||
|
|
||||||
for (i, field) in self.fields.iter().enumerate() {
|
for (i, field) in self.fields.iter().enumerate() {
|
||||||
let member = field.member();
|
let member = field.member();
|
||||||
|
|
@ -125,9 +127,15 @@ impl Compound {
|
||||||
#builder_field_name: #init,
|
#builder_field_name: #init,
|
||||||
});
|
});
|
||||||
|
|
||||||
output_cons.extend(quote! {
|
if is_tuple {
|
||||||
#member: #builder_data_ident.#builder_field_name,
|
output_cons.extend(quote! {
|
||||||
});
|
#builder_data_ident.#builder_field_name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
output_cons.extend(quote! {
|
||||||
|
#member: #builder_data_ident.#builder_field_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FieldBuilderPart::Text {
|
FieldBuilderPart::Text {
|
||||||
|
|
@ -153,12 +161,20 @@ impl Compound {
|
||||||
Self::#default_state_ident { #builder_data_ident }
|
Self::#default_state_ident { #builder_data_ident }
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
output_cons.extend(quote! {
|
|
||||||
#member: #finalize,
|
if is_tuple {
|
||||||
});
|
output_cons.extend(quote! {
|
||||||
|
#finalize,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
output_cons.extend(quote! {
|
||||||
|
#member: #finalize,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FieldBuilderPart::Nested {
|
FieldBuilderPart::Nested {
|
||||||
|
extra_defs: field_extra_defs,
|
||||||
value: FieldTempInit { ty, init },
|
value: FieldTempInit { ty, init },
|
||||||
matcher,
|
matcher,
|
||||||
builder,
|
builder,
|
||||||
|
|
@ -212,9 +228,17 @@ impl Compound {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
output_cons.extend(quote! {
|
if is_tuple {
|
||||||
#member: #finalize,
|
output_cons.extend(quote! {
|
||||||
});
|
#finalize,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
output_cons.extend(quote! {
|
||||||
|
#member: #finalize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extra_defs.extend(field_extra_defs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -243,6 +267,11 @@ impl Compound {
|
||||||
#path { #output_cons }
|
#path { #output_cons }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ParentRef::Unnamed { .. } => {
|
||||||
|
quote! {
|
||||||
|
( #output_cons )
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
states.push(State::new_with_builder(
|
states.push(State::new_with_builder(
|
||||||
|
|
@ -277,6 +306,8 @@ impl Compound {
|
||||||
|
|
||||||
Ok(FromEventsSubmachine {
|
Ok(FromEventsSubmachine {
|
||||||
defs: quote! {
|
defs: quote! {
|
||||||
|
#extra_defs
|
||||||
|
|
||||||
struct #builder_data_ty {
|
struct #builder_data_ty {
|
||||||
#builder_data_def
|
#builder_data_def
|
||||||
}
|
}
|
||||||
|
|
@ -308,10 +339,11 @@ impl Compound {
|
||||||
pub(crate) fn make_as_item_iter_statemachine(
|
pub(crate) fn make_as_item_iter_statemachine(
|
||||||
&self,
|
&self,
|
||||||
input_name: &ParentRef,
|
input_name: &ParentRef,
|
||||||
|
state_ty_ident: &Ident,
|
||||||
state_prefix: &str,
|
state_prefix: &str,
|
||||||
lifetime: &Lifetime,
|
lifetime: &Lifetime,
|
||||||
) -> Result<AsItemsSubmachine> {
|
) -> Result<AsItemsSubmachine> {
|
||||||
let scope = AsItemsScope::new(lifetime);
|
let scope = AsItemsScope::new(lifetime, state_ty_ident.clone());
|
||||||
|
|
||||||
let element_head_start_state_ident =
|
let element_head_start_state_ident =
|
||||||
quote::format_ident!("{}ElementHeadStart", state_prefix);
|
quote::format_ident!("{}ElementHeadStart", state_prefix);
|
||||||
|
|
@ -322,8 +354,10 @@ impl Compound {
|
||||||
let dummy_ident = quote::format_ident!("dummy");
|
let dummy_ident = quote::format_ident!("dummy");
|
||||||
let mut states = Vec::new();
|
let mut states = Vec::new();
|
||||||
|
|
||||||
|
let is_tuple = !input_name.is_path();
|
||||||
let mut destructure = TokenStream::default();
|
let mut destructure = TokenStream::default();
|
||||||
let mut start_init = TokenStream::default();
|
let mut start_init = TokenStream::default();
|
||||||
|
let mut extra_defs = TokenStream::default();
|
||||||
|
|
||||||
states.push(
|
states.push(
|
||||||
State::new(element_head_start_state_ident.clone())
|
State::new(element_head_start_state_ident.clone())
|
||||||
|
|
@ -345,7 +379,7 @@ impl Compound {
|
||||||
for (i, field) in self.fields.iter().enumerate() {
|
for (i, field) in self.fields.iter().enumerate() {
|
||||||
let member = field.member();
|
let member = field.member();
|
||||||
let bound_name = mangle_member(member);
|
let bound_name = mangle_member(member);
|
||||||
let part = field.make_iterator_part(&scope, &bound_name)?;
|
let part = field.make_iterator_part(&scope, input_name, &bound_name)?;
|
||||||
let state_name = quote::format_ident!("{}Field{}", state_prefix, i);
|
let state_name = quote::format_ident!("{}Field{}", state_prefix, i);
|
||||||
let ty = scope.borrow(field.ty().clone());
|
let ty = scope.borrow(field.ty().clone());
|
||||||
|
|
||||||
|
|
@ -366,9 +400,15 @@ impl Compound {
|
||||||
);
|
);
|
||||||
element_head_end_idx += 1;
|
element_head_end_idx += 1;
|
||||||
|
|
||||||
destructure.extend(quote! {
|
if is_tuple {
|
||||||
#member: ref #bound_name,
|
destructure.extend(quote! {
|
||||||
});
|
ref #bound_name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
destructure.extend(quote! {
|
||||||
|
#member: ref #bound_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
start_init.extend(quote! {
|
start_init.extend(quote! {
|
||||||
#bound_name,
|
#bound_name,
|
||||||
});
|
});
|
||||||
|
|
@ -389,15 +429,22 @@ impl Compound {
|
||||||
))
|
))
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
destructure.extend(quote! {
|
if is_tuple {
|
||||||
#member: #bound_name,
|
destructure.extend(quote! {
|
||||||
});
|
#bound_name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
destructure.extend(quote! {
|
||||||
|
#member: #bound_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
start_init.extend(quote! {
|
start_init.extend(quote! {
|
||||||
#bound_name,
|
#bound_name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
FieldIteratorPart::Content {
|
FieldIteratorPart::Content {
|
||||||
|
extra_defs: field_extra_defs,
|
||||||
value: FieldTempInit { ty, init },
|
value: FieldTempInit { ty, init },
|
||||||
generator,
|
generator,
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -415,12 +462,20 @@ impl Compound {
|
||||||
#generator?
|
#generator?
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
destructure.extend(quote! {
|
if is_tuple {
|
||||||
#member: #bound_name,
|
destructure.extend(quote! {
|
||||||
});
|
#bound_name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
destructure.extend(quote! {
|
||||||
|
#member: #bound_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
start_init.extend(quote! {
|
start_init.extend(quote! {
|
||||||
#bound_name: #init,
|
#bound_name: #init,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
extra_defs.extend(field_extra_defs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -440,14 +495,19 @@ impl Compound {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let ParentRef::Named(input_path) = input_name;
|
let destructure = match input_name {
|
||||||
|
ParentRef::Named(ref input_path) => quote! {
|
||||||
Ok(AsItemsSubmachine {
|
|
||||||
defs: TokenStream::default(),
|
|
||||||
states,
|
|
||||||
destructure: quote! {
|
|
||||||
#input_path { #destructure }
|
#input_path { #destructure }
|
||||||
},
|
},
|
||||||
|
ParentRef::Unnamed { .. } => quote! {
|
||||||
|
( #destructure )
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AsItemsSubmachine {
|
||||||
|
defs: extra_defs,
|
||||||
|
states,
|
||||||
|
destructure,
|
||||||
init: quote! {
|
init: quote! {
|
||||||
Self::#element_head_start_state_ident { #dummy_ident: ::std::marker::PhantomData, #name_ident: name.1, #ns_ident: name.0, #start_init }
|
Self::#element_head_start_state_ident { #dummy_ident: ::std::marker::PhantomData, #name_ident: name.1, #ns_ident: name.0, #start_init }
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use crate::compound::Compound;
|
||||||
use crate::error_message::ParentRef;
|
use crate::error_message::ParentRef;
|
||||||
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
|
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
|
||||||
use crate::state::{AsItemsStateMachine, FromEventsStateMachine};
|
use crate::state::{AsItemsStateMachine, FromEventsStateMachine};
|
||||||
|
use crate::types::{ref_ty, ty_from_ident};
|
||||||
|
|
||||||
/// The definition of an enum variant, switched on the XML element's name.
|
/// The definition of an enum variant, switched on the XML element's name.
|
||||||
struct NameVariant {
|
struct NameVariant {
|
||||||
|
|
@ -103,6 +104,7 @@ impl NameVariant {
|
||||||
&self,
|
&self,
|
||||||
xml_namespace: &NamespaceRef,
|
xml_namespace: &NamespaceRef,
|
||||||
enum_ident: &Ident,
|
enum_ident: &Ident,
|
||||||
|
state_ty_ident: &Ident,
|
||||||
item_iter_ty_lifetime: &Lifetime,
|
item_iter_ty_lifetime: &Lifetime,
|
||||||
) -> Result<AsItemsStateMachine> {
|
) -> Result<AsItemsStateMachine> {
|
||||||
let xml_name = &self.name;
|
let xml_name = &self.name;
|
||||||
|
|
@ -119,6 +121,7 @@ impl NameVariant {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
}),
|
}),
|
||||||
|
state_ty_ident,
|
||||||
&self.ident.to_string(),
|
&self.ident.to_string(),
|
||||||
&item_iter_ty_lifetime,
|
&item_iter_ty_lifetime,
|
||||||
)?
|
)?
|
||||||
|
|
@ -316,17 +319,17 @@ impl ItemDef for EnumDef {
|
||||||
statemachine.merge(variant.make_as_item_iter_statemachine(
|
statemachine.merge(variant.make_as_item_iter_statemachine(
|
||||||
&self.namespace,
|
&self.namespace,
|
||||||
target_ty_ident,
|
target_ty_ident,
|
||||||
|
&state_ty_ident,
|
||||||
&item_iter_ty_lifetime,
|
&item_iter_ty_lifetime,
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
let defs = statemachine.render(
|
let defs = statemachine.render(
|
||||||
vis,
|
vis,
|
||||||
&TypePath {
|
&ref_ty(
|
||||||
qself: None,
|
ty_from_ident(target_ty_ident.clone()).into(),
|
||||||
path: target_ty_ident.clone().into(),
|
item_iter_ty_lifetime.clone(),
|
||||||
}
|
),
|
||||||
.into(),
|
|
||||||
&state_ty_ident,
|
&state_ty_ident,
|
||||||
&item_iter_ty_lifetime,
|
&item_iter_ty_lifetime,
|
||||||
&item_iter_ty,
|
&item_iter_ty,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,25 @@ pub(super) enum ParentRef {
|
||||||
/// The parent is addressable by a path, e.g. a struct type or enum
|
/// The parent is addressable by a path, e.g. a struct type or enum
|
||||||
/// variant.
|
/// variant.
|
||||||
Named(Path),
|
Named(Path),
|
||||||
|
|
||||||
|
/// The parent is not addressable by a path, because it is in fact an
|
||||||
|
/// ephemeral structure.
|
||||||
|
///
|
||||||
|
/// Used to reference the ephemeral structures used by fields declared
|
||||||
|
/// with `#[xml(extract(..))]`.
|
||||||
|
Unnamed {
|
||||||
|
/// The parent's ref.
|
||||||
|
///
|
||||||
|
/// For extracts, this refers to the compound where the field with
|
||||||
|
/// the extract is declared.
|
||||||
|
parent: Box<ParentRef>,
|
||||||
|
|
||||||
|
/// The field inside that parent.
|
||||||
|
///
|
||||||
|
/// For extracts, this refers to the compound field where the extract
|
||||||
|
/// is declared.
|
||||||
|
field: Member,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Path> for ParentRef {
|
impl From<Path> for ParentRef {
|
||||||
|
|
@ -47,6 +66,32 @@ impl fmt::Display for ParentRef {
|
||||||
}
|
}
|
||||||
write!(f, " element")
|
write!(f, " element")
|
||||||
}
|
}
|
||||||
|
Self::Unnamed { parent, field } => {
|
||||||
|
write!(f, "extraction for {} in {}", FieldName(field), parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentRef {
|
||||||
|
/// Create a new `ParentRef` for a member inside this one.
|
||||||
|
///
|
||||||
|
/// Returns a [`Self::Unnamed`] with `self` as parent and `member` as
|
||||||
|
/// field.
|
||||||
|
pub(crate) fn child(&self, member: Member) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Named { .. } | Self::Unnamed { .. } => Self::Unnamed {
|
||||||
|
parent: Box::new(self.clone()),
|
||||||
|
field: member,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true if and only if this ParentRef can be addressed as a path.
|
||||||
|
pub(crate) fn is_path(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Named { .. } => true,
|
||||||
|
Self::Unnamed { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use syn::{spanned::Spanned, *};
|
||||||
|
|
||||||
use rxml_validation::NcName;
|
use rxml_validation::NcName;
|
||||||
|
|
||||||
|
use crate::compound::Compound;
|
||||||
use crate::error_message::{self, ParentRef};
|
use crate::error_message::{self, ParentRef};
|
||||||
use crate::meta::{AmountConstraint, Flag, NameRef, NamespaceRef, QNameRef, XmlFieldMeta};
|
use crate::meta::{AmountConstraint, Flag, NameRef, NamespaceRef, QNameRef, XmlFieldMeta};
|
||||||
use crate::scope::{AsItemsScope, FromEventsScope};
|
use crate::scope::{AsItemsScope, FromEventsScope};
|
||||||
|
|
@ -19,7 +20,7 @@ use crate::types::{
|
||||||
as_optional_xml_text_fn, as_xml_iter_fn, as_xml_text_fn, default_fn, extend_fn, from_events_fn,
|
as_optional_xml_text_fn, as_xml_iter_fn, as_xml_text_fn, default_fn, extend_fn, from_events_fn,
|
||||||
from_xml_builder_ty, from_xml_text_fn, into_iterator_into_iter_fn, into_iterator_item_ty,
|
from_xml_builder_ty, from_xml_text_fn, into_iterator_into_iter_fn, into_iterator_item_ty,
|
||||||
into_iterator_iter_ty, item_iter_ty, option_ty, ref_ty, string_ty, text_codec_decode_fn,
|
into_iterator_iter_ty, item_iter_ty, option_ty, ref_ty, string_ty, text_codec_decode_fn,
|
||||||
text_codec_encode_fn,
|
text_codec_encode_fn, ty_from_ident,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Code slices necessary for declaring and initializing a temporary variable
|
/// Code slices necessary for declaring and initializing a temporary variable
|
||||||
|
|
@ -63,6 +64,10 @@ pub(crate) enum FieldBuilderPart {
|
||||||
|
|
||||||
/// Parse a field from child element events.
|
/// Parse a field from child element events.
|
||||||
Nested {
|
Nested {
|
||||||
|
/// Additional definition items which need to be inserted at module
|
||||||
|
/// level for the rest of the implementation to work.
|
||||||
|
extra_defs: TokenStream,
|
||||||
|
|
||||||
/// Expression and type which initializes a buffer to use during
|
/// Expression and type which initializes a buffer to use during
|
||||||
/// parsing.
|
/// parsing.
|
||||||
value: FieldTempInit,
|
value: FieldTempInit,
|
||||||
|
|
@ -117,6 +122,10 @@ pub(crate) enum FieldIteratorPart {
|
||||||
|
|
||||||
/// The field is emitted as series of items which form a child element.
|
/// The field is emitted as series of items which form a child element.
|
||||||
Content {
|
Content {
|
||||||
|
/// Additional definition items which need to be inserted at module
|
||||||
|
/// level for the rest of the implementation to work.
|
||||||
|
extra_defs: TokenStream,
|
||||||
|
|
||||||
/// Expression and type which initializes the nested iterator.
|
/// Expression and type which initializes the nested iterator.
|
||||||
///
|
///
|
||||||
/// Note that this is evaluated at construction time of the iterator.
|
/// Note that this is evaluated at construction time of the iterator.
|
||||||
|
|
@ -161,6 +170,26 @@ enum FieldKind {
|
||||||
/// Number of child elements allowed.
|
/// Number of child elements allowed.
|
||||||
amount: AmountConstraint,
|
amount: AmountConstraint,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Extract contents from a child element.
|
||||||
|
Extract {
|
||||||
|
/// The XML namespace of the child to extract data from.
|
||||||
|
xml_namespace: NamespaceRef,
|
||||||
|
|
||||||
|
/// The XML name of the child to extract data from.
|
||||||
|
xml_name: NameRef,
|
||||||
|
|
||||||
|
/// Compound which contains the arguments of the `extract(..)` meta
|
||||||
|
/// (except the `from`), transformed into a struct with unnamed
|
||||||
|
/// fields.
|
||||||
|
///
|
||||||
|
/// This is used to generate the parsing/serialisation code, by
|
||||||
|
/// essentially "declaring" a shim struct, as if it were a real Rust
|
||||||
|
/// struct, and using the result of the parsing process directly for
|
||||||
|
/// the field on which the `extract(..)` option was used, instead of
|
||||||
|
/// putting it into a Rust struct.
|
||||||
|
parts: Compound,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FieldKind {
|
impl FieldKind {
|
||||||
|
|
@ -168,7 +197,9 @@ impl FieldKind {
|
||||||
///
|
///
|
||||||
/// `field_ident` is, for some field types, used to infer an XML name if
|
/// `field_ident` is, for some field types, used to infer an XML name if
|
||||||
/// it is not specified explicitly.
|
/// it is not specified explicitly.
|
||||||
fn from_meta(meta: XmlFieldMeta, field_ident: Option<&Ident>) -> Result<Self> {
|
///
|
||||||
|
/// `field_ty` is needed for type inferrence on extracted fields.
|
||||||
|
fn from_meta(meta: XmlFieldMeta, field_ident: Option<&Ident>, field_ty: &Type) -> Result<Self> {
|
||||||
match meta {
|
match meta {
|
||||||
XmlFieldMeta::Attribute {
|
XmlFieldMeta::Attribute {
|
||||||
span,
|
span,
|
||||||
|
|
@ -204,9 +235,13 @@ impl FieldKind {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
XmlFieldMeta::Text { codec } => Ok(Self::Text { codec }),
|
XmlFieldMeta::Text { span: _, codec } => Ok(Self::Text { codec }),
|
||||||
|
|
||||||
XmlFieldMeta::Child { default_, amount } => {
|
XmlFieldMeta::Child {
|
||||||
|
span: _,
|
||||||
|
default_,
|
||||||
|
amount,
|
||||||
|
} => {
|
||||||
if let Some(AmountConstraint::Any(ref amount_span)) = amount {
|
if let Some(AmountConstraint::Any(ref amount_span)) = amount {
|
||||||
if let Flag::Present(ref flag_span) = default_ {
|
if let Flag::Present(ref flag_span) = default_ {
|
||||||
let mut err = Error::new(
|
let mut err = Error::new(
|
||||||
|
|
@ -226,6 +261,55 @@ impl FieldKind {
|
||||||
amount: amount.unwrap_or(AmountConstraint::FixedSingle(Span::call_site())),
|
amount: amount.unwrap_or(AmountConstraint::FixedSingle(Span::call_site())),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
XmlFieldMeta::Extract {
|
||||||
|
span,
|
||||||
|
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 field = {
|
||||||
|
let mut fields = fields.into_iter();
|
||||||
|
let Some(field) = fields.next() else {
|
||||||
|
return Err(Error::new(
|
||||||
|
span,
|
||||||
|
"`#[xml(extract(..))]` must contain one `fields(..)` nested meta which contains at least one field meta."
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(field) = fields.next() {
|
||||||
|
return Err(Error::new(
|
||||||
|
field.span(),
|
||||||
|
"more than one extracted piece of data is currently not supported",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
field
|
||||||
|
};
|
||||||
|
|
||||||
|
let parts = Compound::from_field_defs(
|
||||||
|
[FieldDef::from_extract(field, 0, field_ty)].into_iter(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self::Extract {
|
||||||
|
xml_namespace,
|
||||||
|
xml_name,
|
||||||
|
parts,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -268,9 +352,22 @@ impl FieldDef {
|
||||||
let ty = field.ty.clone();
|
let ty = field.ty.clone();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
kind: FieldKind::from_meta(meta, ident, &ty)?,
|
||||||
member,
|
member,
|
||||||
ty,
|
ty,
|
||||||
kind: FieldKind::from_meta(meta, ident)?,
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new field definition from its declaration.
|
||||||
|
///
|
||||||
|
/// 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> {
|
||||||
|
let span = meta.span();
|
||||||
|
Ok(Self {
|
||||||
|
member: Member::Unnamed(Index { index, span }),
|
||||||
|
ty: ty.clone(),
|
||||||
|
kind: FieldKind::from_meta(meta, None, ty)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,6 +505,7 @@ impl FieldDef {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(FieldBuilderPart::Nested {
|
Ok(FieldBuilderPart::Nested {
|
||||||
|
extra_defs: TokenStream::default(),
|
||||||
value: FieldTempInit {
|
value: FieldTempInit {
|
||||||
init: quote! { ::std::option::Option::None },
|
init: quote! { ::std::option::Option::None },
|
||||||
ty: option_ty(self.ty.clone()),
|
ty: option_ty(self.ty.clone()),
|
||||||
|
|
@ -438,6 +536,7 @@ impl FieldDef {
|
||||||
let ty_extend = extend_fn(self.ty.clone(), element_ty.clone());
|
let ty_extend = extend_fn(self.ty.clone(), element_ty.clone());
|
||||||
let ty_default = default_fn(self.ty.clone());
|
let ty_default = default_fn(self.ty.clone());
|
||||||
Ok(FieldBuilderPart::Nested {
|
Ok(FieldBuilderPart::Nested {
|
||||||
|
extra_defs: TokenStream::default(),
|
||||||
value: FieldTempInit {
|
value: FieldTempInit {
|
||||||
init: quote! { #ty_default() },
|
init: quote! { #ty_default() },
|
||||||
ty: self.ty.clone(),
|
ty: self.ty.clone(),
|
||||||
|
|
@ -452,6 +551,80 @@ impl FieldDef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FieldKind::Extract {
|
||||||
|
ref xml_namespace,
|
||||||
|
ref xml_name,
|
||||||
|
ref parts,
|
||||||
|
} => {
|
||||||
|
let FromEventsScope {
|
||||||
|
ref substate_result,
|
||||||
|
..
|
||||||
|
} = scope;
|
||||||
|
let field_access = scope.access_field(&self.member);
|
||||||
|
|
||||||
|
let missing_msg = error_message::on_missing_child(container_name, &self.member);
|
||||||
|
let duplicate_msg = error_message::on_duplicate_child(container_name, &self.member);
|
||||||
|
|
||||||
|
let on_absent = quote! {
|
||||||
|
return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into())
|
||||||
|
};
|
||||||
|
|
||||||
|
let from_xml_builder_ty_ident =
|
||||||
|
scope.make_member_type_name(&self.member, "FromXmlBuilder");
|
||||||
|
let state_ty_ident = quote::format_ident!("{}State", from_xml_builder_ty_ident,);
|
||||||
|
|
||||||
|
let extra_defs = parts.make_from_events_statemachine(
|
||||||
|
&state_ty_ident,
|
||||||
|
&container_name.child(self.member.clone()),
|
||||||
|
"",
|
||||||
|
)?.with_augmented_init(|init| quote! {
|
||||||
|
if name.0 == #xml_namespace && name.1 == #xml_name {
|
||||||
|
#init
|
||||||
|
} else {
|
||||||
|
::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs })
|
||||||
|
}
|
||||||
|
}).compile().render(
|
||||||
|
&Visibility::Inherited,
|
||||||
|
&from_xml_builder_ty_ident,
|
||||||
|
&state_ty_ident,
|
||||||
|
&Type::Tuple(TypeTuple {
|
||||||
|
paren_token: token::Paren::default(),
|
||||||
|
elems: [
|
||||||
|
self.ty.clone(),
|
||||||
|
].into_iter().collect(),
|
||||||
|
})
|
||||||
|
)?;
|
||||||
|
let from_xml_builder_ty = ty_from_ident(from_xml_builder_ty_ident.clone()).into();
|
||||||
|
|
||||||
|
Ok(FieldBuilderPart::Nested {
|
||||||
|
extra_defs,
|
||||||
|
value: FieldTempInit {
|
||||||
|
init: quote! { ::std::option::Option::None },
|
||||||
|
ty: option_ty(self.ty.clone()),
|
||||||
|
},
|
||||||
|
matcher: quote! {
|
||||||
|
match #state_ty_ident::new(name, attrs) {
|
||||||
|
::core::result::Result::Ok(v) => if #field_access.is_some() {
|
||||||
|
::core::result::Result::Err(::xso::error::FromEventsError::Invalid(::xso::error::Error::Other(#duplicate_msg)))
|
||||||
|
} else {
|
||||||
|
::core::result::Result::Ok(#from_xml_builder_ty_ident(::core::option::Option::Some(v)))
|
||||||
|
},
|
||||||
|
::core::result::Result::Err(e) => ::core::result::Result::Err(e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: from_xml_builder_ty,
|
||||||
|
collect: quote! {
|
||||||
|
#field_access = ::std::option::Option::Some(#substate_result.0);
|
||||||
|
},
|
||||||
|
finalize: quote! {
|
||||||
|
match #field_access {
|
||||||
|
::std::option::Option::Some(value) => value,
|
||||||
|
::std::option::Option::None => #on_absent,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,6 +635,7 @@ impl FieldDef {
|
||||||
pub(crate) fn make_iterator_part(
|
pub(crate) fn make_iterator_part(
|
||||||
&self,
|
&self,
|
||||||
scope: &AsItemsScope,
|
scope: &AsItemsScope,
|
||||||
|
container_name: &ParentRef,
|
||||||
bound_name: &Ident,
|
bound_name: &Ident,
|
||||||
) -> Result<FieldIteratorPart> {
|
) -> Result<FieldIteratorPart> {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
|
|
@ -515,6 +689,7 @@ impl FieldDef {
|
||||||
let item_iter = item_iter_ty(self.ty.clone(), lifetime.clone());
|
let item_iter = item_iter_ty(self.ty.clone(), lifetime.clone());
|
||||||
|
|
||||||
Ok(FieldIteratorPart::Content {
|
Ok(FieldIteratorPart::Content {
|
||||||
|
extra_defs: TokenStream::default(),
|
||||||
value: FieldTempInit {
|
value: FieldTempInit {
|
||||||
init: quote! {
|
init: quote! {
|
||||||
#as_xml_iter(#bound_name)?
|
#as_xml_iter(#bound_name)?
|
||||||
|
|
@ -561,6 +736,7 @@ impl FieldDef {
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(FieldIteratorPart::Content {
|
Ok(FieldIteratorPart::Content {
|
||||||
|
extra_defs: TokenStream::default(),
|
||||||
value: FieldTempInit {
|
value: FieldTempInit {
|
||||||
init: quote! {
|
init: quote! {
|
||||||
(#into_iter(#bound_name), ::core::option::Option::None)
|
(#into_iter(#bound_name), ::core::option::Option::None)
|
||||||
|
|
@ -583,6 +759,70 @@ impl FieldDef {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FieldKind::Extract {
|
||||||
|
ref xml_namespace,
|
||||||
|
ref xml_name,
|
||||||
|
ref parts,
|
||||||
|
} => {
|
||||||
|
let AsItemsScope { ref lifetime, .. } = scope;
|
||||||
|
let item_iter_ty_ident = scope.make_member_type_name(&self.member, "AsXmlIterator");
|
||||||
|
let state_ty_ident = quote::format_ident!("{}State", item_iter_ty_ident,);
|
||||||
|
let mut item_iter_ty = ty_from_ident(item_iter_ty_ident.clone());
|
||||||
|
item_iter_ty.path.segments[0].arguments =
|
||||||
|
PathArguments::AngleBracketed(AngleBracketedGenericArguments {
|
||||||
|
colon2_token: None,
|
||||||
|
lt_token: token::Lt::default(),
|
||||||
|
args: [GenericArgument::Lifetime(lifetime.clone())]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
gt_token: token::Gt::default(),
|
||||||
|
});
|
||||||
|
let item_iter_ty = item_iter_ty.into();
|
||||||
|
|
||||||
|
let extra_defs = parts
|
||||||
|
.make_as_item_iter_statemachine(
|
||||||
|
&container_name.child(self.member.clone()),
|
||||||
|
&state_ty_ident,
|
||||||
|
"",
|
||||||
|
lifetime,
|
||||||
|
)?
|
||||||
|
.with_augmented_init(|init| {
|
||||||
|
quote! {
|
||||||
|
let name = (
|
||||||
|
::xso::exports::rxml::Namespace::from(#xml_namespace),
|
||||||
|
::std::borrow::Cow::Borrowed(#xml_name),
|
||||||
|
);
|
||||||
|
#init
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compile()
|
||||||
|
.render(
|
||||||
|
&Visibility::Inherited,
|
||||||
|
&Type::Tuple(TypeTuple {
|
||||||
|
paren_token: token::Paren::default(),
|
||||||
|
elems: [ref_ty(self.ty.clone(), lifetime.clone())]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
&state_ty_ident,
|
||||||
|
lifetime,
|
||||||
|
&item_iter_ty,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(FieldIteratorPart::Content {
|
||||||
|
extra_defs,
|
||||||
|
value: FieldTempInit {
|
||||||
|
init: quote! {
|
||||||
|
#item_iter_ty_ident::new((&#bound_name,))?
|
||||||
|
},
|
||||||
|
ty: item_iter_ty,
|
||||||
|
},
|
||||||
|
generator: quote! {
|
||||||
|
#bound_name.next().transpose()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -638,18 +638,42 @@ pub(crate) enum XmlFieldMeta {
|
||||||
|
|
||||||
/// `#[xml(text)]`
|
/// `#[xml(text)]`
|
||||||
Text {
|
Text {
|
||||||
|
/// The span of the `#[xml(text)]` meta from which this was parsed.
|
||||||
|
///
|
||||||
|
/// This is useful for error messages.
|
||||||
|
span: Span,
|
||||||
|
|
||||||
/// The path to the optional codec type.
|
/// The path to the optional codec type.
|
||||||
codec: Option<Expr>,
|
codec: Option<Expr>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// `#[xml(child)`
|
/// `#[xml(child)`
|
||||||
Child {
|
Child {
|
||||||
|
/// The span of the `#[xml(child)]` meta from which this was parsed.
|
||||||
|
///
|
||||||
|
/// This is useful for error messages.
|
||||||
|
span: Span,
|
||||||
|
|
||||||
/// The `default` flag.
|
/// The `default` flag.
|
||||||
default_: Flag,
|
default_: Flag,
|
||||||
|
|
||||||
/// The `n` flag.
|
/// The `n` flag.
|
||||||
amount: Option<AmountConstraint>,
|
amount: Option<AmountConstraint>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// `#[xml(extract)]
|
||||||
|
Extract {
|
||||||
|
/// The span of the `#[xml(extract)]` meta from which this was parsed.
|
||||||
|
///
|
||||||
|
/// This is useful for error messages.
|
||||||
|
span: Span,
|
||||||
|
|
||||||
|
/// The namespace/name keys.
|
||||||
|
qname: QNameRef,
|
||||||
|
|
||||||
|
/// The `fields` nested meta.
|
||||||
|
fields: Vec<XmlFieldMeta>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XmlFieldMeta {
|
impl XmlFieldMeta {
|
||||||
|
|
@ -725,7 +749,10 @@ impl XmlFieldMeta {
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Self::Text { codec: Some(codec) })
|
Ok(Self::Text {
|
||||||
|
span: meta.path.span(),
|
||||||
|
codec: Some(codec),
|
||||||
|
})
|
||||||
} else if meta.input.peek(syn::token::Paren) {
|
} else if meta.input.peek(syn::token::Paren) {
|
||||||
let mut codec: Option<Expr> = None;
|
let mut codec: Option<Expr> = None;
|
||||||
meta.parse_nested_meta(|meta| {
|
meta.parse_nested_meta(|meta| {
|
||||||
|
|
@ -750,9 +777,15 @@ impl XmlFieldMeta {
|
||||||
Err(Error::new_spanned(meta.path, "unsupported key"))
|
Err(Error::new_spanned(meta.path, "unsupported key"))
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
Ok(Self::Text { codec })
|
Ok(Self::Text {
|
||||||
|
span: meta.path.span(),
|
||||||
|
codec,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(Self::Text { codec: None })
|
Ok(Self::Text {
|
||||||
|
span: meta.path.span(),
|
||||||
|
codec: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -778,15 +811,53 @@ impl XmlFieldMeta {
|
||||||
Err(Error::new_spanned(meta.path, "unsupported key"))
|
Err(Error::new_spanned(meta.path, "unsupported key"))
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
Ok(Self::Child { default_, amount })
|
Ok(Self::Child {
|
||||||
|
span: meta.path.span(),
|
||||||
|
default_,
|
||||||
|
amount,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(Self::Child {
|
Ok(Self::Child {
|
||||||
|
span: meta.path.span(),
|
||||||
default_: Flag::Absent,
|
default_: Flag::Absent,
|
||||||
amount: None,
|
amount: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a `#[xml(extract)]` meta.
|
||||||
|
fn extract_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
||||||
|
let mut qname = QNameRef::default();
|
||||||
|
let mut fields = None;
|
||||||
|
meta.parse_nested_meta(|meta| {
|
||||||
|
if meta.path.is_ident("fields") {
|
||||||
|
if let Some((fields_span, _)) = fields.as_ref() {
|
||||||
|
let mut error = Error::new_spanned(meta.path, "duplicate `fields` meta");
|
||||||
|
error.combine(Error::new(*fields_span, "previous `fields` meta was here"));
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let mut new_fields = Vec::new();
|
||||||
|
meta.parse_nested_meta(|meta| {
|
||||||
|
new_fields.push(XmlFieldMeta::parse_from_meta(meta)?);
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
fields = Some((meta.path.span(), new_fields));
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
match qname.parse_incremental_from_meta(meta)? {
|
||||||
|
None => Ok(()),
|
||||||
|
Some(meta) => Err(Error::new_spanned(meta.path, "unsupported key")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let fields = fields.map(|(_, x)| x).unwrap_or_else(Vec::new);
|
||||||
|
Ok(Self::Extract {
|
||||||
|
span: meta.path.span(),
|
||||||
|
qname,
|
||||||
|
fields,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse [`Self`] from a nestd meta, switching on the identifier
|
/// Parse [`Self`] from a nestd meta, switching on the identifier
|
||||||
/// of that nested meta.
|
/// of that nested meta.
|
||||||
fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
||||||
|
|
@ -796,6 +867,8 @@ impl XmlFieldMeta {
|
||||||
Self::text_from_meta(meta)
|
Self::text_from_meta(meta)
|
||||||
} else if meta.path.is_ident("child") {
|
} else if meta.path.is_ident("child") {
|
||||||
Self::child_from_meta(meta)
|
Self::child_from_meta(meta)
|
||||||
|
} else if meta.path.is_ident("extract") {
|
||||||
|
Self::extract_from_meta(meta)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::new_spanned(meta.path, "unsupported field meta"))
|
Err(Error::new_spanned(meta.path, "unsupported field meta"))
|
||||||
}
|
}
|
||||||
|
|
@ -868,4 +941,15 @@ impl XmlFieldMeta {
|
||||||
Err(Error::new(*err_span, "missing #[xml(..)] meta on field"))
|
Err(Error::new(*err_span, "missing #[xml(..)] meta on field"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a span which points at the meta which constructed this
|
||||||
|
/// XmlFieldMeta.
|
||||||
|
pub(crate) fn span(&self) -> Span {
|
||||||
|
match self {
|
||||||
|
Self::Attribute { ref span, .. } => *span,
|
||||||
|
Self::Child { ref span, .. } => *span,
|
||||||
|
Self::Text { ref span, .. } => *span,
|
||||||
|
Self::Extract { ref span, .. } => *span,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,15 @@ pub(crate) struct FromEventsScope {
|
||||||
///
|
///
|
||||||
/// See [`crate::field::FieldBuilderPart::Nested`].
|
/// See [`crate::field::FieldBuilderPart::Nested`].
|
||||||
pub(crate) substate_result: Ident,
|
pub(crate) substate_result: Ident,
|
||||||
|
|
||||||
|
/// Prefix which should be used for any types which are declared, to
|
||||||
|
/// ensure they don't collide with other names.
|
||||||
|
pub(crate) type_prefix: Ident,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromEventsScope {
|
impl FromEventsScope {
|
||||||
/// Create a fresh scope with all necessary identifiers.
|
/// Create a fresh scope with all necessary identifiers.
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new(type_prefix: Ident) -> Self {
|
||||||
// Sadly, `Ident::new` is not `const`, so we have to create even the
|
// Sadly, `Ident::new` is not `const`, so we have to create even the
|
||||||
// well-known identifiers from scratch all the time.
|
// well-known identifiers from scratch all the time.
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -65,6 +69,7 @@ impl FromEventsScope {
|
||||||
builder_data_ident: Ident::new("__xso_proc_macro_builder_data", Span::call_site()),
|
builder_data_ident: Ident::new("__xso_proc_macro_builder_data", Span::call_site()),
|
||||||
substate_data: Ident::new("__xso_proc_macro_substate_data", Span::call_site()),
|
substate_data: Ident::new("__xso_proc_macro_substate_data", Span::call_site()),
|
||||||
substate_result: Ident::new("__xso_proc_macro_substate_result", Span::call_site()),
|
substate_result: Ident::new("__xso_proc_macro_substate_result", Span::call_site()),
|
||||||
|
type_prefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +89,24 @@ impl FromEventsScope {
|
||||||
member: Member::Named(mangle_member(member)),
|
member: Member::Named(mangle_member(member)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate an ident with proper scope and span from the type prefix and
|
||||||
|
/// the given member and actual type name.
|
||||||
|
///
|
||||||
|
/// Due to being merged with the type prefix of this scope and the given
|
||||||
|
/// member, this type name is guaranteed to be unique for unique values of
|
||||||
|
/// `name`.
|
||||||
|
pub(crate) fn make_member_type_name(&self, member: &Member, name: &str) -> Ident {
|
||||||
|
quote::format_ident!(
|
||||||
|
"{}Member{}{}",
|
||||||
|
self.type_prefix,
|
||||||
|
match member {
|
||||||
|
Member::Named(ref ident) => ident.to_string(),
|
||||||
|
Member::Unnamed(Index { index, .. }) => index.to_string(),
|
||||||
|
},
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Container struct for various identifiers used throughout the generator
|
/// Container struct for various identifiers used throughout the generator
|
||||||
|
|
@ -97,13 +120,18 @@ impl FromEventsScope {
|
||||||
pub(crate) struct AsItemsScope {
|
pub(crate) struct AsItemsScope {
|
||||||
/// Lifetime for data borrowed by the implementation.
|
/// Lifetime for data borrowed by the implementation.
|
||||||
pub(crate) lifetime: Lifetime,
|
pub(crate) lifetime: Lifetime,
|
||||||
|
|
||||||
|
/// Prefix which should be used for any types which are declared, to
|
||||||
|
/// ensure they don't collide with other names.
|
||||||
|
pub(crate) type_prefix: Ident,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsItemsScope {
|
impl AsItemsScope {
|
||||||
/// Create a fresh scope with all necessary identifiers.
|
/// Create a fresh scope with all necessary identifiers.
|
||||||
pub(crate) fn new(lifetime: &Lifetime) -> Self {
|
pub(crate) fn new(lifetime: &Lifetime, type_prefix: Ident) -> Self {
|
||||||
Self {
|
Self {
|
||||||
lifetime: lifetime.clone(),
|
lifetime: lifetime.clone(),
|
||||||
|
type_prefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,6 +140,24 @@ impl AsItemsScope {
|
||||||
pub(crate) fn borrow(&self, ty: Type) -> Type {
|
pub(crate) fn borrow(&self, ty: Type) -> Type {
|
||||||
ref_ty(ty, self.lifetime.clone())
|
ref_ty(ty, self.lifetime.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate an ident with proper scope and span from the type prefix and
|
||||||
|
/// the given member and actual type name.
|
||||||
|
///
|
||||||
|
/// Due to being merged with the type prefix of this scope and the given
|
||||||
|
/// member, this type name is guaranteed to be unique for unique values of
|
||||||
|
/// `name`.
|
||||||
|
pub(crate) fn make_member_type_name(&self, member: &Member, name: &str) -> Ident {
|
||||||
|
quote::format_ident!(
|
||||||
|
"{}Member{}{}",
|
||||||
|
self.type_prefix,
|
||||||
|
match member {
|
||||||
|
Member::Named(ref ident) => ident.to_string(),
|
||||||
|
Member::Unnamed(Index { index, .. }) => index.to_string(),
|
||||||
|
},
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn mangle_member(member: &Member) -> Ident {
|
pub(crate) fn mangle_member(member: &Member) -> Ident {
|
||||||
|
|
|
||||||
|
|
@ -638,7 +638,7 @@ impl AsItemsStateMachine {
|
||||||
pub(crate) fn render(
|
pub(crate) fn render(
|
||||||
self,
|
self,
|
||||||
vis: &Visibility,
|
vis: &Visibility,
|
||||||
input_ty: &Type,
|
input_ty_ref: &Type,
|
||||||
state_ty_ident: &Ident,
|
state_ty_ident: &Ident,
|
||||||
item_iter_ty_lifetime: &Lifetime,
|
item_iter_ty_lifetime: &Lifetime,
|
||||||
item_iter_ty: &Type,
|
item_iter_ty: &Type,
|
||||||
|
|
@ -650,8 +650,8 @@ impl AsItemsStateMachine {
|
||||||
mut variants,
|
mut variants,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let input_ty_ref = make_ty_ref(input_ty);
|
let input_ty_ref_text = make_ty_ref(input_ty_ref);
|
||||||
let docstr = format!("Convert a {0} into XML events.\n\nThis type is generated using the [`macro@xso::AsXml`] derive macro and implements [`std::iter:Iterator`] for {0}.", input_ty_ref);
|
let docstr = format!("Convert a {0} into XML events.\n\nThis type is generated using the [`macro@xso::AsXml`] derive macro and implements [`std::iter:Iterator`] for {0}.", input_ty_ref_text);
|
||||||
|
|
||||||
let init_body = if variants.len() == 1 {
|
let init_body = if variants.len() == 1 {
|
||||||
let AsItemsEntryPoint { destructure, init } = variants.remove(0);
|
let AsItemsEntryPoint { destructure, init } = variants.remove(0);
|
||||||
|
|
@ -691,7 +691,7 @@ impl AsItemsStateMachine {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(
|
fn new(
|
||||||
value: &#item_iter_ty_lifetime #input_ty,
|
value: #input_ty_ref,
|
||||||
) -> ::core::result::Result<Self, ::xso::error::Error> {
|
) -> ::core::result::Result<Self, ::xso::error::Error> {
|
||||||
::core::result::Result::Ok(#init_body)
|
::core::result::Result::Ok(#init_body)
|
||||||
}
|
}
|
||||||
|
|
@ -729,7 +729,7 @@ impl AsItemsStateMachine {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<#item_iter_ty_lifetime> #item_iter_ty {
|
impl<#item_iter_ty_lifetime> #item_iter_ty {
|
||||||
fn new(value: &#item_iter_ty_lifetime #input_ty) -> ::core::result::Result<Self, ::xso::error::Error> {
|
fn new(value: #input_ty_ref) -> ::core::result::Result<Self, ::xso::error::Error> {
|
||||||
#state_ty_ident::new(value).map(|ok| Self(::core::option::Option::Some(ok)))
|
#state_ty_ident::new(value).map(|ok| Self(::core::option::Option::Some(ok)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -765,6 +765,7 @@ fn doc_link_path(ty: &Type) -> Option<String> {
|
||||||
}
|
}
|
||||||
Some(buf)
|
Some(buf)
|
||||||
}
|
}
|
||||||
|
Type::Reference(TypeReference { ref elem, .. }) => doc_link_path(elem),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use syn::*;
|
||||||
use crate::common::{AsXmlParts, FromXmlParts, ItemDef};
|
use crate::common::{AsXmlParts, FromXmlParts, ItemDef};
|
||||||
use crate::compound::Compound;
|
use crate::compound::Compound;
|
||||||
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
|
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
|
||||||
|
use crate::types::{ref_ty, ty_from_ident};
|
||||||
|
|
||||||
/// Definition of a struct and how to parse it.
|
/// Definition of a struct and how to parse it.
|
||||||
pub(crate) struct StructDef {
|
pub(crate) struct StructDef {
|
||||||
|
|
@ -178,6 +179,7 @@ impl ItemDef for StructDef {
|
||||||
.inner
|
.inner
|
||||||
.make_as_item_iter_statemachine(
|
.make_as_item_iter_statemachine(
|
||||||
&Path::from(target_ty_ident.clone()).into(),
|
&Path::from(target_ty_ident.clone()).into(),
|
||||||
|
&state_ty_ident,
|
||||||
"Struct",
|
"Struct",
|
||||||
&item_iter_ty_lifetime,
|
&item_iter_ty_lifetime,
|
||||||
)?
|
)?
|
||||||
|
|
@ -193,11 +195,10 @@ impl ItemDef for StructDef {
|
||||||
.compile()
|
.compile()
|
||||||
.render(
|
.render(
|
||||||
vis,
|
vis,
|
||||||
&TypePath {
|
&ref_ty(
|
||||||
qself: None,
|
ty_from_ident(target_ty_ident.clone()).into(),
|
||||||
path: target_ty_ident.clone().into(),
|
item_iter_ty_lifetime.clone(),
|
||||||
}
|
),
|
||||||
.into(),
|
|
||||||
&state_ty_ident,
|
&state_ty_ident,
|
||||||
&item_iter_ty_lifetime,
|
&item_iter_ty_lifetime,
|
||||||
&item_iter_ty,
|
&item_iter_ty,
|
||||||
|
|
|
||||||
|
|
@ -745,3 +745,11 @@ pub(crate) fn extend_fn(of_ty: Type, item_ty: Type) -> Expr {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct a [`syn::TypePath`] which references the given type name.
|
||||||
|
pub(crate) fn ty_from_ident(ident: Ident) -> TypePath {
|
||||||
|
TypePath {
|
||||||
|
qself: None,
|
||||||
|
path: ident.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ Version NEXT:
|
||||||
- Support for overriding the names of the types generated by the derive
|
- Support for overriding the names of the types generated by the derive
|
||||||
macros.
|
macros.
|
||||||
- Support for deriving FromXml and AsXml on enums.
|
- Support for deriving FromXml and AsXml on enums.
|
||||||
|
- Support for extracting data from child elements without intermediate
|
||||||
|
structs.
|
||||||
|
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ such:
|
||||||
- *type*: A Rust type.
|
- *type*: A Rust type.
|
||||||
- *expression*: A Rust expression.
|
- *expression*: A Rust expression.
|
||||||
- *ident*: A Rust identifier.
|
- *ident*: A Rust identifier.
|
||||||
|
- *nested*: The meta is followed by parentheses, inside of which meta-specific
|
||||||
|
additional keys are present.
|
||||||
- flag: Has no value. The key's mere presence has relevance and it must not be
|
- flag: Has no value. The key's mere presence has relevance and it must not be
|
||||||
followed by a `=` sign.
|
followed by a `=` sign.
|
||||||
|
|
||||||
|
|
@ -148,6 +150,7 @@ The following mapping types are defined:
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [`attribute`](#attribute-meta) | Map the field to an XML attribute on the struct's element |
|
| [`attribute`](#attribute-meta) | Map the field to an XML attribute on the struct's element |
|
||||||
| [`child`](#child-meta) | Map the field to a child element |
|
| [`child`](#child-meta) | Map the field to a child element |
|
||||||
|
| [`extract`](#extract-meta) | Map the field to contents of a child element of specified structure |
|
||||||
| [`text`](#text-meta) | Map the field to the text content of the struct's element |
|
| [`text`](#text-meta) | Map the field to the text content of the struct's element |
|
||||||
|
|
||||||
#### `attribute` meta
|
#### `attribute` meta
|
||||||
|
|
@ -294,6 +297,62 @@ assert_eq!(parent, Parent {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `extract` meta
|
||||||
|
|
||||||
|
The `extract` meta causes the field to be mapped to the *contents* of a child
|
||||||
|
element.
|
||||||
|
|
||||||
|
The following keys can be used inside the `#[xml(extract(..))]` meta:
|
||||||
|
|
||||||
|
| Key | Value type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `namespace` | *string literal* or *path* | The XML namespace of the child element. |
|
||||||
|
| `name` | *string literal* or *path* | The XML name of the child element. If it is a *path*, it must point at a `&'static NcNameStr`. |
|
||||||
|
| `fields` | *nested* | A list of [field meta](#field-meta) which describe the contents of the child element. |
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
the field.
|
||||||
|
|
||||||
|
**Note:** Currently, only a single field can be extracted. This restriction
|
||||||
|
will be lifted in the future. Collections are not supported yet, either.
|
||||||
|
|
||||||
|
Using `extract` instead of `child` combined with a specific struct declaration
|
||||||
|
comes with trade-offs. On the one hand, using `extract` gives you flexibility
|
||||||
|
in regard of the specific serialisation of a field: it is possible to exchange
|
||||||
|
a nested child element for an attribute without changing the Rust interface
|
||||||
|
of the struct.
|
||||||
|
|
||||||
|
On the other hand, `extract` meta declarations can quickly become unwieldly
|
||||||
|
and they may not support all configuration options which may in the future be
|
||||||
|
added on structs (such as configuring handling of undeclared attributes) and
|
||||||
|
they cannot be used for enumerations.
|
||||||
|
|
||||||
|
##### Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# use xso::FromXml;
|
||||||
|
#[derive(FromXml, Debug, PartialEq)]
|
||||||
|
#[xml(namespace = "urn:example", name = "foo")]
|
||||||
|
struct Foo {
|
||||||
|
#[xml(extract(namespace = "urn:example", name = "bar", fields(attribute = "a")))]
|
||||||
|
a: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let foo: Foo = xso::from_bytes(b"<foo
|
||||||
|
xmlns='urn:example'><bar a='xyz'/></foo>").unwrap();
|
||||||
|
assert_eq!(foo, Foo {
|
||||||
|
a: "xyz".to_string(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
#### `text` meta
|
#### `text` meta
|
||||||
|
|
||||||
The `text` meta causes the field to be mapped to the text content of the
|
The `text` meta causes the field to be mapped to the text content of the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue