galvyn-macros 0.2.0

Auto-generated OpenAPI documentation
Documentation
use std::collections::HashMap;

use proc_macro2::Delimiter;
use proc_macro2::Group;
use proc_macro2::Ident;
use proc_macro2::Literal;
use proc_macro2::TokenStream;
use proc_macro2::TokenTree;
use quote::quote;
use quote::quote_spanned;
use syn::parse2;
use syn::ItemFn;

pub fn parse(args: TokenStream, item: TokenStream) -> Result<(Args, ItemFn), TokenStream> {
    match parse2(item) {
        Ok(item) => Ok((parse_args(args)?, item)),
        Err(err) => Err(err.into_compile_error()),
    }
}

pub struct Args {
    pub positional: Vec<TokenTree>,
    pub keyword: HashMap<Ident, TokenTree>,
}
fn parse_args(args: TokenStream) -> Result<Args, TokenStream> {
    let mut args_iter = args.clone().into_iter();
    enum Arg {
        Pos(TokenTree),
        Key(Ident, TokenTree),
    }
    let mut args_vec = Vec::new();
    loop {
        let Some(first) = args_iter.next() else {
            break;
        };

        let Some(punct) = args_iter.next() else {
            args_vec.push(Arg::Pos(first));
            break;
        };

        match punct {
            TokenTree::Punct(punct) if punct.as_char() == ',' => {
                args_vec.push(Arg::Pos(first));
                continue;
            }
            TokenTree::Punct(punct) if punct.as_char() == '=' => {
                let TokenTree::Ident(first) = first else {
                    return Err(quote_spanned!(first.span()=>
                        compile_error!(concat!("expected identifier got `", stringify!(#first), "`"));
                    ));
                };

                let Some(second) = args_iter.next() else {
                    return Err(quote_spanned! {punct.span()=>
                        compile_error!("missing value");
                    });
                };

                args_vec.push(Arg::Key(first, second));
            }
            TokenTree::Group(group) if matches!(group.delimiter(), Delimiter::Parenthesis) => {
                let TokenTree::Ident(first) = first else {
                    return Err(quote_spanned!(first.span()=>
                        compile_error!(concat!("expected identifier got `", stringify!(#first), "`"));
                    ));
                };

                args_vec.push(Arg::Key(
                    first,
                    TokenTree::Group(Group::new(Delimiter::Bracket, group.stream())),
                ));
            }
            _ => {
                return Err(quote_spanned! {punct.span()=>
                    compile_error!(concat!("expected `,`, `=` or `(` got `", stringify!(#punct), "`"));
                })
            }
        }

        match args_iter.next() {
            None => {
                break;
            }
            Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => {
                continue;
            }
            Some(token) => {
                return Err(quote_spanned!(token.span()=>
                    compile_error!(concat!("expected `,` got `", stringify!(#token), "`"));
                ))
            }
        }
    }

    let mut positional = Vec::new();
    let mut keyword = HashMap::new();
    let mut duplicate = false;
    let mut wrong_order = false;
    for arg in args_vec {
        match arg {
            Arg::Pos(arg) => {
                positional.push(arg);
                if !keyword.is_empty() {
                    wrong_order = true;
                }
            }
            Arg::Key(key, val) => {
                if keyword.insert(key, val).is_some() {
                    duplicate = true;
                }
            }
        }
    }

    if duplicate || wrong_order {
        let mut format_str = String::with_capacity((positional.len() + keyword.len()) * 2);
        while format_str.len() < format_str.capacity() {
            format_str.push_str("{}");
        }
        let format_str = Literal::string(&format_str);
        return Err(quote!(
            const _: fn() = || {
                format!(#format_str, #args):
            };
        ));
    }

    Ok(Args {
        positional,
        keyword,
    })
}