Skip to main content

zyn_core/
case.rs

1/// Converts a string to PascalCase.
2///
3/// Handles snake_case, camelCase, PascalCase, and SCREAMING_SNAKE_CASE inputs.
4/// First normalizes to snake_case to detect word boundaries, then capitalizes each word.
5pub fn to_pascal(s: &str) -> String {
6    let snake = to_snake(s);
7    let mut out = String::new();
8    let mut capitalize = true;
9
10    for c in snake.chars() {
11        if c == '_' {
12            capitalize = true;
13        } else if capitalize {
14            out.extend(c.to_uppercase());
15            capitalize = false;
16        } else {
17            out.push(c);
18        }
19    }
20
21    out
22}
23
24/// Converts a string to snake_case.
25pub fn to_snake(s: &str) -> String {
26    let mut out = String::new();
27    let chars: Vec<char> = s.chars().collect();
28
29    for (i, &c) in chars.iter().enumerate() {
30        if c.is_uppercase() {
31            let prev_lower = i > 0 && chars[i - 1].is_lowercase();
32            let next_lower = i + 1 < chars.len() && chars[i + 1].is_lowercase();
33            let prev_upper = i > 0 && chars[i - 1].is_uppercase();
34
35            if prev_lower || (next_lower && prev_upper) {
36                out.push('_');
37            }
38
39            out.extend(c.to_lowercase());
40        } else if c == '_' {
41            if !out.is_empty() && !out.ends_with('_') {
42                out.push('_');
43            }
44        } else {
45            out.push(c);
46        }
47    }
48
49    out
50}
51
52/// Converts a string to camelCase.
53pub fn to_camel(s: &str) -> String {
54    let pascal = to_pascal(s);
55    let mut chars = pascal.chars();
56
57    match chars.next() {
58        None => String::new(),
59        Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
60    }
61}
62
63/// Converts a string to SCREAMING_SNAKE_CASE.
64pub fn to_screaming(s: &str) -> String {
65    to_snake(s).to_uppercase()
66}
67
68/// Converts a string to kebab-case.
69pub fn to_kebab(s: &str) -> String {
70    to_snake(s).replace('_', "-")
71}
72
73/// Converts a string or ident to PascalCase.
74///
75/// # Usage
76///
77/// - `pascal!("hello_world")` → `"HelloWorld"` (`String`)
78/// - `pascal!(ident => ident)` → PascalCase `syn::Ident`
79/// - `pascal!(token_stream => token_stream)` → PascalCase last ident in path
80#[macro_export]
81macro_rules! pascal {
82    ($ident:expr => ident) => {
83        syn::Ident::new(
84            &$crate::case::to_pascal(&$ident.to_string()),
85            $ident.span(),
86        )
87    };
88    ($ts:expr => token_stream) => {{
89        let __tokens: Vec<proc_macro2::TokenTree> = $ts.clone().into_iter().collect();
90        let mut __out = proc_macro2::TokenStream::new();
91
92        for (i, __tt) in __tokens.iter().enumerate() {
93            match __tt {
94                proc_macro2::TokenTree::Ident(__ident) => {
95                    let __is_last_ident = !__tokens[i + 1..]
96                        .iter()
97                        .any(|t| matches!(t, proc_macro2::TokenTree::Ident(_)));
98
99                    if __is_last_ident {
100                        quote::ToTokens::to_tokens(
101                            &$crate::pascal!(__ident => ident),
102                            &mut __out,
103                        );
104                    } else {
105                        quote::ToTokens::to_tokens(__ident, &mut __out);
106                    }
107                }
108                __other => {
109                    quote::ToTokens::to_tokens(__other, &mut __out);
110                }
111            }
112        }
113
114        __out
115    }};
116    ($s:expr) => {
117        $crate::case::to_pascal($s)
118    };
119}
120
121/// Converts a string or ident to snake_case.
122///
123/// - `snake!("HelloWorld")` → `"hello_world"` (`String`)
124/// - `snake!(ident => ident)` → snake_case `syn::Ident`
125#[macro_export]
126macro_rules! snake {
127    ($ident:expr => ident) => {
128        syn::Ident::new(&$crate::case::to_snake(&$ident.to_string()), $ident.span())
129    };
130    ($s:expr) => {
131        $crate::case::to_snake($s)
132    };
133}
134
135/// Converts a string or ident to camelCase.
136///
137/// - `camel!("hello_world")` → `"helloWorld"` (`String`)
138/// - `camel!(ident => ident)` → camelCase `syn::Ident`
139#[macro_export]
140macro_rules! camel {
141    ($ident:expr => ident) => {
142        syn::Ident::new(&$crate::case::to_camel(&$ident.to_string()), $ident.span())
143    };
144    ($s:expr) => {
145        $crate::case::to_camel($s)
146    };
147}
148
149/// Converts a string or ident to SCREAMING_SNAKE_CASE.
150///
151/// - `screaming!("HelloWorld")` → `"HELLO_WORLD"` (`String`)
152/// - `screaming!(ident => ident)` → SCREAMING_SNAKE_CASE `syn::Ident`
153#[macro_export]
154macro_rules! screaming {
155    ($ident:expr => ident) => {
156        syn::Ident::new(
157            &$crate::case::to_screaming(&$ident.to_string()),
158            $ident.span(),
159        )
160    };
161    ($s:expr) => {
162        $crate::case::to_screaming($s)
163    };
164}
165
166/// Converts a string or ident to kebab-case.
167///
168/// - `kebab!("HelloWorld")` → `"hello-world"` (`String`)
169#[macro_export]
170macro_rules! kebab {
171    ($s:expr) => {
172        $crate::case::to_kebab($s)
173    };
174}