tweld 1.0.0

Dynamic identifier generation for Rust macros. Tweld provides a flexible @[] syntax to "fuse" strings, case-conversions, and logic directly into your generated source code.
Documentation
mod modifiers;
#[cfg(test)]
mod tests;

use syn::parse::{Parse, ParseStream};
use syn::{Ident, LitChar, LitStr, Token, bracketed, parenthesized};

use crate::models::{RenderType, TokenParserState, WeldToken};
use crate::parser::modifiers::parse_modifiers;

pub(crate) const MAX_DEPTH: isize = 20;

#[derive(Debug)]
pub struct TweldDsl {
    pub render_type: RenderType,
    pub tokens: Vec<WeldToken>,
}

impl Parse for TweldDsl {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut dsl = TweldDsl {
            render_type: RenderType::Identifier,
            tokens: Vec::new(),
        };

        dsl = parse_stream(input, dsl, TokenParserState::Root, -1isize)?;

        Ok(dsl)
    }
}

fn parse_stream(
    input: &syn::parse::ParseBuffer<'_>,
    mut dsl: TweldDsl,
    state: TokenParserState,
    mut depth: isize,
) -> syn::Result<TweldDsl> {
    if depth >= MAX_DEPTH {
        return Err(syn::Error::new(input.span(), "Maximum nesting exceeded!"));
    }
    depth += 1;

    let mut words: Vec<String> = vec![];
    while !input.is_empty() {
        match state {
            TokenParserState::Root => {
                if input.peek(syn::token::Paren) {
                    parse_concat_group(input, &mut dsl, depth)?;

                    continue;
                }

                if input.peek(syn::token::Bracket) {
                    parse_list_group(input, &mut dsl, depth)?;

                    continue;
                }

                if input.peek(Token![|]) {
                    dsl = parse_stream(input, dsl, TokenParserState::Modifiers, depth)?;
                    continue;
                }

                if input.peek(syn::Ident) {
                    let result = input.parse::<Ident>()?.to_string();
                    dsl.tokens.push(WeldToken::Plain(result));
                    continue;
                }

                if input.peek(syn::LitStr) {
                    let result = input.parse::<LitStr>()?.value();
                    dsl.render_type = RenderType::StringLiteral;
                    dsl.tokens.push(WeldToken::Plain(result));
                    continue;
                }

                if input.peek(syn::LitChar) {
                    let result = input.parse::<LitChar>()?.value().to_string();
                    dsl.render_type = RenderType::StringLiteral;
                    dsl.tokens.push(WeldToken::Plain(result));
                    continue;
                }

                if input.peek(Token![-]) {
                    input.parse::<Token![-]>()?;
                    dsl.tokens.push(WeldToken::Plain("-".to_string()));
                    continue;
                }

                if input.peek(Token![_]) {
                    input.parse::<Token![_]>()?;
                    dsl.tokens.push(WeldToken::Plain("_".to_string()));
                    continue;
                }

                let _ = input.parse::<proc_macro2::TokenTree>()?;
            }
            TokenParserState::InsideGroup(_) => {
                while !input.is_empty() {
                    if input.peek(syn::token::Paren) {
                        parse_concat_group(input, &mut dsl, depth)?;

                        continue;
                    }

                    if input.peek(syn::token::Bracket) {
                        parse_list_group(input, &mut dsl, depth)?;

                        continue;
                    }

                    if input.peek(Token![|]) {
                        if !words.is_empty() {
                            let Some(last) = words.pop() else {
                                return Err(syn::Error::new(
                                    input.span(),
                                    "There was an error when trying to start modifiers...",
                                ));
                            };

                            if !words.is_empty() {
                                dsl.tokens.push(WeldToken::Plain(words.join("")));
                                words.clear();
                            }
                            dsl.tokens.push(WeldToken::Plain(last));
                        }

                        dsl = parse_stream(input, dsl, TokenParserState::Modifiers, depth)?;
                        continue;
                    }

                    if input.peek(syn::Ident) {
                        let value = input.parse::<Ident>()?.to_string();

                        words.push(value);
                        continue;
                    }

                    if input.peek(syn::LitChar) {
                        let value = input.parse::<LitChar>()?.value();

                        words.push(value.to_string());
                        dsl.render_type = RenderType::StringLiteral;
                        continue;
                    }

                    if input.peek(syn::LitStr) {
                        let value = input.parse::<LitStr>()?.value();

                        words.push(value);
                        dsl.render_type = RenderType::StringLiteral;
                        continue;
                    }

                    if input.peek(Token![-]) {
                        input.parse::<Token![-]>()?;
                        words.push("-".to_string());
                        continue;
                    }

                    if input.peek(Token![_]) {
                        input.parse::<Token![_]>()?;
                        words.push("_".to_string());
                        continue;
                    }

                    let _ = input.parse::<proc_macro2::TokenTree>()?;
                }
            }
            TokenParserState::Modifiers => {
                let Some(target) = dsl.tokens.pop() else {
                    return Err(syn::Error::new(input.span(), "Modifiers need a target"));
                };

                let modifiers = parse_modifiers(input)?;
                dsl.tokens
                    .push(WeldToken::Modify(Box::new(target), modifiers));
            }
        }

        if !words.is_empty() {
            parse_words(&mut dsl, &state, &mut words);
        }
    }
    if !words.is_empty() {
        parse_words(&mut dsl, &state, &mut words);
    }

    Ok(dsl)
}

fn parse_words(dsl: &mut TweldDsl, state: &TokenParserState, words: &mut Vec<String>) {
    match state {
        TokenParserState::InsideGroup(single_value) if !single_value => {
            words
                .iter()
                .for_each(|word| dsl.tokens.push(WeldToken::Plain(word.clone())));
        }
        _ => dsl.tokens.push(WeldToken::Plain(words.join(""))),
    }

    words.clear();
}

fn parse_concat_group(
    input: &syn::parse::ParseBuffer<'_>,
    dsl: &mut TweldDsl,
    depth: isize,
) -> Result<(), syn::Error> {
    let group;
    parenthesized!(group in input);
    let mut grouped_dsl = TweldDsl {
        render_type: dsl.render_type.clone(),
        tokens: vec![],
    };
    grouped_dsl = parse_stream(
        &group,
        grouped_dsl,
        TokenParserState::InsideGroup(true),
        depth,
    )?;

    dsl.tokens.push(WeldToken::ConcatGroup(grouped_dsl.tokens));

    if let RenderType::StringLiteral = grouped_dsl.render_type {
        dsl.render_type = RenderType::StringLiteral;
    }

    Ok(())
}

fn parse_list_group(
    input: &syn::parse::ParseBuffer<'_>,
    dsl: &mut TweldDsl,
    depth: isize,
) -> Result<(), syn::Error> {
    let group;
    bracketed!(group in input);
    let mut grouped_dsl = TweldDsl {
        render_type: dsl.render_type.clone(),
        tokens: vec![],
    };
    grouped_dsl = parse_stream(
        &group,
        grouped_dsl,
        TokenParserState::InsideGroup(false),
        depth,
    )?;

    dsl.tokens.push(WeldToken::ListGroup(grouped_dsl.tokens));

    if let RenderType::StringLiteral = grouped_dsl.render_type {
        dsl.render_type = RenderType::StringLiteral;
    }

    Ok(())
}