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}