requestty-macro 0.5.0

The `questions` macro for `requestty`
Documentation
use std::fmt;

use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse::Parse, spanned::Spanned};

use crate::helpers::*;

bitflags::bitflags! {
    pub struct BuilderMethods: u16 {
        const DEFAULT        = 0b000_0000_0001;
        const TRANSFORM      = 0b000_0000_0010;
        const VAL_FIL        = 0b000_0000_0100;
        const VAL_KEY        = 0b000_0000_1000;
        const AUTO_COMPLETE  = 0b000_0001_0000;
        const LOOP_PAGE_SIZE = 0b000_0010_0000;
        const CHOICES        = 0b000_0100_0000;
        const MASK           = 0b000_1000_0000;
        const EDITOR         = 0b001_0000_0000;
        const ON_ESC         = 0b010_0000_0000;
        const PROMPT         = 0b100_0000_0000;
    }
}

#[derive(Clone, Copy)]
pub(crate) enum QuestionKind {
    Input,
    Int,
    Float,
    Confirm,
    Select,
    RawSelect,
    Expand,
    MultiSelect,
    OrderSelect,
    Password,
    Editor,
    Custom,
}

impl QuestionKind {
    fn as_str(&self) -> &str {
        match self {
            QuestionKind::Input => "input",
            QuestionKind::Int => "int",
            QuestionKind::Float => "float",
            QuestionKind::Confirm => "confirm",
            QuestionKind::Select => "select",
            QuestionKind::RawSelect => "raw_select",
            QuestionKind::Expand => "expand",
            QuestionKind::MultiSelect => "multi_select",
            QuestionKind::OrderSelect => "order_select",
            QuestionKind::Password => "password",
            QuestionKind::Editor => "editor",
            QuestionKind::Custom => "custom",
        }
    }

    fn get_builder_methods(&self) -> BuilderMethods {
        match *self {
            QuestionKind::Input => {
                BuilderMethods::DEFAULT
                    | BuilderMethods::TRANSFORM
                    | BuilderMethods::VAL_FIL
                    | BuilderMethods::VAL_KEY
                    | BuilderMethods::AUTO_COMPLETE
                    | BuilderMethods::LOOP_PAGE_SIZE
                    | BuilderMethods::ON_ESC
            }
            QuestionKind::Int | QuestionKind::Float => {
                BuilderMethods::DEFAULT
                    | BuilderMethods::TRANSFORM
                    | BuilderMethods::VAL_FIL
                    | BuilderMethods::VAL_KEY
                    | BuilderMethods::ON_ESC
            }
            QuestionKind::Confirm => {
                BuilderMethods::DEFAULT | BuilderMethods::TRANSFORM | BuilderMethods::ON_ESC
            }
            QuestionKind::Select | QuestionKind::RawSelect | QuestionKind::Expand => {
                BuilderMethods::DEFAULT
                    | BuilderMethods::TRANSFORM
                    | BuilderMethods::LOOP_PAGE_SIZE
                    | BuilderMethods::CHOICES
                    | BuilderMethods::ON_ESC
            }
            QuestionKind::MultiSelect => {
                BuilderMethods::TRANSFORM
                    | BuilderMethods::VAL_FIL
                    | BuilderMethods::LOOP_PAGE_SIZE
                    | BuilderMethods::CHOICES
                    | BuilderMethods::ON_ESC
            }
            QuestionKind::OrderSelect => {
                BuilderMethods::TRANSFORM
                    | BuilderMethods::VAL_FIL
                    | BuilderMethods::LOOP_PAGE_SIZE
                    | BuilderMethods::CHOICES
                    | BuilderMethods::ON_ESC
            }
            QuestionKind::Password => {
                BuilderMethods::TRANSFORM
                    | BuilderMethods::VAL_FIL
                    | BuilderMethods::VAL_KEY
                    | BuilderMethods::MASK
                    | BuilderMethods::ON_ESC
            }
            QuestionKind::Editor => {
                BuilderMethods::DEFAULT
                    | BuilderMethods::TRANSFORM
                    | BuilderMethods::VAL_FIL
                    | BuilderMethods::EDITOR
                    | BuilderMethods::ON_ESC
            }
            QuestionKind::Custom => BuilderMethods::PROMPT,
        }
    }
}

impl Parse for QuestionKind {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let ident = input.parse::<syn::Ident>()?;

        let kind = if ident == "Input" {
            QuestionKind::Input
        } else if ident == "Int" {
            QuestionKind::Int
        } else if ident == "Float" {
            QuestionKind::Float
        } else if ident == "Confirm" {
            QuestionKind::Confirm
        } else if ident == "Select" {
            QuestionKind::Select
        } else if ident == "RawSelect" {
            QuestionKind::RawSelect
        } else if ident == "Expand" {
            QuestionKind::Expand
        } else if ident == "MultiSelect" {
            QuestionKind::MultiSelect
        } else if ident == "OrderSelect" {
            QuestionKind::OrderSelect
        } else if ident == "Password" {
            QuestionKind::Password
        } else if ident == "Editor" {
            QuestionKind::Editor
        } else if ident == "Custom" {
            QuestionKind::Custom
        } else {
            return Err(syn::Error::new(
                ident.span(),
                format!("unknown question kind {}", ident),
            ));
        };

