1// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7#![forbid(unsafe_code)]
8#![warn(missing_docs)]
9#![allow(rustdoc::private_intra_doc_links)]
10/*!
11# Macros for parsing XML into Rust structs, and vice versa
12
13**If you are a user of `xso_proc` or `xso`, please
14return to `xso` for more information**. The documentation of
15`xso_proc` is geared toward developers of `…_macros` and `…_core`.
16
17**You have been warned.**
18*/
19
20// Wondering about RawTokenStream vs. TokenStream?
21// syn mostly works with proc_macro2, while the proc macros themselves use
22// proc_macro.
23use proc_macro::TokenStream as RawTokenStream;
24use proc_macro2::TokenStream;
25use quote::quote;
26use syn::*;
27
28mod meta;
29
30/// Convert an [`syn::Item`] into the parts relevant for us.
31///
32/// If the item is of an unsupported variant, an appropriate error is
33/// returned.
34fn parse_struct(item: Item) -> Result<(Visibility, meta::XmlCompoundMeta, Ident)> {
35 match item {
36 Item::Struct(item) => {
37 match item.fields {
38 Fields::Unit => (),
39 other => {
40 return Err(Error::new_spanned(
41 other,
42 "cannot derive on non-unit struct (yet!)",
43 ))
44 }
45 }
46 let meta = meta::XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
47 Ok((item.vis, meta, item.ident))
48 }
49 other => Err(Error::new_spanned(other, "cannot derive on this item")),
50 }
51}
52
53/// Generate a `xso::FromXml` implementation for the given item, or fail with
54/// a proper compiler error.
55fn from_xml_impl(input: Item) -> Result<TokenStream> {
56 let (
57 vis,
58 meta::XmlCompoundMeta {
59 namespace,
60 name,
61 span,
62 },
63 ident,
64 ) = parse_struct(input)?;
65
66 // we rebind to a different name here because otherwise some expressions
67 // inside `quote! {}` below get a bit tricky to read (such as
68 // `name.1 == #name`).
69 let Some(xml_namespace) = namespace else {
70 return Err(Error::new(span, "`namespace` key is required"));
71 };
72
73 let Some(xml_name) = name else {
74 return Err(Error::new(span, "`name` key is required"));
75 };
76
77 let from_events_builder_ty_name = quote::format_ident!("{}FromEvents", ident);
78 let state_ty_name = quote::format_ident!("{}FromEventsState", ident);
79
80 let unknown_attr_err = format!("Unknown attribute in {} element.", xml_name.value());
81 let unknown_child_err = format!("Unknown child in {} element.", xml_name.value());
82 let docstr = format!("Build a [`{}`] from XML events", ident);
83
84 #[cfg_attr(not(feature = "minidom"), allow(unused_mut))]
85 let mut result = quote! {
86 enum #state_ty_name {
87 Default,
88 }
89
90 #[doc = #docstr]
91 #vis struct #from_events_builder_ty_name(::core::option::Option<#state_ty_name>);
92
93 impl ::xso::FromEventsBuilder for #from_events_builder_ty_name {
94 type Output = #ident;
95
96 fn feed(
97 &mut self,
98 ev: ::xso::exports::rxml::Event
99 ) -> ::core::result::Result<::core::option::Option<Self::Output>, ::xso::error::Error> {
100 match self.0 {
101 ::core::option::Option::None => panic!("feed() called after it returned a non-None value"),
102 ::core::option::Option::Some(#state_ty_name::Default) => match ev {
103 ::xso::exports::rxml::Event::StartElement(..) => {
104 ::core::result::Result::Err(::xso::error::Error::Other(#unknown_child_err))
105 }
106 ::xso::exports::rxml::Event::EndElement(..) => {
107 self.0 = ::core::option::Option::None;
108 ::core::result::Result::Ok(::core::option::Option::Some(#ident))
109 }
110 ::xso::exports::rxml::Event::Text(..) => {
111 ::core::result::Result::Err(::xso::error::Error::Other("Unexpected text content".into()))
112 }
113 // we ignore these: a correct parser only generates
114 // them at document start, and there we want to indeed
115 // not worry about them being in front of the first
116 // element.
117 ::xso::exports::rxml::Event::XmlDeclaration(_, ::xso::exports::rxml::XmlVersion::V1_0) => ::core::result::Result::Ok(::core::option::Option::None)
118 }
119 }
120 }
121 }
122
123 impl ::xso::FromXml for #ident {
124 type Builder = #from_events_builder_ty_name;
125
126 fn from_events(
127 name: ::xso::exports::rxml::QName,
128 attrs: ::xso::exports::rxml::AttrMap,
129 ) -> ::core::result::Result<Self::Builder, ::xso::error::FromEventsError> {
130 if name.0 != #xml_namespace || name.1 != #xml_name {
131 return ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs });
132 }
133 if attrs.len() > 0 {
134 return ::core::result::Result::Err(::xso::error::Error::Other(#unknown_attr_err).into());
135 }
136 ::core::result::Result::Ok(#from_events_builder_ty_name(::core::option::Option::Some(#state_ty_name::Default)))
137 }
138 }
139 };
140
141 #[cfg(feature = "minidom")]
142 result.extend(quote! {
143 impl ::std::convert::TryFrom<::xso::exports::minidom::Element> for #ident {
144 type Error = ::xso::error::FromElementError;
145
146 fn try_from(other: ::xso::exports::minidom::Element) -> ::core::result::Result<Self, Self::Error> {
147 ::xso::try_from_element(other)
148 }
149 }
150 });
151
152 Ok(result)
153}
154
155/// Macro to derive a `xso::FromXml` implementation on a type.
156///
157/// The user-facing documentation for this macro lives in the `xso` crate.
158#[proc_macro_derive(FromXml, attributes(xml))]
159pub fn from_xml(input: RawTokenStream) -> RawTokenStream {
160 // Shim wrapper around `from_xml_impl` which converts any errors into
161 // actual compiler errors within the resulting token stream.
162 let item = syn::parse_macro_input!(input as Item);
163 match from_xml_impl(item) {
164 Ok(v) => v.into(),
165 Err(e) => e.into_compile_error().into(),
166 }
167}
168
169/// Generate a `xso::IntoXml` implementation for the given item, or fail with
170/// a proper compiler error.
171fn into_xml_impl(input: Item) -> Result<TokenStream> {
172 let (
173 vis,
174 meta::XmlCompoundMeta {
175 namespace,
176 name,
177 span,
178 },
179 ident,
180 ) = parse_struct(input)?;
181
182 // we rebind to a different name here to stay consistent with
183 // `from_xml_impl`.
184 let Some(xml_namespace) = namespace else {
185 return Err(Error::new(span, "`namespace` key is required"));
186 };
187
188 let Some(xml_name) = name else {
189 return Err(Error::new(span, "`name` key is required"));
190 };
191
192 let into_events_iter_ty_name = quote::format_ident!("{}IntoEvents", ident);
193 let state_ty_name = quote::format_ident!("{}IntoEventsState", ident);
194
195 let docstr = format!("Decompose a [`{}`] into XML events", ident);
196
197 #[cfg_attr(not(feature = "minidom"), allow(unused_mut))]
198 let mut result = quote! {
199 enum #state_ty_name {
200 Header,
201 Footer,
202 }
203
204 #[doc = #docstr]
205 #vis struct #into_events_iter_ty_name(::core::option::Option<#state_ty_name>);
206
207 impl ::std::iter::Iterator for #into_events_iter_ty_name {
208 type Item = ::core::result::Result<::xso::exports::rxml::Event, ::xso::error::Error>;
209
210 fn next(&mut self) -> ::core::option::Option<Self::Item> {
211 match self.0 {
212 ::core::option::Option::Some(#state_ty_name::Header) => {
213 self.0 = ::core::option::Option::Some(#state_ty_name::Footer);
214 ::core::option::Option::Some(::core::result::Result::Ok(::xso::exports::rxml::Event::StartElement(
215 ::xso::exports::rxml::parser::EventMetrics::zero(),
216 (
217 ::xso::exports::rxml::Namespace::from_str(#xml_namespace),
218 match ::xso::exports::rxml::NcName::try_from(#xml_name) {
219 ::core::result::Result::Ok(v) => v,
220 ::core::result::Result::Err(e) => {
221 self.0 = ::core::option::Option::None;
222 return ::core::option::Option::Some(::core::result::Result::Err(e.into()));
223
224 }
225
226 }
227 ),
228 ::xso::exports::rxml::AttrMap::new(),
229 )))
230 }
231 ::core::option::Option::Some(#state_ty_name::Footer) => {
232 self.0 = ::core::option::Option::None;
233 ::core::option::Option::Some(::core::result::Result::Ok(::xso::exports::rxml::Event::EndElement(
234 ::xso::exports::rxml::parser::EventMetrics::zero(),
235 )))
236 }
237 ::core::option::Option::None => ::core::option::Option::None,
238 }
239 }
240 }
241
242 impl ::xso::IntoXml for #ident {
243 type EventIter = #into_events_iter_ty_name;
244
245 fn into_event_iter(self) -> ::core::result::Result<Self::EventIter, ::xso::error::Error> {
246 ::core::result::Result::Ok(#into_events_iter_ty_name(::core::option::Option::Some(#state_ty_name::Header)))
247 }
248 }
249 };
250
251 #[cfg(all(feature = "minidom", feature = "panicking-into-impl"))]
252 result.extend(quote! {
253 impl ::std::convert::From<#ident> for ::xso::exports::minidom::Element {
254 fn from(other: #ident) -> Self {
255 ::xso::transform(other).expect("seamless conversion into minidom::Element")
256 }
257 }
258 });
259
260 #[cfg(all(feature = "minidom", not(feature = "panicking-into-impl")))]
261 result.extend(quote! {
262 impl ::std::convert::TryFrom<#ident> for ::xso::exports::minidom::Element {
263 type Error = ::xso::error::Error;
264
265 fn try_from(other: #ident) -> ::core::result::Result<Self, Self::Error> {
266 ::xso::transform(other)
267 }
268 }
269 });
270
271 Ok(result)
272}
273
274/// Macro to derive a `xso::IntoXml` implementation on a type.
275///
276/// The user-facing documentation for this macro lives in the `xso` crate.
277#[proc_macro_derive(IntoXml, attributes(xml))]
278pub fn into_xml(input: RawTokenStream) -> RawTokenStream {
279 // Shim wrapper around `into_xml_impl` which converts any errors into
280 // actual compiler errors within the resulting token stream.
281 let item = syn::parse_macro_input!(input as Item);
282 match into_xml_impl(item) {
283 Ok(v) => v.into(),
284 Err(e) => e.into_compile_error().into(),
285 }
286}