tokenstream2_tmpl/
lib.rs

1//! [![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)
2//!
3//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
4//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
5//! [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
6//! [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
7//!
8//! This crate is meant to be a complement to [quote]. Where as [quote] does quasi-quote interpolations at
9//! compile-time, this crate does them at run-time. This is handy for macros receiving templates from client code with
10//! markers to be replaced when the macro is run.
11//!
12//! [quote]: https://github.com/dtolnay/quote
13//!
14//! # Examples
15//! ```
16//! use proc_macro2::TokenStream;
17//! use tokenstream2_tmpl::interpolate;
18//! use quote::ToTokens;
19//! use std::collections::HashMap;
20//! use syn::{Ident, parse_str};
21//!
22//! let input: TokenStream = parse_str("let NAME: int = 5;")?;
23//! let expected: TokenStream = parse_str("let age: int = 5;")?;
24//!
25//! let mut replacements: HashMap<&str, &dyn ToTokens> = HashMap::new();
26//! let ident = parse_str::<Ident>("age")?;
27//! replacements.insert("NAME", &ident);
28//!
29//! let output = interpolate(input, &replacements);
30//! assert_eq!(
31//!     format!("{}", output),
32//!     format!("{}", expected)
33//! );
34//!
35//! # Ok::<(), Box<dyn std::error::Error>>(())
36//! ```
37//!
38//! Here `input` might be some input to a macro that functions as a template. [quote] would have tried to expand `NAME`
39//! at the macro's compile-time. [tokenstream2-tmpl] will expand it at the macro's run-time.
40//!
41//! [tokenstream2-tmpl]: https://gitlab.com/chesedo/tokenstream2-tmpl
42//!
43//! ```
44//! extern crate proc_macro;
45//! use proc_macro2::TokenStream;
46//! use std::collections::HashMap;
47//! use syn::{Ident, parse::{Parse, ParseStream, Result}, parse_macro_input, punctuated::Punctuated, Token};
48//! use tokenstream2_tmpl::{Interpolate, interpolate};
49//! use quote::ToTokens;
50//!
51//! /// Create a token for macro using [syn](syn)
52//! /// Type that holds a key and the value it maps to.
53//! /// An acceptable stream will have the following form:
54//! /// ```text
55//! /// key => value
56//! /// ```
57//! struct KeyValue {
58//!     pub key: Ident,
59//!     pub arrow_token: Token![=>],
60//!     pub value: Ident,
61//! }
62//!
63//! /// Make KeyValue parsable from a token stream
64//! impl Parse for KeyValue {
65//!     fn parse(input: ParseStream) -> Result<Self> {
66//!         Ok(KeyValue {
67//!             key: input.parse()?,
68//!             arrow_token: input.parse()?,
69//!             value: input.parse()?,
70//!         })
71//!     }
72//! }
73//!
74//! /// Make KeyValue interpolatible
75//! impl Interpolate for KeyValue {
76//!     fn interpolate(&self, stream: TokenStream) -> TokenStream {
77//!         let mut replacements: HashMap<_, &dyn ToTokens> = HashMap::new();
78//!
79//!         // Replace each "KEY" with the key
80//!         replacements.insert("KEY", &self.key);
81//!
82//!         // Replace each "VALUE" with the value
83//!         replacements.insert("VALUE", &self.value);
84//!
85//!         interpolate(stream, &replacements)
86//!     }
87//! }
88//!
89//! /// Macro to take a list of key-values with a template to expand each key-value
90//! # const IGNORE: &str = stringify! {
91//! #[proc_macro_attribute]
92//! # };
93//! pub fn map(tokens: proc_macro::TokenStream, template: proc_macro::TokenStream) -> proc_macro::TokenStream {
94//!     // Parse a comma separated list of key-values
95//!     let maps =
96//!         parse_macro_input!(tokens with Punctuated::<KeyValue, Token![,]>::parse_terminated);
97//!
98//!     maps.interpolate(template.into()).into()
99//! }
100//!
101//! pub fn main() {
102//! # const IGNORE: &str = stringify! {
103//!     #[map(
104//!         usize => 10,
105//!         isize => -2,
106//!         bool => false,
107//!     )]
108//!     let _: KEY = VALUE;
109//! # };
110//!     // Output:
111//!     // let _: usize = 10;
112//!     // let _: isize = -2;
113//!     // let _: bool = false;
114//! }
115//! ```
116
117use proc_macro2::{Group, TokenStream, TokenTree};
118use quote::{ToTokens, TokenStreamExt};
119use std::collections::HashMap;
120use syn::punctuated::Punctuated;
121
122/// Trait for tokens that can replace interpolation markers
123pub trait Interpolate {
124    /// Take a token stream and replace interpolation markers with their actual values into a new stream
125    /// using [interpolate](interpolate)
126    fn interpolate(&self, stream: TokenStream) -> TokenStream;
127}
128
129/// Make a Punctuated list interpolatible if it holds interpolatible types
130impl<T: Interpolate, P> Interpolate for Punctuated<T, P> {
131    fn interpolate(&self, stream: TokenStream) -> TokenStream {
132        self.iter()
133            .fold(TokenStream::new(), |mut implementations, t| {
134                implementations.extend(t.interpolate(stream.clone()));
135                implementations
136            })
137    }
138}
139
140/// Replace the interpolation markers in a token stream with a specific text.
141/// See this [crate's](crate) documentation for an example on how to use this.
142pub fn interpolate(
143    stream: TokenStream,
144    replacements: &HashMap<&str, &dyn ToTokens>,
145) -> TokenStream {
146    let mut new = TokenStream::new();
147
148    // Loop over each token in the stream
149    // `Literal`, `Punct`, and `Group` are kept as is
150    for token in stream.into_iter() {
151        match token {
152            TokenTree::Literal(literal) => new.append(literal),
153            TokenTree::Punct(punct) => new.append(punct),
154            TokenTree::Group(group) => {
155                // Recursively interpolate the stream in group
156                let mut new_group =
157                    Group::new(group.delimiter(), interpolate(group.stream(), replacements));
158                new_group.set_span(group.span());
159
160                new.append(new_group);
161            }
162            TokenTree::Ident(ident) => {
163                let ident_str: &str = &ident.to_string();
164
165                // Check if identifier is in the replacement set
166                if let Some(value) = replacements.get(ident_str) {
167                    // Replace with replacement value
168                    value.to_tokens(&mut new);
169
170                    continue;
171                }
172
173                // Identifier did not match, so copy as is
174                new.append(ident);
175            }
176        }
177    }
178
179    new
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use pretty_assertions::assert_eq;
186    use quote::quote;
187    use syn::{parse_str, Ident, Token, Type};
188
189    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
190
191    #[test]
192    fn complete_replacements() -> Result {
193        let input = quote! {
194            let VAR: TRAIT = if true {
195                CONCRETE{}
196            } else {
197                Alternative{}
198            }
199        };
200
201        let expected = quote! {
202            let var: abstract_type = if true {
203                concrete{}
204            } else {
205                Alternative{}
206            }
207        };
208
209        let mut r: HashMap<&str, &dyn ToTokens> = HashMap::new();
210        let v: Ident = parse_str("var")?;
211        let a: Type = parse_str("abstract_type")?;
212        let c: Type = parse_str("concrete")?;
213
214        r.insert("VAR", &v);
215        r.insert("TRAIT", &a);
216        r.insert("CONCRETE", &c);
217
218        assert_eq!(
219            format!("{}", &interpolate(input, &r)),
220            format!("{}", expected)
221        );
222
223        Ok(())
224    }
225
226    /// Partial replacements should preverse the uninterpolated identifiers
227    #[test]
228    fn partial_replacements() -> Result {
229        let input: TokenStream = parse_str("let a: TRAIT = OTHER;")?;
230        let expected: TokenStream = parse_str("let a: Display = OTHER;")?;
231
232        let mut r: HashMap<&str, &dyn ToTokens> = HashMap::new();
233        let t: Type = parse_str("Display")?;
234        r.insert("TRAIT", &t);
235
236        assert_eq!(
237            format!("{}", interpolate(input, &r)),
238            format!("{}", expected)
239        );
240
241        Ok(())
242    }
243
244    /// Test the interpolation of Punctuated items
245    #[test]
246    fn interpolate_on_punctuated() -> Result {
247        #[allow(dead_code)]
248        pub struct TraitSpecifier {
249            pub abstract_trait: Type,
250            pub arrow_token: Token![=>],
251            pub concrete: Type,
252        }
253
254        /// Make TraitSpecifier interpolatible
255        impl Interpolate for TraitSpecifier {
256            fn interpolate(&self, stream: TokenStream) -> TokenStream {
257                let mut replacements: HashMap<_, &dyn ToTokens> = HashMap::new();
258
259                // Replace each "TRAIT" with the absract trait
260                replacements.insert("TRAIT", &self.abstract_trait);
261
262                // Replace each "CONCRETE" with the concrete type
263                replacements.insert("CONCRETE", &self.concrete);
264
265                interpolate(stream, &replacements)
266            }
267        }
268        let mut traits: Punctuated<TraitSpecifier, Token![,]> = Punctuated::new();
269
270        traits.push(TraitSpecifier {
271            abstract_trait: parse_str("IButton")?,
272            arrow_token: Default::default(),
273            concrete: parse_str("BigButton")?,
274        });
275        traits.push(TraitSpecifier {
276            abstract_trait: parse_str("IWindow")?,
277            arrow_token: Default::default(),
278            concrete: parse_str("MinimalWindow")?,
279        });
280
281        let input = quote! {
282            let _: TRAIT = CONCRETE{};
283        };
284        let expected = quote! {
285            let _: IButton = BigButton{};
286            let _: IWindow = MinimalWindow{};
287        };
288
289        assert_eq!(
290            format!("{}", traits.interpolate(input)),
291            format!("{}", expected)
292        );
293
294        Ok(())
295    }
296}