ext_format 0.1.1

A small, yet powerful, Rust crate for string interpolation
Documentation
use crate::parse::QuoteToken;
use proc_macro2::Ident;
use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::{HashMap, HashSet};

fn get_macro_definitions() -> TokenStream {
    quote!(
        #[allow(unused_macros)]
        macro_rules! nested_tuple {
                (@inner [] $($r:tt)*) => {$($r),*};
                (@inner [$i:tt|$($r:tt|)*] $($rev:tt)*) => (
                    nested_tuple!(@inner [$($r|)*] ($($rev)*, $i))
                );
                ($i:tt) => ($i);
                ($i:tt, $($r:tt),* $(,)?) => (
                    nested_tuple!(@inner [$($r|)*] $i)
                )
            }

        #[allow(unused_macros)]
        macro_rules! fizip {
                (@inner [] $($a:tt)*) => ($($a)*);
                (@inner [$i:expr => $($r:expr => )*] $($a:tt)*) => (
                    fizip!(@inner [$($r => )*] $($a)*.zip($i))
                );
                ($i:expr) => ($i);
                ($i:expr, $($r:expr),* $(,)?) => (
                    fizip!(@inner [$($r => )*] $i)
                )
            }
    )
    .into()
}

pub(crate) fn generate_code(tokens: Vec<QuoteToken>) -> TokenStream {
    let mut rust_tokens: Vec<TokenStream> = vec![];
    for token in generate_inner_code(tokens, HashMap::new()) {
        rust_tokens.push(token.into());
    }
    let inner_stream: TokenStream = TokenStream::from_iter(rust_tokens).into();

    let macro_tokens = get_macro_definitions();

    quote!({
        #macro_tokens

        let mut res = String::new();
        #inner_stream
        res
    })
    .into()
}

fn generate_inner_code(
    tokens: Vec<QuoteToken>,
    mut mapping: HashMap<String, String>,
) -> TokenStream {
    let mut rust_tokens: Vec<TokenStream> = vec![];
    for token in tokens {
        let new_tokens = match token {
            QuoteToken::Literal(literal) => generate_literal_code(literal),
            QuoteToken::Variable(ident, inner_ident) => {
                generate_variable_code(ident, inner_ident, &mut mapping)
            }
            QuoteToken::HiddenVariable(ident, inner_ident) => {
                generate_hidden_variable_code(ident, inner_ident, &mut mapping)
            }
            QuoteToken::Group(tokens, separator) => generate_group_code(tokens, separator),
        };
        rust_tokens.push(new_tokens);
    }
    TokenStream::from_iter(rust_tokens)
}

