cmdparse-derive 0.1.1

Derive macro implementation for cmdparse.
Documentation
use crate::context::CodegenContext;
use crate::fields::{FieldView, FieldsSet, StructType};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use std::collections::HashSet;

impl<'a> FieldView<'a> {
    fn var_ident(&self) -> syn::Ident {
        format_ident!("field_{}", self.field_index())
    }

    fn gen_var_instantiate(&self) -> TokenStream {
        match self {
            FieldView::Default { .. } => TokenStream::new(),
            _ => {
                let var_ident = self.var_ident();
                quote! { let mut #var_ident = None; }
            }
        }
    }

    fn gen_parse_required(&self, ctx: &CodegenContext) -> TokenStream {
        match self {
            FieldView::Required {
                position, parser, ..
            } => {
                let var_ident = self.var_ident();
                let parser_ident = parser.ident();
                let parse_ctx = ctx.parse_context_ident();

                quote! {
                    #position => match input.with_nested(|input| ::cmdparse::Parser::<#parse_ctx>::parse(&#parser_ident, input, ctx.clone())) {
                        Ok((result, remaining)) => {
                            input = remaining;
                            #var_ident = Some(result);
                            first_token = false;
                            required_index += 1;
                            continue
                        }
                        Err(error @ ::cmdparse::error::ParseFailure::Error(_)) => return Err(error),
                        Err(::cmdparse::error::ParseFailure::Unrecognized(unrecognized)) => unrecognized,
                    }
                }
            }
            _ => TokenStream::new(),
        }
    }

