tauri_typegen/analysis/
serde_parser.rs1use quote::ToTokens;
2use syn::Attribute;
3
4#[derive(Debug)]
6pub struct SerdeParser;
7
8impl SerdeParser {
9 pub fn new() -> Self {
10 Self
11 }
12
13 pub fn parse_struct_serde_attrs(&self, attrs: &[Attribute]) -> SerdeStructAttributes {
15 let mut result = SerdeStructAttributes { rename_all: None };
16
17 for attr in attrs {
18 if attr.path().is_ident("serde") {
19 if let Ok(tokens) = syn::parse2::<syn::MetaList>(attr.meta.to_token_stream()) {
20 let tokens_str = tokens.tokens.to_string();
21
22 if let Some(convention) = self.parse_rename_all(&tokens_str) {
24 result.rename_all = Some(convention);
25 }
26 }
27 }
28 }
29
30 result
31 }
32
33 pub fn parse_field_serde_attrs(&self, attrs: &[Attribute]) -> SerdeFieldAttributes {
35 let mut result = SerdeFieldAttributes {
36 rename: None,
37 skip: false,
38 };
39
40 for attr in attrs {
41 if attr.path().is_ident("serde") {
42 if let Ok(tokens) = syn::parse2::<syn::MetaList>(attr.meta.to_token_stream()) {
43 let tokens_str = tokens.tokens.to_string();
44
45 if tokens_str.contains("skip") && !tokens_str.contains("skip_serializing") {
47 result.skip = true;
48 }
49
50 if let Some(rename) = self.parse_rename(&tokens_str) {
52 result.rename = Some(rename);
53 }
54 }
55 }
56 }
57
58 result
59 }
60
61 fn parse_rename_all(&self, tokens: &str) -> Option<String> {
63 if let Some(start) = tokens.find("rename_all") {
64 if let Some(eq_pos) = tokens[start..].find('=') {
65 let after_eq = &tokens[start + eq_pos + 1..].trim_start();
66
67 if let Some(quote_start) = after_eq.find('"') {
69 if let Some(quote_end) = after_eq[quote_start + 1..].find('"') {
70 let value = &after_eq[quote_start + 1..quote_start + 1 + quote_end];
71 return Some(value.to_string());
72 }
73 }
74 }
75 }
76 None
77 }
78
79 fn parse_rename(&self, tokens: &str) -> Option<String> {
81 let mut search_start = 0;
83 while let Some(pos) = tokens[search_start..].find("rename") {
84 let abs_pos = search_start + pos;
85
86 let after_rename = &tokens[abs_pos + 6..];
88 if after_rename.trim_start().starts_with("_all") {
89 search_start = abs_pos + 10; continue;
92 }
93
94 if let Some(eq_pos) = after_rename.find('=') {
96 let after_eq = &after_rename[eq_pos + 1..].trim_start();
97
98 if let Some(quote_start) = after_eq.find('"') {
100 if let Some(quote_end) = after_eq[quote_start + 1..].find('"') {
101 let value = &after_eq[quote_start + 1..quote_start + 1 + quote_end];
102 return Some(value.to_string());
103 }
104 }
105 }
106
107 break;
108 }
109 None
110 }
111}
112
113impl Default for SerdeParser {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119#[derive(Debug, Default, Clone)]
121pub struct SerdeStructAttributes {
122 pub rename_all: Option<String>,
123}
124
125#[derive(Debug, Default, Clone)]
127pub struct SerdeFieldAttributes {
128 pub rename: Option<String>,
129 pub skip: bool,
130}
131
132pub fn apply_naming_convention(field_name: &str, convention: &str) -> String {
134 match convention {
135 "camelCase" => to_camel_case(field_name),
136 "PascalCase" => to_pascal_case(field_name),
137 "snake_case" => to_snake_case(field_name),
138 "SCREAMING_SNAKE_CASE" => to_screaming_snake_case(field_name),
139 "kebab-case" => to_kebab_case(field_name),
140 "SCREAMING-KEBAB-CASE" => to_screaming_kebab_case(field_name),
141 _ => field_name.to_string(), }
143}
144
145fn to_camel_case(s: &str) -> String {
147 let words = split_into_words(s);
148 if words.is_empty() {
149 return String::new();
150 }
151
152 let mut result = words[0].to_lowercase();
153 for word in &words[1..] {
154 result.push_str(&capitalize_first(word));
155 }
156 result
157}
158
159fn to_pascal_case(s: &str) -> String {
161 split_into_words(s)
162 .iter()
163 .map(|word| capitalize_first(word))
164 .collect::<String>()
165}
166
167fn to_snake_case(s: &str) -> String {
169 split_into_words(s)
170 .iter()
171 .map(|word| word.to_lowercase())
172 .collect::<Vec<_>>()
173 .join("_")
174}
175
176fn to_screaming_snake_case(s: &str) -> String {
178 split_into_words(s)
179 .iter()
180 .map(|word| word.to_uppercase())
181 .collect::<Vec<_>>()
182 .join("_")
183}
184
185fn to_kebab_case(s: &str) -> String {
187 split_into_words(s)
188 .iter()
189 .map(|word| word.to_lowercase())
190 .collect::<Vec<_>>()
191 .join("-")
192}
193
194fn to_screaming_kebab_case(s: &str) -> String {
196 split_into_words(s)
197 .iter()
198 .map(|word| word.to_uppercase())
199 .collect::<Vec<_>>()
200 .join("-")
201}
202
203fn split_into_words(s: &str) -> Vec<String> {
205 let mut words = Vec::new();
206 let mut current_word = String::new();
207 let mut prev_was_lowercase = false;
208
209 for ch in s.chars() {
210 if ch == '_' || ch == '-' {
211 if !current_word.is_empty() {
212 words.push(current_word.clone());
213 current_word.clear();
214 }
215 prev_was_lowercase = false;
216 } else if ch.is_uppercase() {
217 if prev_was_lowercase && !current_word.is_empty() {
219 words.push(current_word.clone());
220 current_word.clear();
221 }
222 current_word.push(ch);
223 prev_was_lowercase = false;
224 } else {
225 current_word.push(ch);
226 prev_was_lowercase = true;
227 }
228 }
229
230 if !current_word.is_empty() {
231 words.push(current_word);
232 }
233
234 words
235}
236
237fn capitalize_first(s: &str) -> String {
239 let mut chars = s.chars();
240 match chars.next() {
241 None => String::new(),
242 Some(first) => {
243 let mut result = first.to_uppercase().to_string();
244 result.push_str(&chars.as_str().to_lowercase());
245 result
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_camel_case() {
256 assert_eq!(to_camel_case("user_id"), "userId");
257 assert_eq!(to_camel_case("user_name"), "userName");
258 assert_eq!(to_camel_case("is_active"), "isActive");
259 assert_eq!(to_camel_case("UserName"), "userName");
260 }
261
262 #[test]
263 fn test_pascal_case() {
264 assert_eq!(to_pascal_case("user_id"), "UserId");
265 assert_eq!(to_pascal_case("user_name"), "UserName");
266 assert_eq!(to_pascal_case("userName"), "UserName");
267 }
268
269 #[test]
270 fn test_snake_case() {
271 assert_eq!(to_snake_case("userId"), "user_id");
272 assert_eq!(to_snake_case("UserName"), "user_name");
273 assert_eq!(to_snake_case("user_name"), "user_name");
274 }
275
276 #[test]
277 fn test_screaming_snake_case() {
278 assert_eq!(to_screaming_snake_case("user_id"), "USER_ID");
279 assert_eq!(to_screaming_snake_case("userName"), "USER_NAME");
280 }
281
282 #[test]
283 fn test_kebab_case() {
284 assert_eq!(to_kebab_case("user_id"), "user-id");
285 assert_eq!(to_kebab_case("userName"), "user-name");
286 }
287
288 #[test]
289 fn test_screaming_kebab_case() {
290 assert_eq!(to_screaming_kebab_case("user_id"), "USER-ID");
291 assert_eq!(to_screaming_kebab_case("userName"), "USER-NAME");
292 }
293
294 #[test]
295 fn test_apply_naming_convention() {
296 assert_eq!(
297 apply_naming_convention("user_name", "camelCase"),
298 "userName"
299 );
300 assert_eq!(
301 apply_naming_convention("user_name", "PascalCase"),
302 "UserName"
303 );
304 assert_eq!(
305 apply_naming_convention("userName", "snake_case"),
306 "user_name"
307 );
308 }
309}