Skip to main content

zyn_core/
pipes.rs

1//! Built-in pipe transforms for template value interpolation.
2//!
3//! Each pipe is a zero-sized struct implementing [`crate::Pipe`]. Pipes are applied
4//! in `{{ expr | pipe }}` expressions inside `zyn!` templates and can be chained.
5//!
6//! # Template usage
7//!
8//! ```ignore
9//! // Single pipe
10//! zyn::zyn! { fn {{ name | snake }}() {} }
11//! // name = "HelloWorld" → fn hello_world() {}
12//!
13//! // Chained pipes
14//! zyn::zyn! { fn {{ name | snake | ident:"get_{}" }}() {} }
15//! // name = "HelloWorld" → fn get_hello_world() {}
16//!
17//! // Pipe to string literal
18//! zyn::zyn! { const PATH: &str = {{ name | kebab }}; }
19//! // name = "MyComponent" → const PATH: &str = "my-component";
20//! ```
21//!
22//! # Using pipes outside templates
23//!
24//! ```ignore
25//! use zyn_core::{Pipe, pipes};
26//!
27//! let ident = pipes::Snake.pipe("HelloWorld".to_string());
28//! // → hello_world (proc_macro2::Ident)
29//!
30//! let lit = pipes::Fmt("error_{}.rs").pipe("not_found".to_string());
31//! // → "error_not_found.rs" (syn::LitStr)
32//! ```
33
34use crate::Pipe;
35use crate::case;
36
37/// Converts the input to UPPERCASE.
38///
39/// Template usage: `{{ name | upper }}`
40pub struct Upper;
41
42impl Pipe for Upper {
43    type Input = String;
44    type Output = proc_macro2::Ident;
45
46    fn pipe(&self, input: String) -> proc_macro2::Ident {
47        proc_macro2::Ident::new(&input.to_uppercase(), proc_macro2::Span::call_site())
48    }
49}
50
51/// Converts the input to lowercase.
52///
53/// Template usage: `{{ name | lower }}`
54pub struct Lower;
55
56impl Pipe for Lower {
57    type Input = String;
58    type Output = proc_macro2::Ident;
59
60    fn pipe(&self, input: String) -> proc_macro2::Ident {
61        proc_macro2::Ident::new(&input.to_lowercase(), proc_macro2::Span::call_site())
62    }
63}
64
65/// Converts the input to snake_case.
66///
67/// Template usage: `{{ name | snake }}`
68pub struct Snake;
69
70impl Pipe for Snake {
71    type Input = String;
72    type Output = proc_macro2::Ident;
73
74    fn pipe(&self, input: String) -> proc_macro2::Ident {
75        proc_macro2::Ident::new(&case::to_snake(&input), proc_macro2::Span::call_site())
76    }
77}
78
79/// Converts the input to camelCase.
80///
81/// Template usage: `{{ name | camel }}`
82pub struct Camel;
83
84impl Pipe for Camel {
85    type Input = String;
86    type Output = proc_macro2::Ident;
87
88    fn pipe(&self, input: String) -> proc_macro2::Ident {
89        proc_macro2::Ident::new(&case::to_camel(&input), proc_macro2::Span::call_site())
90    }
91}
92
93/// Converts the input to PascalCase.
94///
95/// Template usage: `{{ name | pascal }}`
96pub struct Pascal;
97
98impl Pipe for Pascal {
99    type Input = String;
100    type Output = proc_macro2::Ident;
101
102    fn pipe(&self, input: String) -> proc_macro2::Ident {
103        proc_macro2::Ident::new(&case::to_pascal(&input), proc_macro2::Span::call_site())
104    }
105}
106
107/// Converts the input to kebab-case as a string literal.
108///
109/// Unlike other pipes that return `Ident`, this returns a `LitStr`
110/// because hyphens are not valid in Rust identifiers.
111///
112/// Template usage: `{{ name | kebab }}`
113pub struct Kebab;
114
115impl Pipe for Kebab {
116    type Input = String;
117    type Output = syn::LitStr;
118
119    fn pipe(&self, input: String) -> syn::LitStr {
120        syn::LitStr::new(&case::to_kebab(&input), proc_macro2::Span::call_site())
121    }
122}
123
124/// Converts the input to SCREAMING_SNAKE_CASE.
125///
126/// Template usage: `{{ name | screaming }}`
127pub struct Screaming;
128
129impl Pipe for Screaming {
130    type Input = String;
131    type Output = proc_macro2::Ident;
132
133    fn pipe(&self, input: String) -> proc_macro2::Ident {
134        proc_macro2::Ident::new(&case::to_screaming(&input), proc_macro2::Span::call_site())
135    }
136}
137
138/// Formats the input using a pattern string, producing an `Ident`.
139///
140/// The `{}` placeholder in the pattern is replaced with the input value.
141///
142/// Template usage: `{{ name | ident:"get_{}" }}`
143pub struct Ident(pub &'static str);
144
145impl Pipe for Ident {
146    type Input = String;
147    type Output = proc_macro2::Ident;
148
149    fn pipe(&self, input: String) -> proc_macro2::Ident {
150        let formatted = self.0.replace("{}", &input);
151        proc_macro2::Ident::new(&formatted, proc_macro2::Span::call_site())
152    }
153}
154
155/// Formats the input using a pattern string, producing a string literal.
156///
157/// The `{}` placeholder in the pattern is replaced with the input value.
158///
159/// Template usage: `{{ name | fmt:"hello {}" }}`
160pub struct Fmt(pub &'static str);
161
162impl Pipe for Fmt {
163    type Input = String;
164    type Output = syn::LitStr;
165
166    fn pipe(&self, input: String) -> syn::LitStr {
167        let formatted = self.0.replace("{}", &input);
168        syn::LitStr::new(&formatted, proc_macro2::Span::call_site())
169    }
170}
171
172/// Converts the input to a string literal.
173///
174/// Template usage: `{{ name | str }}`
175pub struct Str;
176
177impl Pipe for Str {
178    type Input = String;
179    type Output = syn::LitStr;
180
181    fn pipe(&self, input: String) -> syn::LitStr {
182        syn::LitStr::new(&input, proc_macro2::Span::call_site())
183    }
184}
185
186/// Trims characters from the start and end of the input.
187///
188/// Template usage: `{{ name | trim:"_" }}`
189pub struct Trim(pub &'static str, pub &'static str);
190
191impl Pipe for Trim {
192    type Input = String;
193    type Output = proc_macro2::Ident;
194
195    fn pipe(&self, input: String) -> proc_macro2::Ident {
196        let trimmed = input
197            .trim_start_matches(|c: char| self.0.contains(c))
198            .trim_end_matches(|c: char| self.1.contains(c));
199        proc_macro2::Ident::new(trimmed, proc_macro2::Span::call_site())
200    }
201}
202
203/// Converts the input to its plural form.
204///
205/// Template usage: `{{ name | plural }}`
206pub struct Plural;
207
208impl Pipe for Plural {
209    type Input = String;
210    type Output = proc_macro2::Ident;
211
212    fn pipe(&self, input: String) -> proc_macro2::Ident {
213        let result = if input.ends_with('y')
214            && input
215                .chars()
216                .rev()
217                .nth(1)
218                .is_some_and(|c| !"aeiou".contains(c))
219        {
220            format!("{}ies", &input[..input.len() - 1])
221        } else if input.ends_with('s')
222            || input.ends_with('x')
223            || input.ends_with('z')
224            || input.ends_with("ch")
225            || input.ends_with("sh")
226        {
227            format!("{}es", input)
228        } else {
229            format!("{}s", input)
230        };
231        proc_macro2::Ident::new(&result, proc_macro2::Span::call_site())
232    }
233}
234
235/// Converts the input to its singular form.
236///
237/// Template usage: `{{ name | singular }}`
238pub struct Singular;
239
240impl Pipe for Singular {
241    type Input = String;
242    type Output = proc_macro2::Ident;
243
244    fn pipe(&self, input: String) -> proc_macro2::Ident {
245        let result = if input.ends_with("ies") {
246            format!("{}y", &input[..input.len() - 3])
247        } else if input.ends_with("ses")
248            || input.ends_with("xes")
249            || input.ends_with("zes")
250            || input.ends_with("ches")
251            || input.ends_with("shes")
252        {
253            input[..input.len() - 2].to_string()
254        } else if input.ends_with('s') && !input.ends_with("ss") {
255            input[..input.len() - 1].to_string()
256        } else {
257            input
258        };
259        proc_macro2::Ident::new(&result, proc_macro2::Span::call_site())
260    }
261}