mirror of
https://gitlab.com/xmpp-rs/xmpp-rs.git
synced 2024-07-12 22:21:53 +00:00
xso-proc: start making derive macros for FromXml and IntoXml
For now, these macros only support empty elements. Everything else will be rejected with a compile-time error.
This commit is contained in:
parent
9ec9a0f0c6
commit
0adfd1218b
10 changed files with 662 additions and 1 deletions
|
@ -7,6 +7,7 @@ members = [ # alphabetically sorted
|
||||||
"tokio-xmpp",
|
"tokio-xmpp",
|
||||||
"xmpp",
|
"xmpp",
|
||||||
"xso",
|
"xso",
|
||||||
|
"xso-proc",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
@ -18,3 +19,4 @@ tokio-xmpp = { path = "tokio-xmpp" }
|
||||||
xmpp-parsers = { path = "parsers" }
|
xmpp-parsers = { path = "parsers" }
|
||||||
xmpp = { path = "xmpp" }
|
xmpp = { path = "xmpp" }
|
||||||
xso = { path = "xso" }
|
xso = { path = "xso" }
|
||||||
|
xso_proc = { path = "xso-proc" }
|
||||||
|
|
|
@ -24,7 +24,7 @@ chrono = { version = "0.4.5", default-features = false, features = ["std"] }
|
||||||
# same repository dependencies
|
# same repository dependencies
|
||||||
jid = { version = "0.10", features = ["minidom"], path = "../jid" }
|
jid = { version = "0.10", features = ["minidom"], path = "../jid" }
|
||||||
minidom = { version = "0.15", path = "../minidom" }
|
minidom = { version = "0.15", path = "../minidom" }
|
||||||
xso = { version = "0.0.2" }
|
xso = { version = "0.0.2", features = ["macros", "minidom", "panicking-into-impl"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Build xmpp-parsers to make components instead of clients.
|
# Build xmpp-parsers to make components instead of clients.
|
||||||
|
|
150
parsers/src/util/macro_tests.rs
Normal file
150
parsers/src/util/macro_tests.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
#![deny(
|
||||||
|
non_camel_case_types,
|
||||||
|
non_snake_case,
|
||||||
|
unsafe_code,
|
||||||
|
unused_variables,
|
||||||
|
unused_mut,
|
||||||
|
dead_code
|
||||||
|
)]
|
||||||
|
|
||||||
|
mod helpers {
|
||||||
|
// we isolate the helpers into a module, because we do not want to have
|
||||||
|
// them in scope below.
|
||||||
|
// this is to ensure that the macros do not have hidden dependencies on
|
||||||
|
// any specific names being imported.
|
||||||
|
use minidom::Element;
|
||||||
|
use xso::{error::FromElementError, transform, try_from_element, FromXml, IntoXml};
|
||||||
|
|
||||||
|
pub(super) fn roundtrip_full<T: IntoXml + FromXml + PartialEq + std::fmt::Debug + Clone>(
|
||||||
|
s: &str,
|
||||||
|
) {
|
||||||
|
let initial: Element = s.parse().unwrap();
|
||||||
|
let structural: T = match try_from_element(initial.clone()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => panic!("failed to parse from {:?}: {}", s, e),
|
||||||
|
};
|
||||||
|
let recovered =
|
||||||
|
transform(structural.clone()).expect("roundtrip did not produce an element");
|
||||||
|
assert_eq!(initial, recovered);
|
||||||
|
let structural2: T = match try_from_element(recovered) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => panic!("failed to parse from serialisation of {:?}: {}", s, e),
|
||||||
|
};
|
||||||
|
assert_eq!(structural, structural2);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_str<T: FromXml>(s: &str) -> Result<T, FromElementError> {
|
||||||
|
let initial: Element = s.parse().unwrap();
|
||||||
|
try_from_element(initial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use self::helpers::{parse_str, roundtrip_full};
|
||||||
|
|
||||||
|
use xso::{FromXml, IntoXml};
|
||||||
|
|
||||||
|
// these are adverserial local names in order to trigger any issues with
|
||||||
|
// unqualified names in the macro expansions.
|
||||||
|
#[allow(dead_code, non_snake_case)]
|
||||||
|
fn Err() {}
|
||||||
|
#[allow(dead_code, non_snake_case)]
|
||||||
|
fn Ok() {}
|
||||||
|
#[allow(dead_code, non_snake_case)]
|
||||||
|
fn Some() {}
|
||||||
|
#[allow(dead_code, non_snake_case)]
|
||||||
|
fn None() {}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
type Option = ((),);
|
||||||
|
#[allow(dead_code)]
|
||||||
|
type Result = ((),);
|
||||||
|
|
||||||
|
static NS1: &str = "urn:example:ns1";
|
||||||
|
|
||||||
|
#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)]
|
||||||
|
#[xml(namespace = NS1, name = "foo")]
|
||||||
|
struct Empty;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_roundtrip() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
roundtrip_full::<Empty>("<foo xmlns='urn:example:ns1'/>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_name_mismatch() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<Empty>("<bar xmlns='urn:example:ns1'/>") {
|
||||||
|
Err(xso::error::FromElementError::Mismatch(..)) => (),
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_namespace_mismatch() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<Empty>("<foo xmlns='urn:example:ns2'/>") {
|
||||||
|
Err(xso::error::FromElementError::Mismatch(..)) => (),
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_unexpected_attribute() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<Empty>("<foo xmlns='urn:example:ns1' fnord='bar'/>") {
|
||||||
|
Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => {
|
||||||
|
assert_eq!(e, "Unknown attribute in foo element.");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_unexpected_child() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<Empty>("<foo xmlns='urn:example:ns1'><coucou/></foo>") {
|
||||||
|
Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => {
|
||||||
|
assert_eq!(e, "Unknown child in foo element.");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_qname_check_has_precedence_over_attr_check() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<Empty>("<bar xmlns='urn:example:ns1' fnord='bar'/>") {
|
||||||
|
Err(xso::error::FromElementError::Mismatch(..)) => (),
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,3 +10,6 @@ pub(crate) mod text_node_codecs;
|
||||||
/// Helper macros to parse and serialise more easily.
|
/// Helper macros to parse and serialise more easily.
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod macro_tests;
|
||||||
|
|
24
xso-proc/Cargo.toml
Normal file
24
xso-proc/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "xso_proc"
|
||||||
|
version = "0.0.2"
|
||||||
|
authors = [
|
||||||
|
"Jonas Schäfer <jonas@zombofant.net>",
|
||||||
|
]
|
||||||
|
description = "Macro implementation of #[derive(FromXml, IntoXml)]"
|
||||||
|
homepage = "https://xmpp.rs"
|
||||||
|
repository = "https://gitlab.com/xmpp-rs/xmpp-rs"
|
||||||
|
keywords = ["xso", "derive", "serialization"]
|
||||||
|
license = "MPL-2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "^1"
|
||||||
|
syn = { version = "^2", features = ["full", "extra-traits"] }
|
||||||
|
proc-macro2 = "^1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
panicking-into-impl = ["minidom"]
|
||||||
|
minidom = []
|
286
xso-proc/src/lib.rs
Normal file
286
xso-proc/src/lib.rs
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![allow(rustdoc::private_intra_doc_links)]
|
||||||
|
/*!
|
||||||
|
# Macros for parsing XML into Rust structs, and vice versa
|
||||||
|
|
||||||
|
**If you are a user of `xso_proc` or `xso`, please
|
||||||
|
return to `xso` for more information**. The documentation of
|
||||||
|
`xso_proc` is geared toward developers of `…_macros` and `…_core`.
|
||||||
|
|
||||||
|
**You have been warned.**
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Wondering about RawTokenStream vs. TokenStream?
|
||||||
|
// syn mostly works with proc_macro2, while the proc macros themselves use
|
||||||
|
// proc_macro.
|
||||||
|
use proc_macro::TokenStream as RawTokenStream;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::*;
|
||||||
|
|
||||||
|
mod meta;
|
||||||
|
|
||||||
|
/// Convert an [`syn::Item`] into the parts relevant for us.
|
||||||
|
///
|
||||||
|
/// If the item is of an unsupported variant, an appropriate error is
|
||||||
|
/// returned.
|
||||||
|
fn parse_struct(item: Item) -> Result<(Visibility, meta::XmlCompoundMeta, Ident)> {
|
||||||
|
match item {
|
||||||
|
Item::Struct(item) => {
|
||||||
|
match item.fields {
|
||||||
|
Fields::Unit => (),
|
||||||
|
other => {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
other,
|
||||||
|
"cannot derive on non-unit struct (yet!)",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let meta = meta::XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
|
||||||
|
Ok((item.vis, meta, item.ident))
|
||||||
|
}
|
||||||
|
other => Err(Error::new_spanned(other, "cannot derive on this item")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a `xso::FromXml` implementation for the given item, or fail with
|
||||||
|
/// a proper compiler error.
|
||||||
|
fn from_xml_impl(input: Item) -> Result<TokenStream> {
|
||||||
|
let (
|
||||||
|
vis,
|
||||||
|
meta::XmlCompoundMeta {
|
||||||
|
namespace,
|
||||||
|
name,
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
ident,
|
||||||
|
) = parse_struct(input)?;
|
||||||
|
|
||||||
|
// we rebind to a different name here because otherwise some expressions
|
||||||
|
// inside `quote! {}` below get a bit tricky to read (such as
|
||||||
|
// `name.1 == #name`).
|
||||||
|
let Some(xml_namespace) = namespace else {
|
||||||
|
return Err(Error::new(span, "`namespace` key is required"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(xml_name) = name else {
|
||||||
|
return Err(Error::new(span, "`name` key is required"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let from_events_builder_ty_name = quote::format_ident!("{}FromEvents", ident);
|
||||||
|
let state_ty_name = quote::format_ident!("{}FromEventsState", ident);
|
||||||
|
|
||||||
|
let unknown_attr_err = format!("Unknown attribute in {} element.", xml_name.value());
|
||||||
|
let unknown_child_err = format!("Unknown child in {} element.", xml_name.value());
|
||||||
|
let docstr = format!("Build a [`{}`] from XML events", ident);
|
||||||
|
|
||||||
|
#[cfg_attr(not(feature = "minidom"), allow(unused_mut))]
|
||||||
|
let mut result = quote! {
|
||||||
|
enum #state_ty_name {
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc = #docstr]
|
||||||
|
#vis struct #from_events_builder_ty_name(::core::option::Option<#state_ty_name>);
|
||||||
|
|
||||||
|
impl ::xso::FromEventsBuilder for #from_events_builder_ty_name {
|
||||||
|
type Output = #ident;
|
||||||
|
|
||||||
|
fn feed(
|
||||||
|
&mut self,
|
||||||
|
ev: ::xso::exports::rxml::Event
|
||||||
|
) -> ::core::result::Result<::core::option::Option<Self::Output>, ::xso::error::Error> {
|
||||||
|
match self.0 {
|
||||||
|
::core::option::Option::None => panic!("feed() called after it returned a non-None value"),
|
||||||
|
::core::option::Option::Some(#state_ty_name::Default) => match ev {
|
||||||
|
::xso::exports::rxml::Event::StartElement(..) => {
|
||||||
|
::core::result::Result::Err(::xso::error::Error::Other(#unknown_child_err))
|
||||||
|
}
|
||||||
|
::xso::exports::rxml::Event::EndElement(..) => {
|
||||||
|
self.0 = ::core::option::Option::None;
|
||||||
|
::core::result::Result::Ok(::core::option::Option::Some(#ident))
|
||||||
|
}
|
||||||
|
::xso::exports::rxml::Event::Text(..) => {
|
||||||
|
::core::result::Result::Err(::xso::error::Error::Other("Unexpected text content".into()))
|
||||||
|
}
|
||||||
|
// we ignore these: a correct parser only generates
|
||||||
|
// them at document start, and there we want to indeed
|
||||||
|
// not worry about them being in front of the first
|
||||||
|
// element.
|
||||||
|
::xso::exports::rxml::Event::XmlDeclaration(_, ::xso::exports::rxml::XmlVersion::V1_0) => ::core::result::Result::Ok(::core::option::Option::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::xso::FromXml for #ident {
|
||||||
|
type Builder = #from_events_builder_ty_name;
|
||||||
|
|
||||||
|
fn from_events(
|
||||||
|
name: ::xso::exports::rxml::QName,
|
||||||
|
attrs: ::xso::exports::rxml::AttrMap,
|
||||||
|
) -> ::core::result::Result<Self::Builder, ::xso::error::FromEventsError> {
|
||||||
|
if name.0 != #xml_namespace || name.1 != #xml_name {
|
||||||
|
return ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs });
|
||||||
|
}
|
||||||
|
if attrs.len() > 0 {
|
||||||
|
return ::core::result::Result::Err(::xso::error::Error::Other(#unknown_attr_err).into());
|
||||||
|
}
|
||||||
|
::core::result::Result::Ok(#from_events_builder_ty_name(::core::option::Option::Some(#state_ty_name::Default)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "minidom")]
|
||||||
|
result.extend(quote! {
|
||||||
|
impl ::std::convert::TryFrom<::xso::exports::minidom::Element> for #ident {
|
||||||
|
type Error = ::xso::error::FromElementError;
|
||||||
|
|
||||||
|
fn try_from(other: ::xso::exports::minidom::Element) -> ::core::result::Result<Self, Self::Error> {
|
||||||
|
::xso::try_from_element(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro to derive a `xso::FromXml` implementation on a type.
|
||||||
|
///
|
||||||
|
/// The user-facing documentation for this macro lives in the `xso` crate.
|
||||||
|
#[proc_macro_derive(FromXml, attributes(xml))]
|
||||||
|
pub fn from_xml(input: RawTokenStream) -> RawTokenStream {
|
||||||
|
// Shim wrapper around `from_xml_impl` which converts any errors into
|
||||||
|
// actual compiler errors within the resulting token stream.
|
||||||
|
let item = syn::parse_macro_input!(input as Item);
|
||||||
|
match from_xml_impl(item) {
|
||||||
|
Ok(v) => v.into(),
|
||||||
|
Err(e) => e.into_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a `xso::IntoXml` implementation for the given item, or fail with
|
||||||
|
/// a proper compiler error.
|
||||||
|
fn into_xml_impl(input: Item) -> Result<TokenStream> {
|
||||||
|
let (
|
||||||
|
vis,
|
||||||
|
meta::XmlCompoundMeta {
|
||||||
|
namespace,
|
||||||
|
name,
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
ident,
|
||||||
|
) = parse_struct(input)?;
|
||||||
|
|
||||||
|
// we rebind to a different name here to stay consistent with
|
||||||
|
// `from_xml_impl`.
|
||||||
|
let Some(xml_namespace) = namespace else {
|
||||||
|
return Err(Error::new(span, "`namespace` key is required"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(xml_name) = name else {
|
||||||
|
return Err(Error::new(span, "`name` key is required"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let into_events_iter_ty_name = quote::format_ident!("{}IntoEvents", ident);
|
||||||
|
let state_ty_name = quote::format_ident!("{}IntoEventsState", ident);
|
||||||
|
|
||||||
|
let docstr = format!("Decompose a [`{}`] into XML events", ident);
|
||||||
|
|
||||||
|
#[cfg_attr(not(feature = "minidom"), allow(unused_mut))]
|
||||||
|
let mut result = quote! {
|
||||||
|
enum #state_ty_name {
|
||||||
|
Header,
|
||||||
|
Footer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc = #docstr]
|
||||||
|
#vis struct #into_events_iter_ty_name(::core::option::Option<#state_ty_name>);
|
||||||
|
|
||||||
|
impl ::std::iter::Iterator for #into_events_iter_ty_name {
|
||||||
|
type Item = ::core::result::Result<::xso::exports::rxml::Event, ::xso::error::Error>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> ::core::option::Option<Self::Item> {
|
||||||
|
match self.0 {
|
||||||
|
::core::option::Option::Some(#state_ty_name::Header) => {
|
||||||
|
self.0 = ::core::option::Option::Some(#state_ty_name::Footer);
|
||||||
|
::core::option::Option::Some(::core::result::Result::Ok(::xso::exports::rxml::Event::StartElement(
|
||||||
|
::xso::exports::rxml::parser::EventMetrics::zero(),
|
||||||
|
(
|
||||||
|
::xso::exports::rxml::Namespace::from_str(#xml_namespace),
|
||||||
|
match ::xso::exports::rxml::NcName::try_from(#xml_name) {
|
||||||
|
::core::result::Result::Ok(v) => v,
|
||||||
|
::core::result::Result::Err(e) => {
|
||||||
|
self.0 = ::core::option::Option::None;
|
||||||
|
return ::core::option::Option::Some(::core::result::Result::Err(e.into()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
),
|
||||||
|
::xso::exports::rxml::AttrMap::new(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
::core::option::Option::Some(#state_ty_name::Footer) => {
|
||||||
|
self.0 = ::core::option::Option::None;
|
||||||
|
::core::option::Option::Some(::core::result::Result::Ok(::xso::exports::rxml::Event::EndElement(
|
||||||
|
::xso::exports::rxml::parser::EventMetrics::zero(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
::core::option::Option::None => ::core::option::Option::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::xso::IntoXml for #ident {
|
||||||
|
type EventIter = #into_events_iter_ty_name;
|
||||||
|
|
||||||
|
fn into_event_iter(self) -> ::core::result::Result<Self::EventIter, ::xso::error::Error> {
|
||||||
|
::core::result::Result::Ok(#into_events_iter_ty_name(::core::option::Option::Some(#state_ty_name::Header)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "minidom", feature = "panicking-into-impl"))]
|
||||||
|
result.extend(quote! {
|
||||||
|
impl ::std::convert::From<#ident> for ::xso::exports::minidom::Element {
|
||||||
|
fn from(other: #ident) -> Self {
|
||||||
|
::xso::transform(other).expect("seamless conversion into minidom::Element")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(all(feature = "minidom", not(feature = "panicking-into-impl")))]
|
||||||
|
result.extend(quote! {
|
||||||
|
impl ::std::convert::TryFrom<#ident> for ::xso::exports::minidom::Element {
|
||||||
|
type Error = ::xso::error::Error;
|
||||||
|
|
||||||
|
fn try_from(other: #ident) -> ::core::result::Result<Self, Self::Error> {
|
||||||
|
::xso::transform(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro to derive a `xso::IntoXml` implementation on a type.
|
||||||
|
///
|
||||||
|
/// The user-facing documentation for this macro lives in the `xso` crate.
|
||||||
|
#[proc_macro_derive(IntoXml, attributes(xml))]
|
||||||
|
pub fn into_xml(input: RawTokenStream) -> RawTokenStream {
|
||||||
|
// Shim wrapper around `into_xml_impl` which converts any errors into
|
||||||
|
// actual compiler errors within the resulting token stream.
|
||||||
|
let item = syn::parse_macro_input!(input as Item);
|
||||||
|
match into_xml_impl(item) {
|
||||||
|
Ok(v) => v.into(),
|
||||||
|
Err(e) => e.into_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
120
xso-proc/src/meta.rs
Normal file
120
xso-proc/src/meta.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
//! # Parse Rust attributes
|
||||||
|
//!
|
||||||
|
//! This module is concerned with parsing attributes from the Rust "meta"
|
||||||
|
//! annotations on structs, enums, enum variants and fields.
|
||||||
|
|
||||||
|
use proc_macro2::Span;
|
||||||
|
use syn::{spanned::Spanned, *};
|
||||||
|
|
||||||
|
/// Type alias for a `#[xml(namespace = ..)]` attribute.
|
||||||
|
///
|
||||||
|
/// This may, in the future, be replaced by an enum supporting multiple
|
||||||
|
/// ways to specify a namespace.
|
||||||
|
pub(crate) type NamespaceRef = Path;
|
||||||
|
|
||||||
|
/// Type alias for a `#[xml(name = ..)]` attribute.
|
||||||
|
///
|
||||||
|
/// This may, in the future, be replaced by an enum supporting both `Path` and
|
||||||
|
/// `LitStr`.
|
||||||
|
pub(crate) type NameRef = LitStr;
|
||||||
|
|
||||||
|
/// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct XmlCompoundMeta {
|
||||||
|
/// The span of the `#[xml(..)]` meta from which this was parsed.
|
||||||
|
///
|
||||||
|
/// This is useful for error messages.
|
||||||
|
pub(crate) span: Span,
|
||||||
|
|
||||||
|
/// The value assigned to `namespace` inside `#[xml(..)]`, if any.
|
||||||
|
pub(crate) namespace: Option<NamespaceRef>,
|
||||||
|
|
||||||
|
/// The value assigned to `name` inside `#[xml(..)]`, if any.
|
||||||
|
pub(crate) name: Option<NameRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XmlCompoundMeta {
|
||||||
|
/// Parse the meta values from a `#[xml(..)]` attribute.
|
||||||
|
///
|
||||||
|
/// Undefined options or options with incompatible values are rejected
|
||||||
|
/// with an appropriate compile-time error.
|
||||||
|
fn parse_from_attribute(attr: &Attribute) -> Result<Self> {
|
||||||
|
let mut namespace = None;
|
||||||
|
let mut name = None;
|
||||||
|
|
||||||
|
attr.parse_nested_meta(|meta| {
|
||||||
|
if meta.path.is_ident("name") {
|
||||||
|
if name.is_some() {
|
||||||
|
return Err(Error::new_spanned(meta.path, "duplicate `name` key"));
|
||||||
|
}
|
||||||
|
name = Some(meta.value()?.parse()?);
|
||||||
|
Ok(())
|
||||||
|
} else if meta.path.is_ident("namespace") {
|
||||||
|
if namespace.is_some() {
|
||||||
|
return Err(Error::new_spanned(meta.path, "duplicate `namespace` key"));
|
||||||
|
}
|
||||||
|
namespace = Some(meta.value()?.parse()?);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::new_spanned(meta.path, "unsupported key"))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
span: attr.span(),
|
||||||
|
namespace,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search through `attrs` for a single `#[xml(..)]` attribute and parse
|
||||||
|
/// it.
|
||||||
|
///
|
||||||
|
/// Undefined options or options with incompatible values are rejected
|
||||||
|
/// with an appropriate compile-time error.
|
||||||
|
///
|
||||||
|
/// If more than one `#[xml(..)]` attribute is found, an error is
|
||||||
|
/// emitted.
|
||||||
|
///
|
||||||
|
/// If no `#[xml(..)]` attribute is found, `None` is returned.
|
||||||
|
pub(crate) fn try_parse_from_attributes(attrs: &[Attribute]) -> Result<Option<Self>> {
|
||||||
|
let mut result = None;
|
||||||
|
for attr in attrs {
|
||||||
|
if !attr.path().is_ident("xml") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if result.is_some() {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
attr.path(),
|
||||||
|
"only one #[xml(..)] per struct or enum variant allowed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
result = Some(Self::parse_from_attribute(attr)?);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search through `attrs` for a single `#[xml(..)]` attribute and parse
|
||||||
|
/// it.
|
||||||
|
///
|
||||||
|
/// Undefined options or options with incompatible values are rejected
|
||||||
|
/// with an appropriate compile-time error.
|
||||||
|
///
|
||||||
|
/// If more than one or no `#[xml(..)]` attribute is found, an error is
|
||||||
|
/// emitted.
|
||||||
|
pub(crate) fn parse_from_attributes(attrs: &[Attribute]) -> Result<Self> {
|
||||||
|
match Self::try_parse_from_attributes(attrs)? {
|
||||||
|
Some(v) => Ok(v),
|
||||||
|
None => Err(syn::Error::new(
|
||||||
|
Span::call_site(),
|
||||||
|
"#[xml(..)] attribute required on struct or enum variant",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,3 +12,9 @@ license = "MPL-2.0"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rxml = { version = "0.11.0", default-features = false }
|
rxml = { version = "0.11.0", default-features = false }
|
||||||
minidom = { version = "^0.15" }
|
minidom = { version = "^0.15" }
|
||||||
|
xso_proc = { version = "0.0.2", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
macros = [ "dep:xso_proc" ]
|
||||||
|
minidom = [ "xso_proc/minidom"]
|
||||||
|
panicking-into-impl = ["xso_proc/panicking-into-impl"]
|
||||||
|
|
50
xso/src/from_xml_doc.md
Normal file
50
xso/src/from_xml_doc.md
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Make a struct or enum parseable from XML
|
||||||
|
|
||||||
|
This derives the [`FromXml`] trait on a struct or enum. It is the counterpart
|
||||||
|
to [`macro@IntoXml`].
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# use xso::FromXml;
|
||||||
|
static MY_NAMESPACE: &str = "urn:example";
|
||||||
|
|
||||||
|
#[derive(FromXml, Debug, PartialEq)]
|
||||||
|
#[xml(namespace = MY_NAMESPACE, name = "foo")]
|
||||||
|
struct Foo;
|
||||||
|
|
||||||
|
let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example'/>").unwrap();
|
||||||
|
assert_eq!(foo, Foo);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
The derive macros need to know which XML namespace and name the elements it
|
||||||
|
is supposed have. This must be specified via key-value pairs on the type the
|
||||||
|
derive macro is invoked on. These are specified as Rust attributes. In order
|
||||||
|
to disambiguate between XML attributes and Rust attributes, we are going to
|
||||||
|
refer to Rust attributes using the term *meta* instead, which is consistent
|
||||||
|
with the Rust language reference calling that syntax construct *meta*.
|
||||||
|
|
||||||
|
All key-value pairs interpreted by these derive macros must be wrapped in a
|
||||||
|
`#[xml( ... )]` *meta*. The following keys are defined on structs:
|
||||||
|
|
||||||
|
| Key | Value type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `namespace` | *path* | The path to a `&'static str` which holds the XML namespace to match. |
|
||||||
|
| `name` | *string literal* | The XML element name to match. |
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
Supports only empty structs currently. For example, the following will not
|
||||||
|
work:
|
||||||
|
|
||||||
|
```compile_fail
|
||||||
|
# use xso::FromXml;
|
||||||
|
# static MY_NAMESPACE: &str = "urn:example";
|
||||||
|
#[derive(FromXml, Debug, PartialEq)]
|
||||||
|
#[xml(namespace = MY_NAMESPACE, name = "foo")]
|
||||||
|
struct Foo {
|
||||||
|
some_field: String,
|
||||||
|
}
|
||||||
|
```
|
|
@ -20,13 +20,32 @@ use of this library in parsing XML streams like specified in RFC 6120.
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
#[cfg(feature = "minidom")]
|
||||||
pub mod minidom_compat;
|
pub mod minidom_compat;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod exports {
|
pub mod exports {
|
||||||
|
#[cfg(feature = "minidom")]
|
||||||
|
pub use minidom;
|
||||||
pub use rxml;
|
pub use rxml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc = include_str!("from_xml_doc.md")]
|
||||||
|
#[doc(inline)]
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
pub use xso_proc::FromXml;
|
||||||
|
|
||||||
|
/// # Make a struct or enum serialisable to XML
|
||||||
|
///
|
||||||
|
/// This derives the [`IntoXml`] trait on a struct or enum. It is the
|
||||||
|
/// counterpart to [`macro@FromXml`].
|
||||||
|
///
|
||||||
|
/// The attributes necessary and available for the derivation to work are
|
||||||
|
/// documented on [`macro@FromXml`].
|
||||||
|
#[doc(inline)]
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
pub use xso_proc::IntoXml;
|
||||||
|
|
||||||
/// Trait allowing to consume a struct and iterate its contents as
|
/// Trait allowing to consume a struct and iterate its contents as
|
||||||
/// serialisable [`rxml::Event`] items.
|
/// serialisable [`rxml::Event`] items.
|
||||||
///
|
///
|
||||||
|
@ -145,6 +164,7 @@ pub fn transform<T: FromXml, F: IntoXml>(from: F) -> Result<T, self::error::Erro
|
||||||
/// Unlike [`transform`] (which can also be used with an element), this
|
/// Unlike [`transform`] (which can also be used with an element), this
|
||||||
/// function will return the element unharmed if its element header does not
|
/// function will return the element unharmed if its element header does not
|
||||||
/// match the expectations of `T`.
|
/// match the expectations of `T`.
|
||||||
|
#[cfg(feature = "minidom")]
|
||||||
pub fn try_from_element<T: FromXml>(
|
pub fn try_from_element<T: FromXml>(
|
||||||
from: minidom::Element,
|
from: minidom::Element,
|
||||||
) -> Result<T, self::error::FromElementError> {
|
) -> Result<T, self::error::FromElementError> {
|
||||||
|
|
Loading…
Reference in a new issue