        Ok(kind)
    }
}

impl fmt::Display for QuestionKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Default)]
pub(crate) struct QuestionOpts {
    pub(crate) message: Option<syn::Expr>,
    pub(crate) when: Option<syn::Expr>,
    pub(crate) ask_if_answered: Option<syn::Expr>,
    pub(crate) on_esc: Option<syn::Expr>,

    pub(crate) default: Option<syn::Expr>,

    pub(crate) validate: Option<syn::Expr>,
    pub(crate) validate_on_key: Option<syn::Expr>,
    pub(crate) filter: Option<syn::Expr>,
    pub(crate) transform: Option<syn::Expr>,
    pub(crate) auto_complete: Option<syn::Expr>,

    pub(crate) choices: Option<Choices>,
    pub(crate) page_size: Option<syn::Expr>,
    pub(crate) should_loop: Option<syn::Expr>,

    pub(crate) mask: Option<syn::Expr>,
    pub(crate) editor: Option<syn::Expr>,
    pub(crate) extension: Option<syn::Expr>,

    pub(crate) prompt: Option<syn::Expr>,
}

fn check_allowed(ident: &syn::Ident, kind: QuestionKind) -> syn::Result<()> {
    // default options which are always there
    if ident == "name" || ident == "message" || ident == "when" || ident == "ask_if_answered" {
        return Ok(());
    }

    let builder_method = if ident == "default" {
        BuilderMethods::DEFAULT
    } else if ident == "transform" {
        BuilderMethods::TRANSFORM
    } else if ident == "validate" || ident == "filter" {
        BuilderMethods::VAL_FIL
    } else if ident == "validate_on_key" {
        BuilderMethods::VAL_KEY
    } else if ident == "auto_complete" {
        BuilderMethods::AUTO_COMPLETE
    } else if ident == "choices" {
        BuilderMethods::CHOICES
    } else if ident == "page_size" || ident == "should_loop" {
        BuilderMethods::LOOP_PAGE_SIZE
    } else if ident == "mask" {
        BuilderMethods::MASK
    } else if ident == "editor" || ident == "extension" {
        BuilderMethods::EDITOR
    } else if ident == "on_esc" {
        BuilderMethods::ON_ESC
    } else if ident == "prompt" {
        BuilderMethods::PROMPT
    } else {
        return Err(syn::Error::new(
            ident.span(),
            format!("unknown question option `{}`", ident),
        ));
    };

    if kind.get_builder_methods().contains(builder_method) {
        Ok(())
    } else {
        Err(syn::Error::new(
            ident.span(),
            format!("option `{}` does not exist for kind `{}`", ident, kind),
        ))
    }
}

pub(crate) struct Question {
    pub(crate) kind: QuestionKind,
    pub(crate) name: syn::Expr,
    pub(crate) opts: QuestionOpts,
}

impl Parse for Question {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let kind = QuestionKind::parse(input)?;
        let content;
        let brace = syn::braced!(content in input);

        let mut opts = QuestionOpts::default();
        let mut name = None;

        while !content.is_empty() {
            let ident = content.parse::<syn::Ident>()?;

            check_allowed(&ident, kind)?;

            if ident == "name" {
                insert_non_dup(ident, &mut name, &content)?;
            } else if ident == "message" {
                insert_non_dup(ident, &mut opts.message, &content)?;
            } else if ident == "when" {
                insert_non_dup(ident, &mut opts.when, &content)?;
            } else if ident == "ask_if_answered" {
                insert_non_dup(ident, &mut opts.ask_if_answered, &content)?;
            } else if ident == "default" {
                insert_non_dup(ident, &mut opts.default, &content)?;
            } else if ident == "validate" {
                insert_non_dup(ident, &mut opts.validate, &content)?;
            } else if ident == "validate_on_key" {
                insert_non_dup(ident, &mut opts.validate_on_key, &content)?;
            } else if ident == "filter" {
                insert_non_dup(ident, &mut opts.filter, &content)?;
            } else if ident == "transform" {
                insert_non_dup(ident, &mut opts.transform, &content)?;
            } else if ident == "auto_complete" {
                insert_non_dup(ident, &mut opts.auto_complete, &content)?;
            } else if ident == "choices" {
                let parser = match kind {
                    QuestionKind::MultiSelect => Choices::parse_multi_select_choice,
                    QuestionKind::OrderSelect => Choices::parse_order_select_choice,
                    _ => Choices::parse_choice,
                };

                insert_non_dup_parse(ident, &mut opts.choices, &content, parser)?;
            } else if ident == "page_size" {
                insert_non_dup(ident, &mut opts.page_size, &content)?;
            } else if ident == "should_loop" {
                insert_non_dup(ident, &mut opts.should_loop, &content)?;
            } else if ident == "mask" {
                insert_non_dup(ident, &mut opts.mask, &content)?;
            } else if ident == "editor" {
                insert_non_dup(ident, &mut opts.editor, &content)?;
            } else if ident == "extension" {
                insert_non_dup(ident, &mut opts.extension, &content)?;
            } else if ident == "on_esc" {
                insert_non_dup(ident, &mut opts.on_esc, &content)?;
            } else if ident == "prompt" {
                insert_non_dup(ident, &mut opts.prompt, &content)?;
            } else {
                unreachable!("check_allowed should have taken care of this case.");
            }

            if parse_optional_comma(&content)?.is_none() {
                break;
            }
        }

