tokenstream2-tmpl 0.1.2

Runtime TokenStream interpolation for Rust macros
Documentation
//! [![github]](https://github.com/chesedo/tokenstream2-tmpl) [![crates-io]](https://crates.io/crates/tokenstream2-tmpl) [![docs-rs]](https://docs.rs/tokenstream2-tmpl) [![workflow]](https://github.com/chesedo/tokenstream2-tmpl/actions?query=workflow%3ARust)
//!
//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K
//! [workflow]: https://img.shields.io/github/workflow/status/chesedo/tokenstream2-tmpl/Rust?color=green&label=&labelColor=555555&logo=github%20actions&logoColor=white&style=for-the-badge
//!
//! This crate is meant to be a complement to [quote]. Where as [quote] does quasi-quote interpolations at
//! compile-time, this crate does them at run-time. This is handy for macros receiving templates from client code with
//! markers to be replaced when the macro is run.
//!
//! [quote]: https://github.com/dtolnay/quote
//!
//! # Examples
//! ```
//! use proc_macro2::TokenStream;
//! use tokenstream2_tmpl::interpolate;
//! use quote::ToTokens;
//! use std::collections::HashMap;
//! use syn::{Ident, parse_str};
//!
//! let input: TokenStream = parse_str("let NAME: int = 5;")?;
//! let expected: TokenStream = parse_str("let age: int = 5;")?;
//!
//! let mut replacements: HashMap<&str, &dyn ToTokens> = HashMap::new();
//! let ident = parse_str::<Ident>("age")?;
//! replacements.insert("NAME", &ident);
//!
//! let output = interpolate(input, &replacements);
//! assert_eq!(
//!     format!("{}", output),
//!     format!("{}", expected)
//! );
//!
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! Here `input` might be some input to a macro that functions as a template. [quote] would have tried to expand `NAME`
//! at the macro's compile-time. [tokenstream2-tmpl] will expand it at the macro's run-time.
//!
//! [tokenstream2-tmpl]: https://gitlab.com/chesedo/tokenstream2-tmpl
//!
//! ```
//! extern crate proc_macro;
//! use proc_macro2::TokenStream;
//! use std::collections::HashMap;
//! use syn::{Ident, parse::{Parse, ParseStream, Result}, parse_macro_input, punctuated::Punctuated, Token};
//! use tokenstream2_tmpl::{Interpolate, interpolate};
//! use quote::ToTokens;
//!
//! /// Create a token for macro using [syn](syn)
//! /// Type that holds a key and the value it maps to.
//! /// An acceptable stream will have the following form:
//! /// ```text
//! /// key => value
//! /// ```
//! struct KeyValue {
//!     pub key: Ident,
//!     pub arrow_token: Token![=>],
//!     pub value: Ident,
//! }
//!
//! /// Make KeyValue parsable from a token stream
//! impl Parse for KeyValue {
//!     fn parse(input: ParseStream) -> Result<Self> {
//!         Ok(KeyValue {
//!             key: input.parse()?,
//!             arrow_token: input.parse()?,
//!             value: input.parse()?,
//!         })
//!     }
//! }
//!
//! /// Make KeyValue interpolatible
//! impl Interpolate for KeyValue {
//!     fn interpolate(&self, stream: TokenStream) -> TokenStream {
//!         let mut replacements: HashMap<_, &dyn ToTokens> = HashMap::new();
//!
//!         // Replace each "KEY" with the key
//!         replacements.insert("KEY", &self.key);
//!
//!         // Replace each "VALUE" with the value
//!         replacements.insert("VALUE", &self.value);
//!
//!         interpolate(stream, &replacements)
//!     }
//! }
//!
//! /// Macro to take a list of key-values with a template to expand each key-value
//! # const IGNORE: &str = stringify! {
//! #[proc_macro_attribute]
//! # };
//! pub fn map(tokens: proc_macro::TokenStream, template: proc_macro::TokenStream) -> proc_macro::TokenStream {
//!     // Parse a comma separated list of key-values
//!     let maps =
//!         parse_macro_input!(tokens with Punctuated::<KeyValue, Token![,]>::parse_terminated);
//!
//!     maps.interpolate(template.into()).into()
//! }
//!
//! pub fn main() {
//! # const IGNORE: &str = stringify! {
//!     #[map(
//!         usize => 10,
//!         isize => -2,
//!         bool => false,
//!     )]
//!     let _: KEY = VALUE;
//! # };
//!     // Output:
//!     // let _: usize = 10;
//!     // let _: isize = -2;
//!     // let _: bool = false;
//! }
//! ```

use proc_macro2::{Group, TokenStream, TokenTree};
use quote::{ToTokens, TokenStreamExt};
use std::collections::HashMap;
use syn::punctuated::Punctuated;

