lib.rs

  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}