        if let QuestionKind::Custom = kind {
            if opts.prompt.is_none() {
                return Err(syn::Error::new(
                    brace.span,
                    "missing required option `prompt`",
                ));
            }
        }

        Ok(Self {
            kind,
            name: name
                .ok_or_else(|| syn::Error::new(brace.span, "missing required option `name`"))?,
            opts,
        })
    }
}

impl Question {
    fn write_main_opts(&self, tokens: &mut TokenStream) {
        if let Some(ref message) = self.opts.message {
            tokens.extend(quote_spanned! { message.span() => .message(#message) });
        }
        if let Some(ref when) = self.opts.when {
            tokens.extend(quote_spanned! { when.span() => .when(#when) });
        }
        if let Some(ref ask_if_answered) = self.opts.ask_if_answered {
            tokens.extend(quote_spanned! {
                ask_if_answered.span() => .ask_if_answered(#ask_if_answered)
            });
        }
    }
}

impl quote::ToTokens for Question {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let name = &self.name;

        if let QuestionKind::Custom = self.kind {
            let prompt = self
                .opts
                .prompt
                .as_ref()
                .expect("Parsing would error if no prompt was there");
            // If just the name was passed into Question::custom, type errors associated
            // with its conversion to a string would take the span _including_ that of
            // prompt. Explicitly performing `String::from`, makes the error span due to
            // the `From` trait will show the span of the name only
            let name = quote_spanned! {
                name.span() => String::from(#name)
            };
            tokens.extend(quote_spanned! {
                prompt.span() => ::requestty::Question::custom(#name, #prompt)
            });
            self.write_main_opts(tokens);
            tokens.extend(quote! { .build() });
            return;
        }

        let kind = syn::Ident::new(self.kind.as_str(), name.span());

        tokens.extend(quote_spanned! {
            name.span() => ::requestty::Question::#kind(#name)
        });

        self.write_main_opts(tokens);
        if let Some(ref default) = self.opts.default {
            tokens.extend(quote_spanned! { default.span() => .default(#default) });
        }
        if let Some(ref validate) = self.opts.validate {
            tokens.extend(quote_spanned! { validate.span() => .validate(#validate) });
        }
        if let Some(ref validate_on_key) = self.opts.validate_on_key {
            tokens.extend(
                quote_spanned! { validate_on_key.span() => .validate_on_key(#validate_on_key) },
            );
        }
        if let Some(ref filter) = self.opts.filter {
            tokens.extend(quote_spanned! { filter.span() => .filter(#filter) });
        }
        if let Some(ref transform) = self.opts.transform {
            tokens.extend(quote_spanned! { transform.span() => .transform(#transform) });
        }
        if let Some(ref auto_complete) = self.opts.auto_complete {
            tokens
                .extend(quote_spanned! { auto_complete.span() => .auto_complete(#auto_complete) });
        }
        if let Some(ref choices) = self.opts.choices {
            tokens.extend(match self.kind {
                QuestionKind::MultiSelect => {
                    quote_spanned! { choices.span() => .choices_with_default(#choices) }
                }
                _ => quote_spanned! { choices.span() => .choices(#choices) },
            });
        }
        if let Some(ref page_size) = self.opts.page_size {
            tokens.extend(quote_spanned! { page_size.span() => .page_size(#page_size) });
        }
        if let Some(ref should_loop) = self.opts.should_loop {
            tokens.extend(quote_spanned! { should_loop.span() => .should_loop(#should_loop) });
        }
        if let Some(ref mask) = self.opts.mask {
            tokens.extend(quote_spanned! { mask.span() => .mask(#mask) });
        }
        if let Some(ref editor) = self.opts.editor {
            tokens.extend(quote_spanned! { editor.span() => .editor(#editor) });
        }
        if let Some(ref extension) = self.opts.extension {
            tokens.extend(quote_spanned! { extension.span() => .extension(#extension) });
        }
        if let Some(ref on_esc) = self.opts.on_esc {
            tokens.extend(quote_spanned! { on_esc.span() => .on_esc(#on_esc) });
        }
        tokens.extend(quote! { .build() });
    }
}