macrofied-toolbox 0.4.2

This library provides an ergonomic experience of adding debugging messages to rust's Result<T,E> and Option<T> patterns
Documentation
use proc_macro2::{Ident, Span};
use quote::ToTokens;
use syn::{Error, Expr, Lit};
use syn::parse::{Parse, ParseStream, Peek};
use syn::spanned::Spanned;
use syn::token::Paren;

use crate::common::{Message, OnFail, OnSuccess, WhenExpr};
#[cfg(any(feature = "result", feature = "option"))]
use crate::common::models::Capture;
use crate::common::models::OnExpr;
use crate::common::parse::utils::search_for_ident;

mod kw {
    custom_keyword![debug];
    custom_keyword![when];
}

const DEBUG_SECTION: &str = "debug";

impl Parse for Capture {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let reference = input.peek(Token![&]);

        if reference { <Token![&]>::parse(input)?; }

        let mutable = input.peek(Token!(mut));

        if mutable { <Token![mut]>::parse(input)?; }

        Ok(Self {
            identifier: <Ident>::parse(input)?.to_string(),
            mutable,
            reference,
        })
    }
}

pub fn decode_expr_type(expr: &Expr) -> &'static str {
    match expr {
        Expr::Array(_) => "array",
        Expr::Assign(_) => "assign",
        Expr::AssignOp(_) => "assign-op",
        Expr::Async(_) => "async",
        Expr::Await(_) => "await",
        Expr::Binary(_) => "binary",
        Expr::Block(_) => "block",
        Expr::Box(_) => "box",
        Expr::Break(_) => "break",
        Expr::Call(_) => "call",
        Expr::Cast(_) => "cast",
        Expr::Closure(_) => "closure",
        Expr::Continue(_) => "continue",
        Expr::Field(_) => "field",
        Expr::ForLoop(_) => "for-loop",
        Expr::Group(_) => "group",
        Expr::If(_) => "if",
        Expr::Index(_) => "index",
        Expr::Let(_) => "let",
        Expr::Lit(_) => "lit",
        Expr::Loop(_) => "loop",
        Expr::Macro(_) => "macro",
        Expr::Match(_) => "match",
        Expr::MethodCall(_) => "method call",
        Expr::Paren(_) => "paren",
        Expr::Path(_) => "path",
        Expr::Range(_) => "range",
        Expr::Reference(_) => "reference",
        Expr::Repeat(_) => "repeat",
        Expr::Return(_) => "return",
        Expr::Struct(_) => "struct",
        Expr::Try(_) => "try",
        Expr::TryBlock(_) => "try-block",
        Expr::Tuple(_) => "tuple",
        Expr::Type(_) => "type",
        Expr::Unary(_) => "unary",
        Expr::Unsafe(_) => "unsafe",
        Expr::Verbatim(_) => "verbatim",
        Expr::While(_) => "while",
        Expr::Yield(_) => "yield",
        Expr::__TestExhaustive(_) => unimplemented!()
    }
}

pub fn parse_debug(
    input: ParseStream, capture: &Option<Capture>,
) -> syn::Result<Option<Message>> {
    if input.peek(Token![@]) && input.peek2(kw::debug) {
        <Token![@]>::parse(input)?;
        <kw::debug>::parse(input)?;

        let message = parse_message(input, DEBUG_SECTION, capture)?;

        parse_optional_semicolon(input)?;

        Ok(Some(message))
    } else {
        Ok(None)
    }
}

pub fn parse_failed<T: Peek>(
    input: ParseStream, token: T, section: &str, capture: Option<Capture>,
) -> syn::Result<Option<OnFail>> {
    if input.peek(Token![@]) && input.peek2(token) {
        <Token![@]>::parse(input)?;
        <Ident>::parse(input)?;

        let message = parse_message(input, section, &capture).ok();

        let expr = if !input.is_empty() && !input.peek(Token![@]) {
            if message.is_some() {
                <Token![;]>::parse(input)?;
            }

            if input.is_empty() {
                None
            } else {
                let expr = parse_expression(input, section)?;
                let captured = capture.and_then(
                    |capture| if utils::search_for_ident(expr.to_token_stream(), &capture.identifier) {
                        Some(capture)
                    } else {
                        None
                    }

                );

                Some(OnExpr { captured, expr })
            }
        } else {
            parse_optional_semicolon(input)?;

            None
        };

        Ok(Some(OnFail { expr, message }))
    } else {
        Ok(None)
    }
}

pub fn parse_message(
    input: ParseStream, section: &str, capture: &Option<Capture>,
) -> syn::Result<Message> {
    if let Some(literal) = input.cursor().literal() {
        if literal.0.to_string().starts_with('\"') {
            let literal = <Lit>::parse(input)?;

            match literal {
                Lit::Str(_) => {}
                _ => return Err(Error::new(literal.span(), format!("{} expects a string literal", section)))
            }

            let mut exprs = Vec::new();
            let mut captured = &None;

            while input.peek(Token![,]) {
                <Token![,]>::parse(input)?;

                let expr = <Expr>::parse(input)?;

                if let Some(checked) = capture {
                    if search_for_ident(expr.to_token_stream(), &checked.identifier) {
                        captured = capture;
                    }
                }

                match expr {
                    Expr::Array(_) | Expr::Await(_) |
                    Expr::Binary(_) | Expr::Block(_) |
                    Expr::Call(_) | Expr::Cast(_) |
                    Expr::Field(_) | Expr::Group(_) |
                    Expr::If(_) | Expr::Index(_) |
                    Expr::Lit(_) | Expr::Macro(_) |
                    Expr::Match(_) | Expr::MethodCall(_) |
                    Expr::Paren(_) | Expr::Path(_) |
                    Expr::Range(_) | Expr::Reference(_) |
                    Expr::Repeat(_) | Expr::Try(_) |
                    Expr::TryBlock(_) | Expr::Tuple(_) |
                    Expr::Unary(_) | Expr::Unsafe(_) => {}
                    _ => return Err(Error::new(
                        expr.span(),
                        format!("{:?} is not a supported {} expression", decode_expr_type(&expr), section)
                    ))
                }

                exprs.push(expr);
            }

            return Ok(Message {
                args: if exprs.is_empty() { None } else { Some(exprs) },
                captured: captured.clone(),
                fmt: literal,
            });
        }
    }

    Err(Error::new(Span::call_site(), "No Message"))
}

