Skip to main content

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