    fn gen_complete_required(&self, ctx: &CodegenContext) -> TokenStream {
        match self {
            FieldView::Required {
                parser, position, ..
            } => {
                let parse_ctx = ctx.parse_context_ident();
                let parser_ident = parser.ident();

                quote! {
                    #position => {
                        let result = input.complete_nested(|input| {
                            ::cmdparse::Parser::<#parse_ctx>::complete(&#parser_ident, input, ctx.clone())
                        });
                        match result.remaining {
                            Some(remaining) => input = remaining,
                            None => return result.add_suggestions(suggestions),
                        }
                        if result.value_consumed {
                            required_index += 1;
                            first_token = false;
                            suggestions.extend(result.suggestions);
                            continue;
                        } else if first_token && matches!(input.peek(), Some(Ok(token)) if token.is_text()) {
                            return result.add_suggestions(suggestions);
                        }
                        suggestions.extend(result.suggestions);
                    }
                }
            }
            _ => TokenStream::new(),
        }
    }

    fn gen_parse_optional(&self, ctx: &CodegenContext) -> TokenStream {
        match self {
            FieldView::Optional { parser, name, .. } => {
                let parser_ident = parser.ident();
                let var_ident = self.var_ident();
                let parse_ctx = ctx.parse_context_ident();
                quote! {
                    #name => {
                        let (result, remaining) = unexpected.remaining().with_nested(|input| {
                            ::cmdparse::Parser::<#parse_ctx>::parse(&#parser_ident, input, ctx.clone())
                        })?;
                        input = remaining;
                        #var_ident = Some(result);
                    }
                }
            }
            FieldView::Fixed { value, name, .. } => {
                let var_ident = self.var_ident();
                quote! {
                    #name => {
                        #var_ident = Some(#value);
                        input = *unexpected.remaining();
                    }
                }
            }
            _ => TokenStream::new(),
        }
    }

    fn gen_complete_optional(&self, ctx: &CodegenContext) -> TokenStream {
        match self {
            FieldView::Optional { parser, name, .. } => {
                let parser_ident = parser.ident();
                let parse_ctx = ctx.parse_context_ident();
                quote! {
                    #name => {
                        let result = remaining.complete_nested(|input| {
                            ::cmdparse::Parser::<#parse_ctx>::complete(&#parser_ident, input, ctx.clone())
                        });
                        match result.remaining {
                            Some(remaining) => input = remaining,
                            None => return result.add_suggestions(suggestions),
                        }
                        if result.value_consumed {
                            first_token = false;
                            suggestions.extend(result.suggestions);
                            continue
                        } else {
                            return ::cmdparse::CompletionResult::new_final(false)
                                .add_suggestions(result.suggestions)
                                .add_suggestions(suggestions);
                        }
                        suggestions.extend(result.suggestions);
                    }
                }
            }
            FieldView::Fixed { name, .. } => quote! { #name => {
                input = remaining;
                continue
            }},
            _ => TokenStream::new(),
        }
    }

    fn gen_unwrap(&self) -> TokenStream {
        match self {
            FieldView::Required { .. } => {
                let var_ident = self.var_ident();
                quote! { #var_ident.unwrap() }
            }
            FieldView::Optional { default: None, .. } | FieldView::Fixed { default: None, .. } => {
                let var_ident = self.var_ident();
                quote! { #var_ident.unwrap_or_default() }
            }
            FieldView::Optional {
                default: Some(default),
                ..
            }
            | FieldView::Fixed {
                default: Some(default),
                ..
            } => {
                let var_ident = self.var_ident();
                quote! { #var_ident.unwrap_or_else(|| #default) }
            }
            FieldView::Default { default: None, .. } => quote! { Default::default() },
            FieldView::Default {
                default: Some(default),
                ..
            } => quote! { #default },
        }
    }
}

pub(crate) fn gen_parse_struct(
    constructor: TokenStream,
    ctx: &CodegenContext,
    fields: &FieldsSet<'_>,
    variant: Option<&str>,
) -> TokenStream {
    let mut initialization = TokenStream::new();
    let mut required_parsing = TokenStream::new();
    let mut optional_parsing = TokenStream::new();
    let mut unwrap_fields = TokenStream::new();

    let mut required_count: usize = 0;
    let mut initialized_field_indices = HashSet::new();

    for field in fields.fields_views(variant) {
        required_parsing.extend(field.gen_parse_required(ctx));
        optional_parsing.extend(field.gen_parse_optional(ctx));

        let index = field.field_index();
        if !initialized_field_indices.contains(&index) {
            initialization.extend(field.gen_var_instantiate());

            let unwrap_value = field.gen_unwrap();
            let unwrap_field = match fields.get_ident(field.field_index()) {
                Some(ident) => quote! { #ident: #unwrap_value, },
                None => quote! { #unwrap_value, },
            };
            unwrap_fields.extend(unwrap_field);
            initialized_field_indices.insert(index);
        }

        if matches!(field, FieldView::Required { .. }) {
            required_count += 1;
        }
    }

    let result_struct = match fields.struct_type() {
        StructType::Named => quote! { #constructor{ #unwrap_fields } },
        StructType::Unnamed => quote! { #constructor( #unwrap_fields ) },
        StructType::Unit => quote! {#constructor},
    };

    quote! {
        #initialization
        let mut required_index = 0;
        let mut first_token = true;
        loop {
            let unexpected = match required_index {
                #required_parsing
                _ => match input.take() {
                    None | Some(Err(_)) => break,
                    Some(Ok((::cmdparse::tokens::Token::Text(_), _))) => break,
                    Some(Ok((token, remaining))) => ::cmdparse::error::UnrecognizedToken::new(token, remaining),
                }
            };
            match unexpected.token() {
                ::cmdparse::tokens::Token::Text(_) => return Err(unexpected.into()),
                ::cmdparse::tokens::Token::Attribute(attribute) => {
                    let attribute = attribute.parse_string();
                    match ::std::borrow::Borrow::<str>::borrow(&attribute) {
                        #optional_parsing
                        _ if required_index >= #required_count => break,
                        _ if first_token => return Err(unexpected.into()),
                        _ => return Err(unexpected.into_error().into()),
                    }
                }
            }
            first_token = false;
        }
        Ok((#result_struct, input))
    }
}

pub(crate) fn gen_complete_struct(
    ctx: &CodegenContext,
    fields: &FieldsSet<'_>,
    variant: Option<&str>,
) -> TokenStream {
    let mut required_complete = TokenStream::new();
    let mut optional_complete = TokenStream::new();
    let mut required_count: usize = 0;
    let mut attribute_names = Vec::new();

    for field in fields.fields_views(variant) {
        required_complete.extend(field.gen_complete_required(ctx));
        optional_complete.extend(field.gen_complete_optional(ctx));
        if matches!(field, FieldView::Required { .. }) {
            required_count += 1;
        }

        match field {
            FieldView::Optional { name, .. } | FieldView::Fixed { name, .. } => {
                attribute_names.push(name);
            }
            _ => {}
        }
    }
    attribute_names.sort_unstable();

    quote! {
        const ATTRIBUTE_NAMES: &[&str] = &[#(#attribute_names),*];
        let mut required_index = 0;
        let mut first_token = true;
        let mut suggestions: ::std::collections::BTreeSet<::std::borrow::Cow<'static, str>> = ::std::collections::BTreeSet::new();

        loop {
            match required_index {
                #required_complete
                _ => (),
            }

            let mut result = match input.take() {
                None if required_index >= #required_count => ::cmdparse::CompletionResult::new(input, true),
                None | Some(Err(_)) => ::cmdparse::CompletionResult::new_final(false),
                Some(Ok((token, remaining))) => {
                    match token {
                        ::cmdparse::tokens::Token::Text(_) if required_index >= #required_count => {
                            ::cmdparse::CompletionResult::new(input, true)
                        }
                        ::cmdparse::tokens::Token::Text(_) => ::cmdparse::CompletionResult::new_final(false),

                        ::cmdparse::tokens::Token::Attribute(attribute) if remaining.is_all_consumed() => {
                            let text = attribute.parse_string();
                            suggestions.extend(
                                ::cmdparse::tokens::complete_variants(&text, ATTRIBUTE_NAMES)
                                    .map(::std::borrow::Cow::Borrowed)
                            );

                            let consumed = !first_token || #required_count == 0;
                            if first_token || required_index >= #required_count {
                                ::cmdparse::CompletionResult::new(input, consumed)
                            } else {
                                ::cmdparse::CompletionResult::new_final(consumed)
                            }
                        }
                        ::cmdparse::tokens::Token::Attribute(attribute) => {
                            let text = attribute.parse_string();
                            match ::std::borrow::Borrow::<str>::borrow(&text) {
                                #optional_complete
                                _ if required_index >= #required_count => ::cmdparse::CompletionResult::new(input, true),
                                _ if first_token => ::cmdparse::CompletionResult::new(input, false),
                                _ => ::cmdparse::CompletionResult::new_final(false),
                            }
                        }
                    }
                }
            };
            result.suggestions = suggestions;
            return result;
        }
    }
}