Skip to main content

zyn_core/ast/
pipe_node.rs

1use proc_macro2::Ident;
2use proc_macro2::Span;
3use proc_macro2::TokenStream;
4use proc_macro2::TokenTree;
5
6use quote::ToTokens;
7use quote::quote;
8
9use syn::Token;
10use syn::parse::Parse;
11use syn::parse::ParseStream;
12
13use crate::Expand;
14use crate::pascal;
15
16/// A single pipe stage in a `{{ expr | pipe }}` interpolation.
17///
18/// At expand time, the name is matched against the built-in pipe list. Unrecognised
19/// names are assumed to be custom pipe structs and are PascalCase-converted.
20///
21/// ```text
22/// {{ name | snake }}          → PipeNode { name: "snake", args: [] }
23/// {{ name | ident:"get_{}" }} → PipeNode { name: "ident", args: ["get_{}"] }
24/// ```
25pub struct PipeNode {
26    /// Source span of the pipe name.
27    pub span: Span,
28    /// The pipe name as written, e.g. `snake`, `ident`, `my_custom_pipe`.
29    pub name: syn::Ident,
30    /// Colon-separated arguments following the name.
31    pub args: Vec<TokenStream>,
32}
33
34impl PipeNode {
35    pub fn span(&self) -> Span {
36        self.span
37    }
38}
39
40impl Parse for PipeNode {
41    fn parse(input: ParseStream) -> syn::Result<Self> {
42        let name: syn::Ident = input.parse()?;
43        let span = name.span();
44
45        let mut args = Vec::new();
46
47        while input.peek(Token![:]) {
48            input.parse::<Token![:]>()?;
49
50            let mut arg = TokenStream::new();
51
52            while !input.is_empty() && !input.peek(Token![:]) && !input.peek(Token![|]) {
53                let tt: TokenTree = input.parse()?;
54                tt.to_tokens(&mut arg);
55            }
56
57            args.push(arg);
58        }
59
60        Ok(Self { span, name, args })
61    }
62}
63
64const BUILTIN_PIPES: &[&str] = &[
65    "upper",
66    "lower",
67    "snake",
68    "camel",
69    "pascal",
70    "kebab",
71    "screaming",
72    "ident",
73    "fmt",
74    "str",
75    "trim",
76    "plural",
77    "singular",
78];
79
80impl Expand for PipeNode {
81    fn expand(&self, _output: &Ident, _idents: &mut crate::ident::Iter) -> TokenStream {
82        let pascal_name = pascal!(self.name => ident);
83        let is_builtin = BUILTIN_PIPES.contains(&self.name.to_string().as_str());
84
85        if is_builtin {
86            if self.name == "trim" {
87                match self.args.as_slice() {
88                    [] => {
89                        quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::Trim(" ", " ")), __zyn_val); }
90                    }
91                    [a] => {
92                        quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::Trim(#a, #a)), __zyn_val); }
93                    }
94                    [a, b] => {
95                        quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::Trim(#a, #b)), __zyn_val); }
96                    }
97                    _ => quote! { compile_error!("trim pipe accepts at most 2 arguments"); },
98                }
99            } else if self.args.is_empty() {
100                quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::#pascal_name), __zyn_val); }
101            } else {
102                let args = &self.args;
103                quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::#pascal_name(#(#args),*)), __zyn_val); }
104            }
105        } else if self.args.is_empty() {
106            quote! { let __zyn_val = ::zyn::Pipe::pipe(&(#pascal_name), __zyn_val); }
107        } else {
108            let args = &self.args;
109            quote! { let __zyn_val = ::zyn::Pipe::pipe(&(#pascal_name(#(#args),*)), __zyn_val); }
110        }
111    }
112}