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
64fn is_builtin(name: &str) -> bool {
65    matches!(
66        name,
67        "upper"
68            | "lower"
69            | "snake"
70            | "camel"
71            | "pascal"
72            | "kebab"
73            | "screaming"
74            | "ident"
75            | "fmt"
76            | "str"
77            | "trim"
78            | "plural"
79            | "singular"
80    )
81}
82
83impl Expand for PipeNode {
84    fn expand(&self, _output: &Ident, _idents: &mut crate::ident::Iter) -> TokenStream {
85        let pascal_name = pascal!(self.name => ident);
86        let name_str = self.name.to_string();
87
88        if is_builtin(&name_str) {
89            if name_str == "trim" {
90                match self.args.as_slice() {
91                    [] => {
92                        quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::Trim(" ", " ")), __zyn_val); }
93                    }
94                    [a] => {
95                        quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::Trim(#a, #a)), __zyn_val); }
96                    }
97                    [a, b] => {
98                        quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::Trim(#a, #b)), __zyn_val); }
99                    }
100                    _ => quote! { compile_error!("trim pipe accepts at most 2 arguments"); },
101                }
102            } else if self.args.is_empty() {
103                quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::#pascal_name), __zyn_val); }
104            } else {
105                let args = &self.args;
106                quote! { let __zyn_val = ::zyn::Pipe::pipe(&(::zyn::pipes::#pascal_name(#(#args),*)), __zyn_val); }
107            }
108        } else if self.args.is_empty() {
109            quote! { let __zyn_val = ::zyn::Pipe::pipe(&(#pascal_name), __zyn_val); }
110        } else {
111            let args = &self.args;
112            quote! { let __zyn_val = ::zyn::Pipe::pipe(&(#pascal_name(#(#args),*)), __zyn_val); }
113        }
114    }
115}