/// Trait for tokens that can replace interpolation markers
pub trait Interpolate {
    /// Take a token stream and replace interpolation markers with their actual values into a new stream
    /// using [interpolate](interpolate)
    fn interpolate(&self, stream: TokenStream) -> TokenStream;
}

/// Make a Punctuated list interpolatible if it holds interpolatible types
impl<T: Interpolate, P> Interpolate for Punctuated<T, P> {
    fn interpolate(&self, stream: TokenStream) -> TokenStream {
        self.iter()
            .fold(TokenStream::new(), |mut implementations, t| {
                implementations.extend(t.interpolate(stream.clone()));
                implementations
            })
    }
}

/// Replace the interpolation markers in a token stream with a specific text.
/// See this [crate's](crate) documentation for an example on how to use this.
pub fn interpolate(
    stream: TokenStream,
    replacements: &HashMap<&str, &dyn ToTokens>,
) -> TokenStream {
    let mut new = TokenStream::new();

    // Loop over each token in the stream
    // `Literal`, `Punct`, and `Group` are kept as is
    for token in stream.into_iter() {
        match token {
            TokenTree::Literal(literal) => new.append(literal),
            TokenTree::Punct(punct) => new.append(punct),
            TokenTree::Group(group) => {
                // Recursively interpolate the stream in group
                let mut new_group =
                    Group::new(group.delimiter(), interpolate(group.stream(), replacements));
                new_group.set_span(group.span());

                new.append(new_group);
            }
            TokenTree::Ident(ident) => {
                let ident_str: &str = &ident.to_string();

                // Check if identifier is in the replacement set
                if let Some(value) = replacements.get(ident_str) {
                    // Replace with replacement value
                    value.to_tokens(&mut new);

                    continue;
                }

                // Identifier did not match, so copy as is
                new.append(ident);
            }
        }
    }

    new
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;
    use quote::quote;
    use syn::{parse_str, Ident, Token, Type};

    type Result = std::result::Result<(), Box<dyn std::error::Error>>;

    #[test]
    fn complete_replacements() -> Result {
        let input = quote! {
            let VAR: TRAIT = if true {
                CONCRETE{}
            } else {
                Alternative{}
            }
        };

        let expected = quote! {
            let var: abstract_type = if true {
                concrete{}
            } else {
                Alternative{}
            }
        };

        let mut r: HashMap<&str, &dyn ToTokens> = HashMap::new();
        let v: Ident = parse_str("var")?;
        let a: Type = parse_str("abstract_type")?;
        let c: Type = parse_str("concrete")?;

        r.insert("VAR", &v);
        r.insert("TRAIT", &a);
        r.insert("CONCRETE", &c);

        assert_eq!(
            format!("{}", &interpolate(input, &r)),
            format!("{}", expected)
        );

        Ok(())
    }

    /// Partial replacements should preverse the uninterpolated identifiers
    #[test]
    fn partial_replacements() -> Result {
        let input: TokenStream = parse_str("let a: TRAIT = OTHER;")?;
        let expected: TokenStream = parse_str("let a: Display = OTHER;")?;

        let mut r: HashMap<&str, &dyn ToTokens> = HashMap::new();
        let t: Type = parse_str("Display")?;
        r.insert("TRAIT", &t);

        assert_eq!(
            format!("{}", interpolate(input, &r)),
            format!("{}", expected)
        );

        Ok(())
    }

    /// Test the interpolation of Punctuated items
    #[test]
    fn interpolate_on_punctuated() -> Result {
        #[allow(dead_code)]
        pub struct TraitSpecifier {
            pub abstract_trait: Type,
            pub arrow_token: Token![=>],
            pub concrete: Type,
        }

        /// Make TraitSpecifier interpolatible
        impl Interpolate for TraitSpecifier {
            fn interpolate(&self, stream: TokenStream) -> TokenStream {
                let mut replacements: HashMap<_, &dyn ToTokens> = HashMap::new();

                // Replace each "TRAIT" with the absract trait
                replacements.insert("TRAIT", &self.abstract_trait);

                // Replace each "CONCRETE" with the concrete type
                replacements.insert("CONCRETE", &self.concrete);

                interpolate(stream, &replacements)
            }
        }
        let mut traits: Punctuated<TraitSpecifier, Token![,]> = Punctuated::new();

        traits.push(TraitSpecifier {
            abstract_trait: parse_str("IButton")?,
            arrow_token: Default::default(),
            concrete: parse_str("BigButton")?,
        });
        traits.push(TraitSpecifier {
            abstract_trait: parse_str("IWindow")?,
            arrow_token: Default::default(),
            concrete: parse_str("MinimalWindow")?,
        });

        let input = quote! {
            let _: TRAIT = CONCRETE{};
        };
        let expected = quote! {
            let _: IButton = BigButton{};
            let _: IWindow = MinimalWindow{};
        };

        assert_eq!(
            format!("{}", traits.interpolate(input)),
            format!("{}", expected)
        );

        Ok(())
    }
}