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