pub fn parse_successful<T: Peek>(
    input: ParseStream, when: &WhenExpr, capture_id: &str, section: &str, token: T,
) -> syn::Result<Option<OnSuccess>> {
    let capture = Some(Capture::from(capture_id));

    Ok(if when.ok_when {
        let success = Ident::new(capture_id, Span::call_site());

        Some(OnSuccess::Expr(OnExpr {
            captured: capture,
            expr: parse_quote! { #success },
        }))
    } else {
        parse_expression_success(input, token, section, capture)?
    })
}

pub fn parse_when<T: Peek>(input: ParseStream, success_kw: T) -> syn::Result<WhenExpr> {
    if input.peek(Token![@]) {
        <Token![@]>::parse(input)?;
    }
    let ok_when: bool = if input.peek(kw::when) {
        <kw::when>::parse(input)?;

        false
    } else if input.peek(success_kw) {
        <Ident>::parse(input)?;

        true
    } else {
        return Err(Error::new(
            Span::call_site(), "expected @when or @ok expression",
        ));
    };

    let expr = <Expr>::parse(input)?;

    match expr {
        Expr::Await(_) | Expr::Call(_) |
        Expr::Cast(_) | Expr::Field(_) |
        Expr::Group(_) | Expr::If(_) |
        Expr::Index(_) | Expr::Macro(_) |
        Expr::Match(_) | Expr::MethodCall(_) |
        Expr::Path(_) | Expr::Reference(_) => {
            parse_optional_semicolon(input)?;

            Ok(WhenExpr { expr, tried: false, ok_when })
        }
        Expr::Try(try_expr) => {
            let tried = true;
            let expr = try_expr.expr.as_ref().clone();

            parse_optional_semicolon(input)?;

            Ok(WhenExpr { expr, tried, ok_when })
        }
        Expr::Block(_) if utils::block_contains_try(&expr) =>
            Err(Error::new(expr.span(), "block can not contain a try expression")),
        Expr::Block(_) =>
            Ok(WhenExpr { expr, tried: false, ok_when }),
        _ => Err(Error::new(
            expr.span(),
            format!("{:?} is not a supported when expression", decode_expr_type(&expr)),
        ))
    }
}

fn parse_expression_success<T: Peek>(
    input: ParseStream, token: T, section: &str, capture: Option<Capture>,
) -> syn::Result<Option<OnSuccess>> {
    if input.peek(Token![@]) && input.peek2(token) {
        <Token![@]>::parse(input)?;
        <Ident>::parse(input)?;

        let mut captured = capture;

        if input.peek(Paren) {
            let content;

            parenthesized!(content in input);

            captured = Some(content.parse::<Capture>()?);

            <Token![=>]>::parse(input)?;
        }

        Ok(Some(
            if let Ok(message) = parse_message(input, section, &captured) {
                parse_optional_semicolon(input)?;
                OnSuccess::Message(message)
            } else {
                let expr = parse_expression(input, section)?;

                OnSuccess::Expr(OnExpr { captured, expr })
            }
        ))
    } else {
        Ok(None)
    }
}

fn parse_expression(input: ParseStream, section: &str) -> syn::Result<Expr> {
    let expr = <Expr>::parse(input)?;

    match expr {
        Expr::Block(_) | Expr::TryBlock(_) | Expr::Unsafe(_) => {}
        Expr::Let(_) | Expr::Struct(_) =>
            return Err(Error::new(
                expr.span(),
                format!(
                    "{:?} is not a supported {} expression, try placing it into a code block",
                    decode_expr_type(&expr), section
                ),
            )),
        _ => parse_optional_semicolon(input)?
    }

    Ok(expr)
}

fn parse_optional_semicolon(input: ParseStream) -> syn::Result<()> {
    if let Some(punct) = input.cursor().punct() {
        if punct.0.as_char() == ';' {
            <Token![;]>::parse(input)?;
        }
    }

    Ok(())
}

pub mod utils {
    #[cfg(any(feature = "option", feature = "result"))]
    use proc_macro2::{TokenStream, TokenTree};
    use syn::{Expr, Stmt};

    pub(super) fn block_contains_try(expr: &Expr) -> bool {
        if let Expr::Block(block_expr) = expr {
            if (&block_expr.block.stmts)
                .iter()
                .any(
                    |stmt| {
                        match stmt {
                            Stmt::Expr(expr) |
                            Stmt::Semi(expr, _) =>
                                if let Expr::Try(_) = expr {
                                    return true;
                                },
                            _ => {}
                        }
                        false
                    }
                ) {
                return true;
            }
        }

        false
    }

    #[cfg(any(feature = "option", feature = "result"))]
    pub fn search_for_ident(stream: TokenStream, checked: &str) -> bool {
        for each in stream {
            match each {
                TokenTree::Group(group) => {
                    return search_for_ident(group.stream(), checked);
                }
                TokenTree::Ident(ident) if ident == checked => {
                    return true;
                }
                _ => {}
            }
        }

        false
    }
}