xso_proc: merge ExtractMeta into XmlFieldMeta

This transforms the `extract(..)` spec into something extremely close to
a real struct type def, with the full flexibility that gives.
This commit is contained in:
Jonas Schäfer 2024-03-30 15:27:09 +01:00
parent 89ce34e729
commit cb51dbf215
4 changed files with 212 additions and 241 deletions

View File

@ -7,7 +7,7 @@ use syn::*;
use crate::compound::Compound;
use crate::error_message::{self, ParentRef};
use crate::meta::{ExtractMeta, Flag, NameRef, NamespaceRef};
use crate::meta::{Flag, NameRef, NamespaceRef, XmlFieldMeta};
use super::{ChildMode, FieldDef, FieldNamespace, FieldParsePart};
@ -61,14 +61,14 @@ impl ExtractDef {
span: Span,
namespace: FieldNamespace,
name: NameRef,
parts: Vec<ExtractMeta>,
parts: Vec<Box<XmlFieldMeta>>,
mut single_extract_type: Option<Type>,
) -> Result<Self> {
if parts.len() != 1 {
single_extract_type = None;
}
let parts = Compound::new(parts.into_iter().enumerate().map(|(i, x)| {
FieldDef::from_extract(span.clone(), x, i as u32, single_extract_type.take())
FieldDef::from_extract(span.clone(), *x, i as u32, single_extract_type.take())
}))?;
Ok(Self {
namespace,
@ -268,7 +268,7 @@ impl ChildField {
mode: ChildMode,
namespace: Option<NamespaceRef>,
name: Option<NameRef>,
extract: Vec<ExtractMeta>,
extract: Vec<Box<XmlFieldMeta>>,
default_on_missing: Flag,
field_type: &Type,
) -> Result<Self> {

View File

@ -21,7 +21,7 @@ use quote::quote;
use syn::{spanned::Spanned, *};
use crate::error_message::ParentRef;
use crate::meta::{ChildMode, ExtractMeta, NamespaceRef, StaticNamespace, XmlFieldMeta};
use crate::meta::{ChildMode, NamespaceRef, StaticNamespace, XmlFieldMeta};
use self::attribute::AttributeField;
use self::child::ChildField;
@ -156,26 +156,78 @@ impl FieldKind {
_ => false,
}
}
}
impl TryFrom<ExtractMeta> for FieldKind {
type Error = Error;
fn try_from(other: ExtractMeta) -> Result<Self> {
match other {
ExtractMeta::Attribute {
/// Construct a `FieldKind` by processing the options in the meta.
///
/// *Note*: Explicit type specifications are rejected by this function.
/// Those must have been taken out of the `meta` before calling this
/// function. The compile-time error message emitted by this function is
/// not very useful in this case, so if you do not want to support
/// explicit types, you should take them out at the call site and emit
/// a more useful error there.
fn new(
span: &Span,
field_ident: Option<&Ident>,
field_ty: &Type,
meta: XmlFieldMeta,
) -> Result<Self> {
match meta {
XmlFieldMeta::Attribute {
name,
ty: _, // this is processed in FieldDef::from_extract already
default_on_missing,
} => Ok(Self::Attribute(AttributeField {
name: name.into(),
codec,
ty,
} => {
if let Some(ty) = ty {
return Err(Error::new_spanned(ty, "cannot set attribute type here"));
};
Ok(FieldKind::Attribute(AttributeField::new(
span,
field_ident,
name,
default_on_missing,
codec,
)?))
}
XmlFieldMeta::Child {
mode,
namespace,
name,
extract,
default_on_missing,
codec: None,
})),
ExtractMeta::Text {
ty: _, // this is processed in FieldDef::from_extract already
} => Ok(Self::Text(TextField { codec: None })),
ExtractMeta::Elements => Ok(Self::Elements(ElementsField { selector: None })),
} => Ok(FieldKind::Child(ChildField::new(
span,
mode,
namespace,
name,
extract,
default_on_missing,
field_ty,
)?)),
XmlFieldMeta::Text { codec, ty } => {
if let Some(ty) = ty {
return Err(Error::new_spanned(ty, "cannot set text type here"));
};
Ok(FieldKind::Text(TextField::new(span, codec)?))
}
XmlFieldMeta::Namespace => Ok(FieldKind::Namespace(NamespaceField::new())),
XmlFieldMeta::Element {
namespace,
name,
default_on_missing,
} => Ok(FieldKind::Element(ElementField::new(
span,
namespace,
name,
default_on_missing,
)?)),
XmlFieldMeta::Elements { namespace, name } => Ok(FieldKind::Elements(
ElementsField::new(span, namespace, name)?,
)),
XmlFieldMeta::Flag { namespace, name } => {
Ok(FieldKind::Flag(FlagField::new(span, namespace, name)?))
}
XmlFieldMeta::Ignore => Ok(FieldKind::Ignore),
}
}
}
@ -228,7 +280,8 @@ fn try_unwrap_option_type(ty: Type) -> Type {
}
impl FieldDef {
/// Generate a [`FieldDef`] from an [`ExtractMeta`] specification.
/// Generate a [`FieldDef`] as extract from an [`XmlFieldMeta`]
/// specification.
///
/// This is used to build a [`crate::compound::Compound`] used to parse
/// the extract. As it would otherwise be an insane amount of duplication,
@ -251,24 +304,20 @@ impl FieldDef {
/// type.
fn from_extract(
span: Span,
mut extract: ExtractMeta,
mut extract: XmlFieldMeta,
index: u32,
single_extract_type: Option<Type>,
) -> Result<Self> {
let string_ty: Type =
parse_str("::std::string::String").expect("cannot construct string type");
let ty = match extract {
ExtractMeta::Attribute { ref mut ty, .. } => ty.take(),
ExtractMeta::Text { ref mut ty, .. } => ty.take(),
ExtractMeta::Elements => Some(
parse_str("::std::vec::Vec<::xso::exports::minidom::Element>")
.expect("cannot construct elements type"),
),
};
let ty = extract.determine_type();
let ty = ty
.or(single_extract_type.map(try_unwrap_option_type))
.unwrap_or(string_ty);
let kind = extract.try_into()?;
let kind = FieldKind::new(
&span, None, // field_ident is always none here, we use unnamed fields.
&ty, extract,
)?;
Ok(Self {
span,
ident: Member::Unnamed(Index {
@ -311,7 +360,7 @@ impl FieldDef {
meta = Some((XmlFieldMeta::parse_from_attribute(attr)?, attr.span()));
}
let Some((meta, span)) = meta else {
let Some((mut meta, span)) = meta else {
return Err(Error::new_spanned(
field,
"exactly one #[xml(..)] attribute per field required.",
@ -326,53 +375,14 @@ impl FieldDef {
}),
};
let kind = match meta {
XmlFieldMeta::Attribute {
name,
default_on_missing,
codec,
} => FieldKind::Attribute(AttributeField::new(
&span,
field.ident.as_ref(),
name,
default_on_missing,
codec,
)?),
XmlFieldMeta::Child {
mode,
namespace,
name,
extract,
default_on_missing,
} => FieldKind::Child(ChildField::new(
&span,
mode,
namespace,
name,
extract,
default_on_missing,
&field.ty,
)?),
XmlFieldMeta::Text { codec } => FieldKind::Text(TextField::new(&span, codec)?),
XmlFieldMeta::Namespace => FieldKind::Namespace(NamespaceField::new()),
XmlFieldMeta::Element {
namespace,
name,
default_on_missing,
} => FieldKind::Element(ElementField::new(
&span,
namespace,
name,
default_on_missing,
)?),
XmlFieldMeta::Elements { namespace, name } => {
FieldKind::Elements(ElementsField::new(&span, namespace, name)?)
}
XmlFieldMeta::Flag { namespace, name } => {
FieldKind::Flag(FlagField::new(&span, namespace, name)?)
}
XmlFieldMeta::Ignore => FieldKind::Ignore,
};
if let Some(ty) = meta.take_type() {
return Err(Error::new_spanned(
ty,
"specifying the type on struct or enum variant fields is redundant and not allowed",
));
}
let kind = FieldKind::new(&span, field.ident.as_ref(), &field.ty, meta)?;
Ok(Self {
span,

View File

@ -142,8 +142,8 @@ impl<'a> MetaParse for ParseNodeFilter<'a> {
}
}
/// Parse a `Vec<ExtractMeta>` from a meta.
struct ParseExtracts<'a>(&'a mut Vec<ExtractMeta>);
/// Parse a `Vec<Box<XmlFieldMeta>>` from a meta.
struct ParseExtracts<'a>(&'a mut Vec<Box<XmlFieldMeta>>);
impl<'a> MetaParse for ParseExtracts<'a> {
fn name(&self) -> &'static str {
@ -152,7 +152,7 @@ impl<'a> MetaParse for ParseExtracts<'a> {
fn force_parse_at_meta<'x>(&mut self, meta: meta::ParseNestedMeta<'x>) -> Result<()> {
meta.parse_nested_meta(|meta| {
self.0.push(ExtractMeta::parse_from_meta(meta)?);
self.0.push(Box::new(XmlFieldMeta::parse_from_meta(meta)?));
Ok(())
})
}
@ -645,82 +645,6 @@ impl AttributeMeta {
}
}
/// A single extraction part inside an `#[xml(..(.., extract(..)))]`
/// attribute.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum ExtractMeta {
/// XML attribute extraction.
///
/// Maps to `extract(.., attribute, ..)`,
/// `extract(.., attribute = .., ..)`, or
/// `extract(.., attribute(..), ..)`.
Attribute {
name: NameRef,
default_on_missing: Flag,
ty: Option<Type>,
},
/// XML text extraction.
///
/// Maps to `extract(.., text, ..)`.
Text { ty: Option<Type> },
/// XML child element extraction.
///
/// Maps to `extract(.., elements, ..)`.
Elements,
}
impl ExtractMeta {
/// Parse a single extraction spec from the given `meta`.
///
/// See the enum variants for accepted syntaxes.
fn parse_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
if meta.path.is_ident("text") {
if meta.input.peek(token::Paren) {
let mut ty: Option<Type> = None;
#[rustfmt::skip] // rustfmt transforms the code so that the attribute inside parse_meta! is on an expression which is not allowed
meta.parse_nested_meta(|meta| {
parse_meta!(meta, ParseValue("type", &mut ty),)
})?;
Ok(Self::Text { ty })
} else {
Ok(Self::Text { ty: None })
}
} else if meta.path.is_ident("attribute") {
let path = meta.path.clone();
let meta = AttributeMeta::parse_from_meta(meta)?;
let Some(name) = meta.name else {
return Err(syn::Error::new_spanned(path, "attribute name must be specified in extract(), e.g. extract(attribute = \"name\")"));
};
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespaced attributes are not supported yet...",
));
}
Ok(Self::Attribute {
name,
ty: meta.ty,
default_on_missing: meta.default_on_missing,
})
} else if meta.path.is_ident("elements") {
if meta.input.peek(token::Paren) {
return Err(syn::Error::new_spanned(
meta.path,
"arguments to `collect` inside #[xml(..(extract(..)))] are not supported.",
));
}
Ok(Self::Elements)
} else {
return Err(syn::Error::new_spanned(
meta.path,
"unsupported extract spec",
));
}
}
}
/// Mode for destructured child collection.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum ChildMode {
@ -753,6 +677,9 @@ pub(crate) enum XmlFieldMeta {
/// Contents of the `codec = ..` option.
codec: Option<Type>,
/// Contents of the `type = ..` option.
ty: Option<Type>,
},
/// Maps the field to a destructured XML child element.
@ -774,7 +701,7 @@ pub(crate) enum XmlFieldMeta {
name: Option<NameRef>,
/// Contents of the `extract(..)` option.
extract: Vec<ExtractMeta>,
extract: Vec<Box<XmlFieldMeta>>,
/// Presence of the `default` flag.
default_on_missing: Flag,
@ -793,6 +720,9 @@ pub(crate) enum XmlFieldMeta {
Text {
/// Contents of the `codec = ..` option.
codec: Option<Type>,
/// Contents of the `type = ..` option.
ty: Option<Type>,
},
/// Maps the field to a subset of XML child elements, without
@ -840,6 +770,9 @@ pub(crate) enum XmlFieldMeta {
Ignore,
}
static VALID_FIELD_XML_OPTIONS: &'static str =
"attribute, child, children, namespace, text, elements, element, flag, ignore";
impl XmlFieldMeta {
/// Processes a `#[xml(attribute)]` meta, creating a [`Self::Attribute`]
/// variant.
@ -851,11 +784,11 @@ impl XmlFieldMeta {
"namespaced attributes are not supported yet...",
));
}
meta.reject_type("type = .. is not allowed on fields (only on extracts)")?;
Ok(Self::Attribute {
name: meta.name,
default_on_missing: meta.default_on_missing,
codec: meta.codec,
ty: meta.ty,
})
}
@ -866,13 +799,13 @@ impl XmlFieldMeta {
) -> Result<(
Option<NamespaceRef>,
Option<NameRef>,
Vec<ExtractMeta>,
Vec<Box<XmlFieldMeta>>,
Flag,
)> {
if meta.input.peek(token::Paren) {
let mut name: Option<NameRef> = None;
let mut namespace: Option<NamespaceRef> = None;
let mut extract: Vec<ExtractMeta> = Vec::new();
let mut extract: Vec<Box<XmlFieldMeta>> = Vec::new();
let mut default_on_missing = Flag::Absent;
meta.parse_nested_meta(|meta| {
parse_meta!(
@ -928,13 +861,18 @@ impl XmlFieldMeta {
/// variant.
fn text_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let mut codec: Option<Type> = None;
let mut ty: Option<Type> = None;
if meta.input.peek(token::Paren) {
#[rustfmt::skip] // rustfmt transforms the code so that the attribute inside parse_meta! is on an expression which is not allowed
meta.parse_nested_meta(|meta| {
parse_meta!(meta, ParseValue("codec", &mut codec),)
parse_meta!(
meta,
ParseValue("codec", &mut codec),
ParseValue("type", &mut ty),
)
})?;
}
Ok(Self::Text { codec })
Ok(Self::Text { codec, ty })
}
/// Processes a `#[xml(element)]` meta, creating a [`Self::Element`]
@ -942,7 +880,7 @@ impl XmlFieldMeta {
fn element_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let meta = AttributeMeta::parse_from_meta(meta)?;
meta.reject_codec("codec = .. is not allowed on #[xml(element)]")?;
meta.reject_type("type = .. is not allowed on fields (only on extracts)")?;
meta.reject_type("type = .. is not allowed on #[xml(element)]")?;
Ok(Self::Element {
name: meta.name,
namespace: meta.namespace,
@ -955,7 +893,7 @@ impl XmlFieldMeta {
fn elements_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let meta = AttributeMeta::parse_from_meta(meta)?;
meta.reject_codec("codec = .. is not allowed on #[xml(elements)]")?;
meta.reject_type("type = .. is not allowed on fields (only on extracts)")?;
meta.reject_type("type = .. is not allowed on #[xml(elements)]")?;
meta.reject_default("default cannot be used on #[xml(elements)] (it is implied, the default is the empty container)")?;
Ok(Self::Elements {
name: meta.name,
@ -966,7 +904,7 @@ impl XmlFieldMeta {
/// Processes a `#[xml(flag)]` meta, creating a [`Self::Flag`] variant.
fn flag_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let meta = AttributeMeta::parse_from_meta(meta)?;
meta.reject_type("type = .. is not allowed on fields (only on extracts)")?;
meta.reject_type("type = .. is not allowed on #[xml(flag)]")?;
if let Flag::Present(default_on_missing) = meta.default_on_missing {
return Err(syn::Error::new(
default_on_missing,
@ -985,6 +923,36 @@ impl XmlFieldMeta {
Ok(Self::Ignore)
}
fn parse_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
if meta.path.is_ident("attribute") {
Self::attribute_from_meta(meta)
} else if meta.path.is_ident("child") {
Self::child_from_meta(meta)
} else if meta.path.is_ident("children") {
Self::children_from_meta(meta)
} else if meta.path.is_ident("namespace") {
Self::namespace_from_meta(meta)
} else if meta.path.is_ident("text") {
Self::text_from_meta(meta)
} else if meta.path.is_ident("elements") {
Self::elements_from_meta(meta)
} else if meta.path.is_ident("element") {
Self::element_from_meta(meta)
} else if meta.path.is_ident("flag") {
Self::flag_from_meta(meta)
} else if meta.path.is_ident("ignore") {
Self::ignore_from_meta(meta)
} else {
Err(Error::new_spanned(
meta.path,
format!(
"unsupported field option. supported options: {}.",
VALID_FIELD_XML_OPTIONS
),
))
}
}
/// Parse an `#[xml(..)]` attribute.
///
/// This switches based on the first identifier within the `#[xml(..)]`
@ -999,9 +967,6 @@ impl XmlFieldMeta {
/// Undefined options or options with incompatible values are rejected
/// with an appropriate compile-time error.
pub(crate) fn parse_from_attribute(attr: &Attribute) -> Result<Self> {
static VALID_OPTIONS: &'static str =
"attribute, child, children, namespace, text, elements, element, flag";
let mut result: Option<Self> = None;
attr.parse_nested_meta(|meta| {
@ -1012,42 +977,8 @@ impl XmlFieldMeta {
));
}
if meta.path.is_ident("attribute") {
result = Some(Self::attribute_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("child") {
result = Some(Self::child_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("children") {
result = Some(Self::children_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("namespace") {
result = Some(Self::namespace_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("text") {
result = Some(Self::text_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("elements") {
result = Some(Self::elements_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("element") {
result = Some(Self::element_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("flag") {
result = Some(Self::flag_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("ignore") {
result = Some(Self::ignore_from_meta(meta)?);
Ok(())
} else {
Err(Error::new_spanned(
meta.path,
format!(
"unsupported field option. supported options: {}.",
VALID_OPTIONS
),
))
}
result = Some(Self::parse_from_meta(meta)?);
Ok(())
})?;
if let Some(result) = result {
@ -1056,10 +987,60 @@ impl XmlFieldMeta {
Err(Error::new_spanned(
attr,
format!(
"missing field options. specify at least one of {}.",
VALID_OPTIONS
"missing field options. specify at one of {}.",
VALID_FIELD_XML_OPTIONS
),
))
}
}
/// Take the explicit type specification out of this meta and return it,
/// if any.
///
/// This function *does not recurse* into nested `extract(..)` specs.
pub(crate) fn take_type(&mut self) -> Option<Type> {
match self {
XmlFieldMeta::Attribute { ref mut ty, .. } => ty.take(),
XmlFieldMeta::Text { ref mut ty, .. } => ty.take(),
_ => None,
}
}
/// Take the explicit type specification out of this meta and return it,
/// if any.
///
/// This function **does recurse** into nested `extract(..)` specs and
/// returns types for kinds with known type.
pub(crate) fn determine_type(&mut self) -> Option<Type> {
match self {
Self::Attribute { ref mut ty, .. } => ty.take(),
Self::Child {
ref mut extract,
mode: ChildMode::Single,
..
} => {
if extract.len() == 1 {
extract[0].take_type()
} else {
None // cannot be determined from meta alone
}
}
Self::Child {
mode: ChildMode::Collection,
..
} => None, // cannot be determined from meta alone
Self::Namespace => None, // cannot be determined from meta alone
Self::Text { ref mut ty, .. } => ty.take(),
Self::Elements { .. } => Some(
parse_str("::std::vec::Vec<::xso::exports::minidom::Element>")
.expect("cannot construct elements type"),
),
Self::Element { .. } => Some(
parse_str("::xso::exports::minidom::Element")
.expect("cannot construct element type"),
),
Self::Flag { .. } => Some(parse_str("bool").expect("cannot construct element type")),
Self::Ignore => None,
}
}
}

View File

@ -350,41 +350,21 @@ The following field kinds are available:
### Extraction specification
Inside `extract(..)`, the following parts can be specified:
Inside `extract(..)`, there must be a list of type annotations as used on
fields. All annotations can be used which can also be used on fields. Because
here there is no possibility to do any inferrence on the field type, the
following field attributes support an additional, optional `type` argument:
- `attribute = ..` (specifying the name), `attribute(..)`: Extract an
attribute from the child element.
- `attribute`
- `text`
Valid options inside `attribute(..)` are:
If the `extract(..)` contains exactly one part and the type of the extract
is not specified on that one part, it is assumed to be equal to the type of
the field the extract is used on.
- `name = ..` (required): The XML name of the attribute to extract. Unlike
on named fields, it is required because the name cannot be inferred from
the field identifier.
- `type = ..`: The type to use for the attribute.
If the extract is used on a `#[xml(child)]` field and it consists of only
one part, then the type defaults to the type of the field minus a single
layer of `Option<..>` wrapping, if any (i.e.: if you have a field with
type `Option<Foo>`, the defaulted type is `Foo`).
If the extract is used on a `#[xml(children)]` field or consists of more
than one part, the type is defaulted to [`String`].
- `text`, `text(..)`: Extract the child's text contents.
Valid options inside `text(..)` are:
- `type = ..`: The type to use for the text.
If the extract is used on a `#[xml(child)]` field and it consists of only
one part, then the type defaults to the type of the field minus a single
layer of `Option<..>` wrapping, if any (i.e.: if you have a field with
type `Option<Foo>`, the defaulted type is `Foo`).
If the extract is used on a `#[xml(children)]` field or consists of more
than one part, the type is defaulted to [`String`].
- `elements`: Extract the child's child elements as `Vec<Element>`.
Otherwise, the default is `String`, which is not going to work in many cases.
This limitation could be lifted, but we need a use case for it first :). So
if you run into this file an issue please!
*/
pub use xso_proc::FromXml;