xml-macro 0.3.0

A convenience macro for quick-xml
Documentation
#![doc=include_str!("../README.md")]
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use syn::{
    braced,
    ext::IdentExt as _,
    parse::{Parse, ParseStream},
    parse_macro_input,
    spanned::Spanned,
    token::{self, Brace},
    Block, LitStr, Token,
};

struct Xmls {
    events: Vec<XmlEvent>,
}

impl Parse for Xmls {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut events = Vec::new();
        while !input.is_empty() {
            events.push(input.parse()?)
        }
        Ok(Self { events })
    }
}

/// An xml event
enum XmlEvent {
    Tag(XmlTag),
    Text(Block),
}

impl Parse for XmlEvent {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(Brace) {
            Ok(Self::Text(input.parse()?))
        } else {
            Ok(Self::Tag(input.parse()?))
        }
    }
}

impl ToTokens for XmlEvent {
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        match self {
            XmlEvent::Tag(x) => x.to_tokens(tokens),
            XmlEvent::Text(x) => quote! {
                ::quick_xml::events::Event::Text (
                    ::quick_xml::events::BytesText::new(&*#x)
                )
            }
            .to_tokens(tokens),
        }
    }
}

/// An xml tag
struct XmlTag {
    /// The open token
    #[allow(dead_code)]
    open: Token![<],
    is_end_tag: Option<Token![/]>,
    name: XmlId,
    attributes: Vec<XmlAttribute>,
    self_close: Option<Token![/]>,
    #[allow(dead_code)]
    close: Token![>],
}

impl Parse for XmlTag {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        Ok(Self {
            open: input.parse()?,
            is_end_tag: input.parse()?,
            name: input.parse()?,
            attributes: {
                let mut vec = Vec::new();
                loop {
                    let lookahead = input.lookahead1();
                    if lookahead.peek(Token![/]) || lookahead.peek(Token![>]) {
                        break;
                    } else {
                        vec.push(input.parse()?)
                    }
                }
                vec
            },
            self_close: input.parse()?,
            close: input.parse()?,
        })
    }
}

impl ToTokens for XmlTag {
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        let mut attrs = Vec::new();
        for attribute in &self.attributes {
            let name = attribute.key();
            let val = attribute.value();
            attrs.push(quote! {
                event_start.push_attribute((#name, #val));
            });
        }

        let name = self.name.to_lit_str();
        let expanded = if self.is_end_tag.is_some() {
            if self.self_close.is_some() {
                quote! { compile_error!("Tag cannot start with `</` and end with `/>`") }
            } else {
                if !attrs.is_empty() {
                    quote! { compile_error!("End tag cannot have attribute") }
                } else {
                    quote! {{
                    let mut event = ::quick_xml::events::BytesEnd::new(#name);
                    ::quick_xml::events::Event::End(event)
                    }}
                }
            }
        } else {
            let event_type = if self.self_close.is_some() {
                quote! {Empty}
            } else {
                quote! {Start}
            };
            quote! {{
                let mut event_start = ::quick_xml::events::BytesStart::new(#name);
                #(#attrs)*
                ::quick_xml::events::Event::#event_type(event_start)
            }}
        };

        tokens.extend(expanded);
    }
}

struct XmlId(Vec<ExtraXmlIdSeg>);

impl XmlId {
    pub fn to_lit_str(&self) -> LitStr {
        let mut buf = String::new();
        for x in &self.0 {
            match x {
                ExtraXmlIdSeg::Ident(x) => buf.push_str(&x.to_string()),
                ExtraXmlIdSeg::Dot(_) => buf.push('.'),
                ExtraXmlIdSeg::Colon(_) => buf.push(':'),
                ExtraXmlIdSeg::Dash(_) => buf.push('-'),
            }
        }
        LitStr::new(&buf, self.0[0].span())
    }
}

impl Parse for XmlId {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut vec = Vec::new();
        let mut last_one_ident = false;
        loop {
            if (input.peek(syn::Ident::peek_any) && !last_one_ident)
                || input.peek(Token![.])
                || input.peek(Token![:])
                || input.peek(Token![-])
            {
                if input.peek(syn::Ident::peek_any) {
                    last_one_ident = true;
                } else {
                    last_one_ident = false;
                }
                vec.push(input.parse()?);
            } else {
                break;
            }
        }
        Ok(Self(vec))
    }
}

impl ToTokens for XmlId {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        for seg in &self.0 {
            seg.to_tokens(tokens);
        }
    }
}

enum ExtraXmlIdSeg {
    Ident(Ident),
    Dot(Token![.]),
    Colon(Token![:]),
    Dash(Token![-]),
}

impl ExtraXmlIdSeg {
    fn span(&self) -> proc_macro2::Span {
        match self {
            ExtraXmlIdSeg::Ident(x) => x.span(),
            ExtraXmlIdSeg::Dot(x) => x.span(),
            ExtraXmlIdSeg::Colon(x) => x.span(),
            ExtraXmlIdSeg::Dash(x) => x.span(),
        }
    }
}

impl Parse for ExtraXmlIdSeg {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(Token![.]) {
            Ok(ExtraXmlIdSeg::Dot(input.parse()?))
        } else if lookahead.peek(Token![:]) {
            Ok(ExtraXmlIdSeg::Colon(input.parse()?))
        } else if lookahead.peek(Token![-]) {
            Ok(ExtraXmlIdSeg::Dash(input.parse()?))
        } else {
            Ok(ExtraXmlIdSeg::Ident(input.parse()?))
        }
    }
}

