telegram-bot2-macros 0.1.3

telegram-bot2-macros contains all macros used by the telegram-bot2 crate
Documentation
use crate::builder::{unwrap_type, Wrapper};
use proc_macro::Span;
use proc_macro::TokenStream;
use proc_macro_error::abort;
use quote::ToTokens;

use crate::constants::*;
use lazy_static::lazy_static;
use regex::Regex;
use std::str::FromStr;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Expr, FnArg, Item, Lit, Token, Type};

#[derive(Debug)]
struct ParamType {
    ty: String,
    wrapper: Option<Wrapper>,
}

#[derive(Debug)]
struct Param {
    name: String,
    ty: Option<ParamType>,
}

pub fn try_command(attr: TokenStream, item: TokenStream) -> Result<TokenStream, ()> {
    // Parse the given token stream into a function item
    let item = if let Item::Fn(f) = syn::parse::<Item>(item).map_err(|_| abort!(Span::call_site(), "Could not parse"))? {
        f
    } else {
        abort!(Span::call_site(), "Can only be applied to functions")
    };

    // Parse the attr token stream into a list of expressions
    let attr = Punctuated::<Expr, Token![,]>::parse_terminated.parse(attr).map_err(|_| abort!(Span::call_site(), "Could not parse attributes"))?;

    // Get the syntax attribute
    let syntax = attr
        .iter()
        .filter(|e| matches!(e, Expr::Lit(_)))
        .map(|e| match e {
            Expr::Lit(l) => &l.lit,
            _ => panic!(),
        })
        .filter(|l| matches!(l, Lit::Str(_)))
        .map(|l| match l {
            Lit::Str(s) => s.value(),
            _ => panic!(),
        })
        .collect::<Vec<String>>()
        .first()
        .ok_or_else(|| abort!(attr.span(), "Missing command syntax attribute"))?
        .clone();

    lazy_static! {
        static ref SYNTAX_DELIMITER: Regex = Regex::new(r"\s+").unwrap();
    }
    // Split the syntax string
    let mut syntax_vec: Vec<String> = SYNTAX_DELIMITER.split(syntax.as_str()).map(str::to_string).collect();

    // Get the name of the command
    let name = syntax_vec.first().unwrap_or_else(|| abort!(Span::call_site(), "Invalid command syntax")).clone();
    syntax_vec.remove(0);

    // Check and remove the leading '/'
    if &name[0..1] != "/" {
        abort!(Span::call_site(), "Invalid command syntax")
    }
    let name = name.as_str()[1..].to_string();

    // Convert the list of parameters of the command
    let params = syntax_vec
        .iter()
        .map(|s| {
            lazy_static! {
                static ref DYNAMIC_PARAM: Regex = Regex::new(r"^<(.*)>$").unwrap();
            }
            if let Some(mat) = DYNAMIC_PARAM.captures(s.as_str()) {
                let s = mat.get(1).unwrap().as_str();
                let fun_par: (String, String) = item
                    .sig
                    .inputs
                    .iter()
                    .find_map(|a| match a {
                        FnArg::Receiver(_) => None,
                        FnArg::Typed(pat) => (pat.pat.to_token_stream().to_string() == *s).then(|| (pat.pat.to_token_stream().to_string(), pat.ty.to_token_stream().to_string())),
                    })
                    .unwrap_or_else(|| abort!(Span::call_site(), "Missing parameter with name {}", s));

                let (ty, wrapper) = unwrap_type(fun_par.1.replace(" ", ""));
                Param {
                    name: s.to_string(),
                    ty: Some(ParamType { ty, wrapper }),
                }
            } else {
                Param { name: s.clone(), ty: None }
            }
        })
        .collect::<Vec<Param>>();

    // Check that all params follow the correct wrapper order (None* -> Option* -> Vec?)
    if !params
        .iter()
        .fold((None, true), |(acc, b), v| {
            if let Some(ty) = &v.ty {
                match (acc, ty.wrapper.as_ref(), b) {
                    (_, _, false) => (None, false),
                    (None, None, _) => (None, true),
                    (Some(Wrapper::Option), Some(Wrapper::Option), _) => (Some(Wrapper::Option), true),
                    (Some(Wrapper::Vec), Some(Wrapper::Vec), _) => (Some(Wrapper::Vec), true),
                    (None, Some(Wrapper::Option), _) => (Some(Wrapper::Option), true),
                    (Some(Wrapper::Option), Some(Wrapper::Vec), _) => (Some(Wrapper::Vec), true),
                    _ => (None, false),
                }
            } else {
                (acc, b)
            }
        })
        .1
    {
        abort!(Span::call_site(), "Parameters order is not respected")
    }

    lazy_static! {
        static ref STATE_REGEX: Regex = Regex::new("(?:(?:std::)?sync::)?Arc<(?:(?:std::)?opt::)?Option<(.*)>>").unwrap();
    }

    let type_bounds = item
        .sig
        .inputs
        .iter()
        .map(|a| match a {
            FnArg::Receiver(_) => abort!(a, "Handlers are not allowed self parameters"),
            FnArg::Typed(t) => {
                let name = t.pat.to_token_stream().to_string();

                if let Some(p) = params.iter().find(|p| p.name == name) {
                    if let Some(ty) = &p.ty {
                        format!("{}: {FROM_STR}", ty.ty)
                    } else {
                        String::new()
                    }
                } else {
                    match t.ty.as_ref() {
                        Type::Reference(r) => {
                            format!("for<'a> &'a {} {}: {FROM_UPDATE}<'a, T>", r.mutability.map(|_| "mut").unwrap_or(""), r.elem.to_token_stream())
                        }
                        _ => format!("for<'a> {}: {FROM_UPDATE}<'a, T>", t.ty.to_token_stream()),
                    }
                }
            }
        })
        .collect::<Vec<_>>()
        .join(", ");

    // Get the rank parameter (or 0 as default)
    let rank = attr
        .iter()
        .find_map(|e| match e {
            Expr::Assign(a) => match (a.left.as_ref(), a.right.as_ref()) {
                (Expr::Path(path), Expr::Lit(lit)) => (path.path.to_token_stream().to_string() == "rank").then(|| match &lit.lit {
                    Lit::Str(s) => u8::from_str(s.value().as_str()).ok(),
                    _ => None,
                }),
                _ => None,
            },
            _ => None,
        })
        .flatten()
        .unwrap_or(0);

    // Get the function visibility
    let vis = item.vis.to_token_stream().to_string();

    Ok(format!(
        r#"
            {vis} fn {name}_command<T: Sync>() -> {COMMAND_HANDLER}<T>
            where {type_bounds}
            {{
                struct H {{}}
                #[async_trait::async_trait]
                impl<T: {SYNC}> {COMMAND_HANDLER_TRAIT}<T> for H
                where {type_bounds}
                {{
                    async fn handle(&self, args: &[String], u: &{UPDATE}, b: &{BOT}, s: Option<&{BOT_STATE}<T>>) -> Result<(), {HANDLER_ERROR}> {{
                        let mut __i = 0;
                        {}
                        {name}({}).await.map_err(|e| {{
                            {LOG}::error!("Command {name} exited with {{:?}}", e);
                            {HANDLER_ERROR}::Runtime
                        }})
                    }}
                }}

                {COMMAND_HANDLER} {{
                    name: "{name}".to_string(),
                    handler: Box::new(H {{}}),
                    rank: {rank},
                    syntax: "{syntax}",
                }}
            }}

            {}
            "#,
        params
            .iter()
            .map(|p| {
                match &p.ty {
                    None => format!("if args.get(__i).is_none() || args.get(__i).unwrap() != \"{}\" {{ return Err({HANDLER_ERROR}::Parse) }}", p.name),
                    Some(ty) => {
                        let from_str = format!("<{} as std::str::FromStr>::from_str(args.get(__i).ok_or({HANDLER_ERROR}::Parse)?.as_str()).map_err(|_| {HANDLER_ERROR}::Parse)?", ty.ty);
                        match ty.wrapper {
                            None => format!(r#"let {} = {};"#, p.name, from_str),
                            Some(Wrapper::Option) => format!(r#"let {} = if let Some(s) = args.get(__i) {{ Some({}) }} else {{ None }};"#, p.name, from_str),
                            Some(Wrapper::Vec) => format!(r#"let mut {name} = vec[]; while let Some(s) = args.get(__i) {{ {name}.push({}}}"#, from_str, name = p.name),
                        }
                    }
                }
            })
            .collect::<Vec<String>>()
            .join("\n__i += 1;\n"),
        item.sig
            .inputs
            .iter()
            .map(|a| {
                if let FnArg::Typed(a) = a {
                    let name = a.pat.to_token_stream().to_string();

                    if params.iter().any(|p| p.name == name) {
                        name
                    } else {
                        format!("<{} as {FROM_UPDATE}<T>>::from_update(b, u, s).ok_or({HANDLER_ERROR}::Parse)?", a.ty.to_token_stream())
                    }
                } else {
                    abort!(a, "Invalid parameter")
                }
            })
            .collect::<Vec<String>>()
            .join(","),
        item.to_token_stream()
    )
    .parse()
    .unwrap())
}

pub fn try_commands(item: TokenStream) -> Result<TokenStream, ()> {
    // Parse the macro parameters
    let commands = Punctuated::<Expr, Token![,]>::parse_terminated.parse(item).map_err(|_| abort!(Span::call_site(), "Could not parse commands"))?;

    // Map all command name into a CommandHandler instance
    let commands = commands.iter().map(|c| c.to_token_stream().to_string().replace(' ', "") + "_command()").collect::<Vec<String>>().join(",");

    Ok(format!(r#"{GROUP_COMMANDS}(vec![{}])"#, commands).parse().unwrap())
}