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