Skip to main content

token_goblin_runtime/
ux.rs

1//! Better UX for proc-macro.
2//! Inspired by `crabtime`.
3//!
4//! Allows to receiving inputs and producing outputs in non `TokenStream` way.
5//!
6//! E.g. instead of:
7//! ```
8//! # use proc_macro2::TokenStream;
9//! # use syn::parse::Parser;
10//!
11//!
12//! fn foo(input: TokenStream) -> TokenStream {
13//!    let parser = syn::punctuated::Punctuated::<syn::LitStr, syn::Token![,]>::parse_terminated;
14//!    let lit_components = parser.parse2(input).unwrap();
15//!    let components = lit_components.iter().map(|c| c.value()).collect::<Vec<_>>();
16//!    // Handling of `components`
17//!    # todo!()
18//! }
19//! ```
20//!
21//! One could write:
22//! ```
23//! # use proc_macro2::TokenStream;
24//! # use syn::parse::Parser;
25//! # use token_goblin_runtime::prelude::*;
26//!
27//! fn foo(components: CommaSeparated<Token>) -> TokenStream {
28//!    // Handling of `components`
29//!    # todo!()
30//! }
31//! ```
32//!
33//! Since extending `syn::parse::Parse` with std types is not possible due to orphan rule.
34//! We use macro `parse_into!`, that hardcodes checks for specific types.
35//!
36//! Note: having `String` and `Vec<String>` in input params remove span information, and reduce IDE/diagnostics quality.
37//!
38//! Output is a little bit more simple, it expected in three forms:
39//! - `String` - For strings that should be converted to `TokenStream` without input span information
40//! - `TokenStream` - as basic case.
41//! - and in empty form - for cases where output is already emitted as `output_str!`, `output!` macros.
42//!
43//! So we have a trait `IntoTokenStream` that is solely focused on converting specific types into `TokenStream`.
44//!
45//! The user can extend it as well, to support custom types in output.
46
47use core::fmt::{self, Display};
48use std::{cell::RefCell, fmt::Debug, str::FromStr};
49
50use proc_macro2::TokenStream;
51use syn::parse::{Parse, ParseStream, Parser};
52/// Represents a comma separated list of parsable values.
53///
54/// Can be used to provide a typed interface for input params of `token-goblin` `charms`.
55///
56/// Example:
57/// ```no_build
58/// #[token_goblin::munch]
59/// fn foo(input: CommaSeparated<syn::LitStr>) -> TokenStream {
60///     output_str!("{}", input.0.iter().map(|s| s.value()).collect::<Vec<_>>().join(", "));
61/// }
62///
63/// foo!("foo", "bar", "baz");
64/// // -> "foo, bar, baz"
65/// ```
66///
67pub struct CommaSeparated<T>(pub Vec<T>);
68
69impl From<CommaSeparated<Token>> for Vec<String> {
70    fn from(value: CommaSeparated<Token>) -> Self {
71        value.0.into_iter().map(|t| t.to_string()).collect()
72    }
73}
74
75/// Represents either `Ident` or `LitStr` token.
76///
77/// Used when macro need a simple interface for input, and user can decide a way to provide string.
78///
79/// Example:
80/// ```no_build
81/// #[token_goblin::munch]
82/// fn foo(input: Token) -> TokenStream {
83///     output_str!("{}", input.to_string());
84/// }
85///
86/// foo!("foo");
87/// // -> foo
88///
89pub enum Token {
90    Ident(syn::Ident),
91    Literal(syn::LitStr),
92}
93impl Display for Token {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            Token::Ident(ident) => write!(f, "{ident}"),
97            Token::Literal(literal) => write!(f, "{}", literal.value()),
98        }
99    }
100}
101
102impl Debug for Token {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            Token::Ident(ident) => write!(f, "Ident({ident:?})"),
106            Token::Literal(literal) => write!(f, "Literal({:?})", literal.value()),
107        }
108    }
109}
110impl PartialEq<&str> for Token {
111    fn eq(&self, other: &&str) -> bool {
112        match self {
113            Token::Ident(ident) => ident == *other,
114            // creates an owned string (but we don't have an api to compare directly)
115            Token::Literal(literal) => literal.value() == *other,
116        }
117    }
118}
119
120#[doc(hidden)] // auto trait for FromTokenStream
121pub trait TokenStreamInto<T> {
122    fn convert_token_stream(self) -> syn::Result<T>;
123}
124impl<T: syn::parse::Parse> TokenStreamInto<T> for TokenStream {
125    fn convert_token_stream(self) -> syn::Result<T> {
126        T::parse.parse2(self)
127    }
128}
129
130/// Convert specific type into `TokenStream`.
131///
132/// In `token-goblin` it is used to convert output types of `token-goblin` `charms` into `TokenStream`.
133/// We provide default implementations for:
134/// - `String`, `TokenStream`, `()` - so them can be used as output for `charm` fn
135///   out of the box.
136///
137/// For `#[munch] mod {..}` user can provide custom implementation, to support custom types in output.
138pub trait IntoTokenStream {
139    fn into_token_stream(self) -> TokenStream;
140}
141
142impl IntoTokenStream for String {
143    fn into_token_stream(self) -> TokenStream {
144        TokenStream::from_str(&self).unwrap_or_else(|e| {
145            compile_error(&format!("Failed to convert String to TokenStream: {e}"))
146        })
147    }
148}
149impl IntoTokenStream for TokenStream {
150    fn into_token_stream(self) -> TokenStream {
151        self
152    }
153}
154impl IntoTokenStream for () {
155    fn into_token_stream(self) -> TokenStream {
156        TokenStream::new()
157    }
158}
159
160fn compile_error(text: &str) -> TokenStream {
161    quote::quote! {
162        ::core::compile_error!(#text)
163    }
164}
165
166/// Emit formatted string as token stream
167///
168/// Example:
169/// ```
170/// # use token_goblin_runtime::prelude::*;
171/// output_str!("foo + 2");
172/// ```
173///
174/// This will spit `foo + 2` token stream (ident, punct, literal) as output of the macro, just before emitting result.
175/// The format of input is the same as in `format!` macro.
176///
177/// Note: If input is invalid `TokenStream` this will emit compile error.
178#[macro_export]
179macro_rules! output_str {
180    ($($tokens:tt)*) => {
181        $crate::ux::push_output(format!($($tokens)*));
182    };
183}
184
185/// Emit quote as token stream
186///
187/// Example:
188/// ```
189/// # use token_goblin_runtime::prelude::*;
190/// output! {
191///     foo + bar
192/// };
193/// ```
194///
195/// This will spit quoted `TokenStream` as output of the macro, just before emitting result.
196/// The format of input is the same as in `quote!` macro.
197///
198/// Note: that this is different from `output_str!` macro:
199/// ```
200/// # use token_goblin_runtime::prelude::*;
201/// output_str!("foo + 2");
202/// output! {
203///     "foo + 2"
204/// };
205/// ```
206///
207/// The first will emit `foo + 2` token stream (ident, punct, literal) as output of the macro.
208/// But the second one will emit `"foo + 2"` as string literal.
209///
210#[macro_export]
211macro_rules! output {
212    ($($tokens:tt)*) => {
213        $crate::ux::push_output($crate::prelude::quote!($($tokens)*));
214    };
215}
216
217thread_local! {
218    static COLLECTED_OUTPUT: RefCell<TokenStream> = RefCell::new(TokenStream::new());
219}
220
221/// For some usages, user might want to emit output streamingly, like `println!` or `write!` macros.
222///
223/// This function is internall implementation of this feature, it's recommended to use:
224/// `output!`, or `output_str!` macros instead.
225pub fn push_output(output: impl IntoTokenStream) {
226    COLLECTED_OUTPUT.with(|collected_output| {
227        collected_output
228            .borrow_mut()
229            .extend(output.into_token_stream());
230    });
231}
232
233#[doc(hidden)]
234#[must_use]
235pub(crate) fn flush_output(last_part: TokenStream) -> TokenStream {
236    COLLECTED_OUTPUT.with(|collected_output| {
237        let mut collected_output = std::mem::take(&mut *collected_output.borrow_mut());
238        collected_output.extend(last_part);
239        collected_output
240    })
241}
242
243impl Parse for Token {
244    fn parse(input: ParseStream) -> syn::Result<Self> {
245        if input.peek(syn::Ident) {
246            Ok(Token::Ident(input.parse()?))
247        } else if input.peek(syn::LitStr) {
248            Ok(Token::Literal(input.parse()?))
249        } else {
250            Err(syn::Error::new(input.span(), "Expected ident or literal"))
251        }
252    }
253}
254
255impl<T: Parse> Parse for CommaSeparated<T> {
256    fn parse(input: ParseStream) -> syn::Result<Self> {
257        let parser = syn::punctuated::Punctuated::<T, syn::Token![,]>::parse_terminated;
258        let components = parser(input)?;
259        Ok(CommaSeparated(components.into_iter().collect()))
260    }
261}
262#[cfg(test)]
263mod tests {
264    use std::str::FromStr;
265
266    use super::*;
267
268    #[test]
269    fn test_parse_string() {
270        let tokens = TokenStream::from_str(" \"123\" ").unwrap();
271        let into: Token = tokens.convert_token_stream().unwrap();
272        assert_eq!(into.to_string(), "123");
273    }
274    #[test]
275    fn test_parse_vec() {
276        let tokens = TokenStream::from_str(" \"1\", \"2\", \"3\" ").unwrap();
277        let into: CommaSeparated<Token> = tokens.convert_token_stream().unwrap();
278        assert_eq!(into.0, vec!["1", "2", "3"]);
279    }
280
281    #[test]
282    fn test_parse_tts() {
283        let tokens = TokenStream::from_str("123").unwrap();
284        let into: TokenStream = tokens.clone().convert_token_stream().unwrap();
285        assert_eq!(into.to_string(), tokens.to_string());
286    }
287
288    #[test]
289    fn test_parse_syn_type() {
290        let tokens = TokenStream::from_str("asd").unwrap();
291        let into: syn::Ident = tokens.convert_token_stream().unwrap();
292        assert_eq!(into.to_string(), "asd");
293    }
294
295    #[test]
296    fn test_streaming_output() {
297        output_str!("foo");
298        output_str!("bar");
299        output! {
300            "baz" // quote will emit tokens so this becumes string literal
301        };
302        let output = flush_output(TokenStream::from_str("qux").unwrap());
303        assert_eq!(output.to_string(), "foo bar \"baz\" qux");
304    }
305}