instant_xml_macros/
lib.rs

1//! Derive macros for instant-xml
2//!
3//! This crate provides the [`ToXml`] and [`FromXml`] derive macros for XML serialization
4//! and deserialization. See the instant-xml crate for usage examples.
5
6extern crate proc_macro;
7
8use std::collections::BTreeSet;
9use std::mem;
10
11use proc_macro2::{Literal, Span, TokenStream};
12use quote::{quote, ToTokens};
13use syn::spanned::Spanned;
14use syn::{parse_macro_input, DeriveInput, Generics};
15
16mod case;
17use case::RenameRule;
18mod de;
19mod meta;
20use meta::{meta_items, MetaItem, Namespace, NamespaceMeta};
21mod ser;
22
23/// Derives XML serialization for a struct or enum
24///
25/// This macro supports `#[xml(...)]` attributes for configuring serialization behavior.
26/// See the instant-xml crate-level documentation for more details.
27#[proc_macro_derive(ToXml, attributes(xml))]
28pub fn to_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
29    let ast = parse_macro_input!(input as syn::DeriveInput);
30    ser::to_xml(&ast).into()
31}
32
33/// Derives XML deserialization for a struct or enum
34///
35/// This macro supports `#[xml(...)]` attributes for configuring deserialization behavior.
36/// See the instant-xml crate-level documentation for more details.
37#[proc_macro_derive(FromXml, attributes(xml))]
38pub fn from_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
39    let ast = parse_macro_input!(input as syn::DeriveInput);
40    proc_macro::TokenStream::from(de::from_xml(&ast))
41}
42
43struct ContainerMeta<'input> {
44    input: &'input DeriveInput,
45    ns: NamespaceMeta,
46    rename: Option<Literal>,
47    rename_all: RenameRule,
48    mode: Option<Mode>,
49}
50
51impl<'input> ContainerMeta<'input> {
52    fn from_derive(input: &'input syn::DeriveInput) -> Result<Self, syn::Error> {
53        let mut ns = NamespaceMeta::default();
54        let mut rename = Default::default();
55        let mut rename_all = Default::default();
56        let mut mode = None;
57
58        for (item, span) in meta_items(&input.attrs) {
59            match item {
60                MetaItem::Ns(namespace) => ns = namespace,
61                MetaItem::Rename(lit) => rename = Some(lit),
62                MetaItem::RenameAll(lit) => {
63                    rename_all = match RenameRule::from_str(&lit.to_string()) {
64                        Ok(rule) => rule,
65                        Err(err) => return Err(syn::Error::new(span, err)),
66                    };
67                }
68                MetaItem::Mode(new) => match mode {
69                    None => mode = Some(new),
70                    Some(_) => return Err(syn::Error::new(span, "cannot have two modes")),
71                },
72                _ => {
73                    return Err(syn::Error::new(
74                        span,
75                        "invalid field in container xml attribute",
76                    ))
77                }
78            }
79        }
80
81        Ok(Self {
82            input,
83            ns,
84            rename,
85            rename_all,
86            mode,
87        })
88    }
89
90    fn xml_generics(&self, borrowed: BTreeSet<syn::Lifetime>) -> Generics {
91        let mut xml_generics = self.input.generics.clone();
92        let mut xml = syn::LifetimeParam::new(syn::Lifetime::new("'xml", Span::call_site()));
93        xml.bounds.extend(borrowed);
94        xml_generics.params.push(xml.into());
95
96        for param in xml_generics.type_params_mut() {
97            param
98                .bounds
99                .push(syn::parse_str("::instant_xml::FromXml<'xml>").unwrap());
100        }
101
102        xml_generics
103    }
104
105    fn tag(&self) -> TokenStream {
106        match &self.rename {
107            Some(name) => quote!(#name),
108            None => self.input.ident.to_string().into_token_stream(),
109        }
110    }
111
112    fn default_namespace(&self) -> TokenStream {
113        match &self.ns.uri {
114            Some(ns) => quote!(#ns),
115            None => quote!(""),
116        }
117    }
118}
119
120#[derive(Debug, Default)]
121struct FieldMeta {
122    attribute: bool,
123    borrow: bool,
124    direct: bool,
125    ns: NamespaceMeta,
126    tag: TokenStream,
127    serialize_with: Option<Literal>,
128    deserialize_with: Option<Literal>,
129}
130
131impl FieldMeta {
132    fn from_field(input: &syn::Field, container: &ContainerMeta) -> Result<FieldMeta, syn::Error> {
133        let field_name = input.ident.as_ref().unwrap();
134        let mut meta = FieldMeta {
135            tag: container
136                .rename_all
137                .apply_to_field(field_name)
138                .into_token_stream(),
139            ..Default::default()
140        };
141
142        for (item, span) in meta_items(&input.attrs) {
143            match item {
144                MetaItem::Attribute => meta.attribute = true,
145                MetaItem::Borrow => meta.borrow = true,
146                MetaItem::Direct => meta.direct = true,
147                MetaItem::Ns(ns) => meta.ns = ns,
148                MetaItem::Rename(lit) => meta.tag = quote!(#lit),
149                MetaItem::SerializeWith(lit) => meta.serialize_with = Some(lit),
150                MetaItem::DeserializeWith(lit) => meta.deserialize_with = Some(lit),
151                MetaItem::RenameAll(_) => {
152                    return Err(syn::Error::new(
153                        span,
154                        "attribute 'rename_all' invalid in field xml attribute",
155                    ))
156                }
157                MetaItem::Mode(_) => {
158                    return Err(syn::Error::new(span, "invalid attribute for struct field"));
159                }
160            }
161        }
162
163        Ok(meta)
164    }
165}
166
167#[derive(Debug, Default)]
168struct VariantMeta {
169    serialize_as: TokenStream,
170}
171
172impl VariantMeta {
173    fn from_variant(
174        input: &syn::Variant,
175        container: &ContainerMeta,
176    ) -> Result<VariantMeta, syn::Error> {
177        if !input.fields.is_empty() {
178            return Err(syn::Error::new(
179                input.fields.span(),
180                "only unit enum variants are permitted!",
181            ));
182        }
183
184        let mut rename = None;
185        for (item, span) in meta_items(&input.attrs) {
186            match item {
187                MetaItem::Rename(lit) => rename = Some(lit.to_token_stream()),
188                _ => {
189                    return Err(syn::Error::new(
190                        span,
191                        "only 'rename' attribute is permitted on enum variants",
192                    ))
193                }
194            }
195        }
196
197        let discriminant = match input.discriminant {
198            Some((
199                _,
200                syn::Expr::Lit(syn::ExprLit {
201                    lit: syn::Lit::Str(ref lit),
202                    ..
203                }),
204            )) => Some(lit.to_token_stream()),
205            Some((
206                _,
207                syn::Expr::Lit(syn::ExprLit {
208                    lit: syn::Lit::Int(ref lit),
209                    ..
210                }),
211            )) => Some(lit.base10_digits().to_token_stream()),
212            Some((_, ref value)) => {
213                return Err(syn::Error::new(
214                    value.span(),
215                    "invalid field discriminant value!",
216                ))
217            }
218            None => None,
219        };
220
221        if discriminant.is_some() && rename.is_some() {
222            return Err(syn::Error::new(
223                input.span(),
224                "conflicting `rename` attribute and variant discriminant!",
225            ));
226        }
227
228        let serialize_as = match rename.or(discriminant) {
229            Some(lit) => lit.into_token_stream(),
230            None => container
231                .rename_all
232                .apply_to_variant(&input.ident)
233                .to_token_stream(),
234        };
235
236        Ok(VariantMeta { serialize_as })
237    }
238}
239
240fn discard_lifetimes(
241    ty: &mut syn::Type,
242    borrowed: &mut BTreeSet<syn::Lifetime>,
243    borrow: bool,
244    top: bool,
245) {
246    match ty {
247        syn::Type::Path(ty) => discard_path_lifetimes(ty, borrowed, borrow),
248        syn::Type::Reference(ty) => {
249            if top {
250                // If at the top level, we'll want to borrow from `&'a str` and `&'a [u8]`.
251                match &*ty.elem {
252                    syn::Type::Path(inner) if top && inner.path.is_ident("str") => {
253                        if let Some(lt) = ty.lifetime.take() {
254                            borrowed.insert(lt);
255                        }
256                    }
257                    syn::Type::Slice(inner) if top => match &*inner.elem {
258                        syn::Type::Path(inner) if inner.path.is_ident("u8") => {
259                            borrowed.extend(ty.lifetime.take());
260                        }
261                        _ => {}
262                    },
263                    _ => {}
264                }
265            } else if borrow {
266                // Otherwise, only borrow if the user has requested it.
267                borrowed.extend(ty.lifetime.take());
268            } else {
269                ty.lifetime = None;
270            }
271
272            discard_lifetimes(&mut ty.elem, borrowed, borrow, false);
273        }
274        _ => {}
275    }
276}
277
278fn discard_path_lifetimes(
279    path: &mut syn::TypePath,
280    borrowed: &mut BTreeSet<syn::Lifetime>,
281    borrow: bool,
282) {
283    if let Some(q) = &mut path.qself {
284        discard_lifetimes(&mut q.ty, borrowed, borrow, false);
285    }
286
287    for segment in &mut path.path.segments {
288        match &mut segment.arguments {
289            syn::PathArguments::None => {}
290            syn::PathArguments::AngleBracketed(args) => {
291                args.args.iter_mut().for_each(|arg| match arg {
292                    syn::GenericArgument::Lifetime(lt) => {
293                        let lt = mem::replace(lt, syn::Lifetime::new("'_", Span::call_site()));
294                        if borrow {
295                            borrowed.insert(lt);
296                        }
297                    }
298                    syn::GenericArgument::Type(ty) => {
299                        discard_lifetimes(ty, borrowed, borrow, false)
300                    }
301                    _ => {}
302                })
303            }
304            syn::PathArguments::Parenthesized(args) => args
305                .inputs
306                .iter_mut()
307                .for_each(|ty| discard_lifetimes(ty, borrowed, borrow, false)),
308        }
309    }
310}
311
312#[derive(Clone, Copy, Debug, Eq, PartialEq)]
313enum Mode {
314    Forward,
315    Scalar,
316    Transparent,
317}
318
319#[cfg(test)]
320mod tests {
321    use syn::parse_quote;
322
323    #[test]
324    fn non_unit_enum_variant_unsupported() {
325        dbg!(super::ser::to_xml(&parse_quote! {
326            #[xml(scalar)]
327            pub enum TestEnum {
328                Foo(String),
329                Bar,
330                Baz
331            }
332        })
333        .to_string())
334        .find("compile_error ! { \"only unit enum variants are permitted!\" }")
335        .unwrap();
336    }
337
338    #[test]
339    fn non_scalar_enums_unsupported() {
340        dbg!(super::ser::to_xml(&parse_quote! {
341            #[xml()]
342            pub enum TestEnum {
343                Foo,
344                Bar,
345                Baz
346            }
347        })
348        .to_string())
349        .find("compile_error ! { \"missing mode\" }")
350        .unwrap();
351    }
352
353    #[test]
354    fn scalar_variant_attribute_not_permitted() {
355        dbg!(super::ser::to_xml(&parse_quote! {
356            #[xml(scalar)]
357            pub enum TestEnum {
358                Foo,
359                Bar,
360                #[xml(scalar)]
361                Baz
362            }
363        })
364        .to_string())
365        .find("compile_error ! { \"only 'rename' attribute is permitted on enum variants\" }")
366        .unwrap();
367    }
368
369    #[test]
370    fn scalar_discrimintant_must_be_literal() {
371        assert_eq!(
372            None,
373            dbg!(super::ser::to_xml(&parse_quote! {
374                #[xml(scalar)]
375                pub enum TestEnum {
376                    Foo = 1,
377                    Bar,
378                    Baz
379                }
380            })
381            .to_string())
382            .find("compile_error ! { \"invalid field discriminant value!\" }")
383        );
384
385        dbg!(super::ser::to_xml(&parse_quote! {
386            #[xml(scalar)]
387            pub enum TestEnum {
388                Foo = 1+1,
389                Bar,
390                Baz
391            }
392        })
393        .to_string())
394        .find("compile_error ! { \"invalid field discriminant value!\" }")
395        .unwrap();
396    }
397
398    #[test]
399    fn rename_all_attribute_not_permitted() {
400        dbg!(super::ser::to_xml(&parse_quote! {
401            pub struct TestStruct {
402                #[xml(rename_all = "UPPERCASE")]
403                field_1: String,
404                field_2: u8,
405            }
406        })
407        .to_string())
408        .find("compile_error ! { \"attribute 'rename_all' invalid in field xml attribute\" }")
409        .unwrap();
410
411        dbg!(super::ser::to_xml(&parse_quote! {
412            #[xml(scalar)]
413            pub enum TestEnum {
414                Foo = 1,
415                Bar,
416                #[xml(rename_all = "UPPERCASE")]
417                Baz
418            }
419        })
420        .to_string())
421        .find("compile_error ! { \"only 'rename' attribute is permitted on enum variants\" }")
422        .unwrap();
423    }
424
425    #[test]
426    fn bogus_rename_all_not_permitted() {
427        dbg!(super::ser::to_xml(&parse_quote! {
428            #[xml(rename_all = "forgetaboutit")]
429            pub struct TestStruct {
430                field_1: String,
431                field_2: u8,
432            }
433        })
434        .to_string())
435        .find("compile_error ! {")
436        .unwrap();
437    }
438}