impl ToTokens for ExtraXmlIdSeg {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        match self {
            Self::Dot(x) => x.to_tokens(tokens),
            Self::Colon(x) => x.to_tokens(tokens),
            Self::Dash(x) => x.to_tokens(tokens),
            Self::Ident(x) => x.to_tokens(tokens),
        }
    }
}

/// An attribute in xml, such as `name="Josh"`
enum XmlAttribute {
    Idented {
        #[allow(dead_code)]
        brace: Brace,
        ident: Ident,
    },
    KV {
        name: XmlId,
        #[allow(dead_code)]
        eq: Token![=],
        value: XmlVal,
    },
}

impl XmlAttribute {
    fn key(&self) -> LitStr {
        match self {
            XmlAttribute::Idented { ident, .. } => LitStr::new(&ident.to_string(), ident.span()),
            XmlAttribute::KV { name, .. } => name.to_lit_str(),
        }
    }

    fn value(&self) -> TokenStream2 {
        match self {
            XmlAttribute::Idented { ident, .. } => quote!({&*#ident}),
            XmlAttribute::KV { value, .. } => quote!(&*#value),
        }
    }
}

impl Parse for XmlAttribute {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(syn::token::Brace) {
            let content;
            Ok(Self::Idented {
                brace: braced!(content in input),
                ident: content.parse()?,
            })
        } else {
            Ok(Self::KV {
                name: input.parse()?,
                eq: input.parse()?,
                value: input.parse()?,
            })
        }
    }
}

enum XmlVal {
    Str(LitStr),
    Block(Block),
}

impl ToTokens for XmlVal {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        match self {
            Self::Str(x) => x.to_tokens(tokens),
            Self::Block(x) => quote! {#x}.to_tokens(tokens),
        }
    }
}

impl Parse for XmlVal {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(token::Brace) {
            Ok(XmlVal::Block(input.parse()?))
        } else {
            Ok(XmlVal::Str(input.parse()?))
        }
    }
}

#[proc_macro]
pub fn xml(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as XmlEvent);
    match input {
        XmlEvent::Tag(tag) => tag.to_token_stream().into(),
        XmlEvent::Text(text) => quote! {
            ::quick_xml::events::Event::Text(
                ::quick_xml::events::BytesText::new(&*#text)
            )
        }
        .into(),
    }
}

#[proc_macro]
pub fn xmls(input: TokenStream) -> TokenStream {
    let xmls = parse_macro_input!(input as Xmls);
    let mut ts = TokenStream2::new();
    for xml in xmls.events {
        xml.to_tokens(&mut ts);
        Token![,](Span::call_site()).to_tokens(&mut ts);
    }
    quote! {[#ts]}.into()
}