tauri_typegen/analysis/
serde_parser.rs

1use quote::ToTokens;
2use syn::Attribute;
3
4/// Parser for serde attributes from Rust struct/enum definitions and fields
5#[derive(Debug)]
6pub struct SerdeParser;
7
8impl SerdeParser {
9    pub fn new() -> Self {
10        Self
11    }
12
13    /// Parse struct-level serde attributes (e.g., rename_all)
14    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                    // Parse rename_all = "convention"
23                    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    /// Parse field-level serde attributes (e.g., rename, skip)
34    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                    // Check for skip flag
46                    if tokens_str.contains("skip") && !tokens_str.contains("skip_serializing") {
47                        result.skip = true;
48                    }
49
50                    // Parse rename = "value"
51                    if let Some(rename) = self.parse_rename(&tokens_str) {
52                        result.rename = Some(rename);
53                    }
54                }
55            }
56        }
57
58        result
59    }
60
61    /// Parse rename_all value like "camelCase", "snake_case", "PascalCase", etc.
62    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                // Extract value from quotes
68                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    /// Parse rename value from field attribute
80    fn parse_rename(&self, tokens: &str) -> Option<String> {
81        // Look for "rename" but not "rename_all"
82        let mut search_start = 0;
83        while let Some(pos) = tokens[search_start..].find("rename") {
84            let abs_pos = search_start + pos;
85
86            // Check if this is followed by "_all"
87            let after_rename = &tokens[abs_pos + 6..];
88            if after_rename.trim_start().starts_with("_all") {
89                // This is rename_all, skip it
90                search_start = abs_pos + 10; // Move past "rename_all"
91                continue;
92            }
93
94            // This is a plain "rename", extract the value
95            if let Some(eq_pos) = after_rename.find('=') {
96                let after_eq = &after_rename[eq_pos + 1..].trim_start();
97
98                // Extract value from quotes
99                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/// Struct-level serde attributes
120#[derive(Debug, Default, Clone)]
121pub struct SerdeStructAttributes {
122    pub rename_all: Option<String>,
123}
124
125/// Field-level serde attributes
126#[derive(Debug, Default, Clone)]
127pub struct SerdeFieldAttributes {
128    pub rename: Option<String>,
129    pub skip: bool,
130}
131
132/// Apply serde naming convention transformations
133pub 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(), // Unknown convention, return as-is
142    }
143}
144
145/// Convert to camelCase (first letter lowercase, subsequent words capitalized)
146fn 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
159/// Convert to PascalCase (all words capitalized, no separators)
160fn to_pascal_case(s: &str) -> String {
161    split_into_words(s)
162        .iter()
163        .map(|word| capitalize_first(word))
164        .collect::<String>()
165}
166
167/// Convert to snake_case (lowercase with underscores)
168fn 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
176/// Convert to SCREAMING_SNAKE_CASE (uppercase with underscores)
177fn 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
185/// Convert to kebab-case (lowercase with hyphens)
186fn 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
194/// Convert to SCREAMING-KEBAB-CASE (uppercase with hyphens)
195fn 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
203/// Split a string into words, handling snake_case, camelCase, and PascalCase
204fn 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            // New word starts if previous was lowercase
218            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
237/// Capitalize the first character of a string
238fn 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}