embedded-cli-macros 0.2.1

Macros for embedded-cli lib
Documentation
use convert_case::{Case, Casing};
use darling::Result;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};

use super::{
    args::ArgType,
    model::{Command, CommandArgType},
    TargetType,
};

pub fn derive_from_raw(target: &TargetType, commands: &[Command]) -> Result<TokenStream> {
    let ident = target.ident();

    let parsing = create_parsing(ident, commands)?;

    let named_lifetime = target.named_lifetime();

    let output = quote! {

        impl<'a> _cli::service::FromRaw<'a> for #ident #named_lifetime {
            fn parse(command: _cli::command::RawCommand<'a>) -> Result<Self, _cli::service::ParseError<'a>> {
                #parsing
                Ok(command)
            }
        }
    };

    Ok(output)
}

fn create_parsing(ident: &Ident, commands: &[Command]) -> Result<TokenStream> {
    let match_arms: Vec<_> = commands.iter().map(|c| command_parsing(ident, c)).collect();

    Ok(quote! {
        let command = match command.name() {
            #(#match_arms)*
            cmd => return Err(_cli::service::ParseError::UnknownCommand),
        };
    })
}

fn command_parsing(ident: &Ident, command: &Command) -> TokenStream {
    let name = &command.name;
    let variant_name = &command.ident;
    let variant_fqn = quote! { #ident::#variant_name };

    let rhs = if command.args.is_empty() && command.subcommand.is_none() {
        quote! { #variant_fqn, }
    } else {
        let (parsing, arguments) = create_arg_parsing(command);
        if command.named_args {
            quote! {
                {
                    #parsing
                    #variant_fqn { #(#arguments)* }
                }
            }
        } else {
            quote! {
                {
                    #parsing
                    #variant_fqn ( #(#arguments)* )
                }
            }
        }
    };

    quote! {  #name => #rhs }
}

fn create_arg_parsing(command: &Command) -> (TokenStream, Vec<TokenStream>) {
    let mut variables = vec![];
    let mut arguments = vec![];
    let mut positional_value_arms = vec![];
    let mut extra_states = vec![];
    let mut option_name_arms = vec![];
    let mut option_value_arms = vec![];

    let mut positional = 0usize;
    for arg in &command.args {
        let fi_raw = format_ident!("{}", arg.field_name);
        let fi = format_ident!("arg_{}", arg.field_name);
        let ty = &arg.field_type;

        let arg_default;

        match &arg.arg_type {
            CommandArgType::Flag { long, short } => {
                arg_default = Some(quote! { false });

                option_name_arms.push(create_option_name_arm(
                    short,
                    long,
                    quote! {
                        {
                            #fi = Some(true);
                            state = States::Normal;
                        }
                    },
                ));
            }
            CommandArgType::Option { long, short } => {
                arg_default = arg.default_value.clone();
                let state = format_ident!(
                    "Expect{}",
                    arg.field_name.from_case(Case::Snake).to_case(Case::Pascal)
                );
                extra_states.push(quote! { #state, });

                let parse_value = create_parse_arg_value(ty);
                option_value_arms.push(quote! {
                    _cli::arguments::Arg::Value(val) if state == States::#state => {
                        #fi = Some(#parse_value);
                        state = States::Normal;
                    }
                });

                option_name_arms.push(create_option_name_arm(
                    short,
                    long,
                    quote! { state = States::#state },
                ));
            }
            CommandArgType::Positional => {
                arg_default = arg.default_value.clone();
                let parse_value = create_parse_arg_value(ty);

                positional_value_arms.push(quote! {
                    #positional => {
                        #fi = Some(#parse_value);
                    },
                });
                positional += 1;
            }
        }

        let constructor_arg = match arg.ty {
            ArgType::Option => quote! { #fi_raw: #fi },
            ArgType::Normal => {
                if let Some(default) = arg_default {
                    quote! {
                        #fi_raw: #fi.unwrap_or(#default)
                    }
                } else {
                    let name = arg.full_name();
                    quote! {
                        #fi_raw: #fi.ok_or(_cli::service::ParseError::MissingRequiredArgument {
                            name: #name,
                        })?
                    }
                }
            }
        };

        variables.push(quote! {
            let mut #fi = None;
        });
        arguments.push(quote! {
            #constructor_arg,
        });
    }

    let subcommand_value_arm;
    if let Some(subcommand) = &command.subcommand {
        let fi_raw;
        let fi;
        if let Some(field_name) = &subcommand.field_name {
            let ident_raw = format_ident!("{}", field_name);
            fi_raw = quote! { #ident_raw: };
            fi = format_ident!("sub_{}", field_name);
        } else {
            fi_raw = quote! {};
            fi = format_ident!("sub_command");
        }
        let ty = &subcommand.field_type;

        subcommand_value_arm = Some(quote! {
            let args = args.into_args();
            let raw = _cli::command::RawCommand::new(name, args);

            #fi = Some(<#ty as _cli::service::FromRaw>::parse(raw)?);

            break;
        });

        let constructor_arg = match subcommand.ty {
            ArgType::Option => quote! { #fi_raw #fi },
            ArgType::Normal => {
                let name = subcommand.full_name();
                quote! {
                    #fi_raw #fi.ok_or(_cli::service::ParseError::MissingRequiredArgument {
                        name: #name,
                    })?
                }
            }
        };

        variables.push(quote! {
            let mut #fi = None;
        });
        arguments.push(quote! {
            #constructor_arg,
        });
    } else {
        subcommand_value_arm = None;
    }

    let value_arm = if let Some(subcommand_arm) = subcommand_value_arm {
        quote! {
            _cli::arguments::Arg::Value(name) if state == States::Normal => {
                #subcommand_arm
            }
        }
    } else if positional_value_arms.is_empty() {
        quote! {
            _cli::arguments::Arg::Value(value) if state == States::Normal =>
            return Err(_cli::service::ParseError::UnexpectedArgument {
                value,
            })
        }
    } else {
        quote! {
            _cli::arguments::Arg::Value(val) if state == States::Normal => {
                match positional {
                    #(#positional_value_arms)*
                    _ => return Err(_cli::service::ParseError::UnexpectedArgument{
                        value: val
                    })
                }
                positional += 1;
            }
        }
    };

    let parsing = quote! {
        #(#variables)*

        #[derive(Eq, PartialEq)]
        enum States {
            Normal,
            #(#extra_states)*
        }
        let mut state = States::Normal;
        let mut positional = 0;

        let mut args = command.args().args();
        while let Some(arg) = args.next() {
            let arg = arg.map_err(|err| match err {
                _cli::arguments::ArgError::NonAsciiShortOption => _cli::service::ParseError::NonAsciiShortOption
            })?;
            match arg {
                #(#option_name_arms)*
                #(#option_value_arms)*
                #value_arm,
                _cli::arguments::Arg::Value(_) => unreachable!(),
                _cli::arguments::Arg::LongOption(option) => {
                    return Err(_cli::service::ParseError::UnexpectedLongOption { name: option })
                }
                _cli::arguments::Arg::ShortOption(option) => {
                    return Err(_cli::service::ParseError::UnexpectedShortOption { name: option })
                }
                _cli::arguments::Arg::DoubleDash => {}
            }
        }
    };

    (parsing, arguments)
}

pub fn create_option_name_arm(
    short: &Option<char>,
    long: &Option<String>,
    action: TokenStream,
) -> TokenStream {
    match (short, long) {
        (Some(short), Some(long)) => {
            quote! {
                _cli::arguments::Arg::LongOption(#long)
                | _cli::arguments::Arg::ShortOption(#short) => #action,
            }
        }
        (Some(short), None) => {
            quote! {
                _cli::arguments::Arg::ShortOption(#short) => #action,
            }
        }
        (None, Some(long)) => {
            quote! {
                _cli::arguments::Arg::LongOption(#long) => #action,
            }
        }
        (None, None) => unreachable!(),
    }
}

fn create_parse_arg_value(ty: &TokenStream) -> TokenStream {
    quote! {
        <#ty as _cli::arguments::FromArgument>::from_arg(val)?,
    }
}