fn generate_literal_code(literal: String) -> TokenStream {
    let new_tokens = quote!(res.push_str(#literal););
    new_tokens.into()
}

fn generate_variable_code(
    ident: String,
    inner_ident: Option<String>,
    mapping: &mut HashMap<String, String>,
) -> TokenStream {
    let new_name = mapping.get(&ident).unwrap_or(&ident);
    let var_ident = Ident::new(new_name, Span::call_site());
    let new_tokens = if let Some(inner_ident) = inner_ident {
        let inner_var_ident = Ident::new(&inner_ident, Span::call_site());
        quote!(
            let #inner_var_ident = #var_ident;
            res.push_str(&#inner_var_ident.to_string());
        )
    } else {
        quote!(
            res.push_str(&#var_ident.to_string());
        )
    };
    new_tokens.into()
}

fn generate_hidden_variable_code(
    ident: String,
    inner_ident: Option<String>,
    mapping: &mut HashMap<String, String>,
) -> TokenStream {
    let new_name = mapping.get(&ident).unwrap_or(&ident);
    let var_ident = Ident::new(new_name, Span::call_site());
    if let Some(inner_ident) = inner_ident {
        if inner_ident != "_" {
            let inner_var_ident = Ident::new(&inner_ident, Span::call_site());
            return quote!(
                let #inner_var_ident = #var_ident;
            );
        }
    }
    TokenStream::new()
}

fn get_variable_names(tokens: &[QuoteToken]) -> Vec<(String, String)> {
    let mut variables = vec![];
    let mut inner_variables = HashSet::new();
    for token in tokens.iter() {
        let (variable, inner) = match token {
            QuoteToken::Variable(ref variable, ref inner) => (variable, inner),
            QuoteToken::HiddenVariable(ref variable, ref inner) => (variable, inner),
            _ => continue,
        };
        if !inner_variables.contains(variable) {
            if let Some(inner) = inner {
                inner_variables.insert(inner);
                variables.push((variable.clone(), inner.clone()))
            } else {
                let inner_name = "__ext_format_inner_".to_string() + variable;
                variables.push((variable.clone(), inner_name))
            }
        }
    }
    variables
}

fn generate_group_code(tokens: Vec<QuoteToken>, separator: Option<String>) -> TokenStream {
    let variables = get_variable_names(&tokens);

    let mut mapping = HashMap::new();
    let mut idents = vec![];
    let mut inner_idents = vec![];

    for (variable, inner) in variables.iter() {
        mapping.insert(variable.clone(), inner.clone());
        idents.push(Ident::new(&variable, Span::call_site()));
        inner_idents.push(Ident::new(&inner, Span::call_site()));
    }

    let token_stream: TokenStream = generate_inner_code(tokens, mapping).into();

    let separator_stream = if let Some(separator) = separator {
        quote!(
            if i < iterator.len() - 1 {
                res.push_str(#separator);
            }
        )
    } else {
        TokenStream::new().into()
    };

    quote!(
        let mut iterator = fizip!(#(#idents.iter()),*).collect::<Vec<_>>();
        if !iterator.is_empty() {
            for (i, nested_tuple!(#(#inner_idents),*)) in iterator.iter().enumerate() {
                #token_stream
                #separator_stream
            }
        };
    )
    .into()
}

#[cfg(test)]
mod tests {
    use super::QuoteToken::*;
    use super::*;
    use crate::util::unindent;

    #[test]
    fn test_generate_inner_code_literal() {
        let tokens = vec![Literal("Hello".to_string())];
        let output = generate_inner_code(tokens, HashMap::new());
        let output_str = output.to_string();

        assert_eq!(output_str, r#"res . push_str ("Hello") ;"#);
    }

    #[test]
    fn test_generate_inner_code_variable() {
        let mut mapping = HashMap::new();
        mapping.insert("var".to_string(), "var_mapped".to_string());

        let tokens = vec![Variable("var".to_string(), None)];
        let output = generate_inner_code(tokens, mapping);
        let output_str = output.to_string();

        assert_eq!(
            output_str,
            r#"res . push_str (& var_mapped . to_string ()) ;"#
        );
    }

    #[test]
    fn test_generate_inner_code_hidden_variable() {
        let tokens = vec![HiddenVariable("var".to_string(), None)];
        let output = generate_inner_code(tokens, HashMap::new());
        let output_str = output.to_string();

        assert_eq!(output_str, "");
    }

    #[test]
    fn test_generate_inner_code_group_with_separator() {
        let mut mapping = HashMap::new();
        mapping.insert("var".to_string(), "var_mapped".to_string());

        let group_tokens = vec![
            Literal("Literal".to_string()),
            Variable("var".to_string(), None),
        ];

        let tokens = vec![Group(group_tokens, Some(",".to_string()))];

        let output = generate_inner_code(tokens, mapping);
        let output_str = output.to_string();

        let expected = unindent(
            r#"
            let mut iterator = fizip ! (var . iter ()) . collect :: < Vec < _ >> () ;
            @ if ! iterator . is_empty () { 
            @for (i , nested_tuple ! (__ext_format_inner_var)) in iterator . iter () . enumerate () {
            @ res . push_str ("Literal") ;
            @ res . push_str (& __ext_format_inner_var . to_string ()) ;
            @ if i < iterator . len () - 1 { res . push_str (",") ; } } } ;
        "#,
        ).trim().replace("\n@", "");

        assert_eq!(output_str, expected);
    }

    #[test]
    fn test_generate_inner_code_group_without_separator() {
        let mut mapping = HashMap::new();
        mapping.insert("var".to_string(), "var_mapped".to_string());

        let group_tokens = vec![
            Literal("Literal".to_string()),
            Variable("var".to_string(), None),
        ];

        let tokens = vec![Group(group_tokens, None)];

        let output = generate_inner_code(tokens, mapping);
        let output_str = output.to_string();

        let expected = unindent(
            r#"
            let mut iterator = fizip ! (var . iter ()) . collect :: < Vec < _ >> () ;
            @ if ! iterator . is_empty () { 
            @for (i , nested_tuple ! (__ext_format_inner_var)) in iterator . iter () . enumerate () {
            @ res . push_str ("Literal") ;
            @ res . push_str (& __ext_format_inner_var . to_string ()) ; } } ;
        "#,
        ).trim().replace("\n@", "");

        assert_eq!(output_str, expected);
    }

    #[test]
    fn test_generate_code_group_with_hidden_variable() {
        let mut mapping = HashMap::new();
        mapping.insert("var1".to_string(), "mapped_var1".to_string());
        mapping.insert("hidden_var".to_string(), "_".to_string());

        let group = Group(
            vec![
                Literal("A".to_string()),
                Variable("var1".to_string(), Some("mapped_var1".to_string())),
                HiddenVariable("hidden_var".to_string(), Some("_".to_string())),
            ],
            Some(", ".to_string()),
        );

        let output = generate_inner_code(vec![group], mapping);
        let output_str = output.to_string();

        let expected = unindent(
            r#"
            let mut iterator = fizip ! (var1 . iter () , hidden_var . iter ()) . collect :: < Vec < _ >> () ;
            @ if ! iterator . is_empty () { for (i , nested_tuple ! (mapped_var1 , _)) in iterator . iter () . enumerate () { res . push_str ("A") ;
            @ let mapped_var1 = mapped_var1 ;
            @ res . push_str (& mapped_var1 . to_string ()) ;
            @ if i < iterator . len () - 1 { res . push_str (", ") ; } } } ;"#,
        ).trim().replace("\n@", "");

        assert_eq!(output_str, expected);
    }
}