tauri_typegen/analysis/
serde_parser.rs

1use quote::ToTokens;
2use serde_rename_rule::RenameRule;
3use syn::Attribute;
4
5/// Parser for serde attributes from Rust struct/enum definitions and fields
6#[derive(Debug)]
7pub struct SerdeParser;
8
9impl SerdeParser {
10    pub fn new() -> Self {
11        Self
12    }
13
14    /// Parse struct-level serde attributes (e.g., rename_all)
15    pub fn parse_struct_serde_attrs(&self, attrs: &[Attribute]) -> SerdeStructAttributes {
16        let mut result = SerdeStructAttributes { rename_all: None };
17
18        for attr in attrs {
19            if attr.path().is_ident("serde") {
20                if let Ok(tokens) = syn::parse2::<syn::MetaList>(attr.meta.to_token_stream()) {
21                    let tokens_str = tokens.tokens.to_string();
22
23                    // Parse rename_all = "convention"
24                    if let Some(convention) = self.parse_rename_all(&tokens_str) {
25                        result.rename_all = Some(convention);
26                    }
27                }
28            }
29        }
30
31        result
32    }
33
34    /// Parse field-level serde attributes (e.g., rename, skip)
35    pub fn parse_field_serde_attrs(&self, attrs: &[Attribute]) -> SerdeFieldAttributes {
36        let mut result = SerdeFieldAttributes {
37            rename: None,
38            skip: false,
39        };
40
41        for attr in attrs {
42            if attr.path().is_ident("serde") {
43                if let Ok(tokens) = syn::parse2::<syn::MetaList>(attr.meta.to_token_stream()) {
44                    let tokens_str = tokens.tokens.to_string();
45
46                    // Check for skip flag
47                    if tokens_str.contains("skip") && !tokens_str.contains("skip_serializing") {
48                        result.skip = true;
49                    }
50
51                    // Parse rename = "value"
52                    if let Some(rename) = self.parse_rename(&tokens_str) {
53                        result.rename = Some(rename);
54                    }
55                }
56            }
57        }
58
59        result
60    }
61
62    /// Parse rename_all value like "camelCase", "snake_case", "PascalCase", etc. to
63    /// find a matching `serde_rename_rule::RenameRule`.
64    fn parse_rename_all(&self, tokens: &str) -> Option<RenameRule> {
65        if let Some(start) = tokens.find("rename_all") {
66            if let Some(eq_pos) = tokens[start..].find('=') {
67                let after_eq = &tokens[start + eq_pos + 1..].trim_start();
68
69                // Extract value from quotes
70                if let Some(quote_start) = after_eq.find('"') {
71                    if let Some(quote_end) = after_eq[quote_start + 1..].find('"') {
72                        let value = &after_eq[quote_start + 1..quote_start + 1 + quote_end];
73
74                        return RenameRule::from_rename_all_str(value).ok();
75                    }
76                }
77            }
78        }
79        None
80    }
81
82    /// Parse rename value from field attribute
83    fn parse_rename(&self, tokens: &str) -> Option<String> {
84        // Look for "rename" but not "rename_all"
85        let mut search_start = 0;
86        while let Some(pos) = tokens[search_start..].find("rename") {
87            let abs_pos = search_start + pos;
88
89            // Check if this is followed by "_all"
90            let after_rename = &tokens[abs_pos + 6..];
91            if after_rename.trim_start().starts_with("_all") {
92                // This is rename_all, skip it
93                search_start = abs_pos + 10; // Move past "rename_all"
94                continue;
95            }
96
97            // This is a plain "rename", extract the value
98            if let Some(eq_pos) = after_rename.find('=') {
99                let after_eq = &after_rename[eq_pos + 1..].trim_start();
100
101                // Extract value from quotes
102                if let Some(quote_start) = after_eq.find('"') {
103                    if let Some(quote_end) = after_eq[quote_start + 1..].find('"') {
104                        let value = &after_eq[quote_start + 1..quote_start + 1 + quote_end];
105                        return Some(value.to_string());
106                    }
107                }
108            }
109
110            break;
111        }
112        None
113    }
114}
115
116impl Default for SerdeParser {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122/// Struct-level serde attributes
123#[derive(Debug, Default, Clone)]
124pub struct SerdeStructAttributes {
125    pub rename_all: Option<RenameRule>,
126}
127
128/// Field-level serde attributes
129#[derive(Debug, Default, Clone)]
130pub struct SerdeFieldAttributes {
131    pub rename: Option<String>,
132    pub skip: bool,
133}
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use syn::parse_quote;
138
139    #[test]
140    fn test_parse_rename_all_camel_case() {
141        let parser = SerdeParser::new();
142        let result = parser.parse_rename_all(r#"rename_all = "camelCase""#);
143
144        assert!(result.is_some());
145        assert!(matches!(result.unwrap(), RenameRule::CamelCase));
146    }
147
148    #[test]
149    fn test_parse_rename_all_snake_case() {
150        let parser = SerdeParser::new();
151        let result = parser.parse_rename_all(r#"rename_all = "snake_case""#);
152
153        assert!(result.is_some());
154        assert!(matches!(result.unwrap(), RenameRule::SnakeCase));
155    }
156
157    #[test]
158    fn test_parse_rename_all_pascal_case() {
159        let parser = SerdeParser::new();
160        let result = parser.parse_rename_all(r#"rename_all = "PascalCase""#);
161
162        assert!(result.is_some());
163        assert!(matches!(result.unwrap(), RenameRule::PascalCase));
164    }
165
166    #[test]
167    fn test_parse_rename_all_screaming_snake_case() {
168        let parser = SerdeParser::new();
169        let result = parser.parse_rename_all(r#"rename_all = "SCREAMING_SNAKE_CASE""#);
170
171        assert!(result.is_some());
172        assert!(matches!(result.unwrap(), RenameRule::ScreamingSnakeCase));
173    }
174
175    #[test]
176    fn test_parse_rename_all_kebab_case() {
177        let parser = SerdeParser::new();
178        let result = parser.parse_rename_all(r#"rename_all = "kebab-case""#);
179
180        assert!(result.is_some());
181        assert!(matches!(result.unwrap(), RenameRule::KebabCase));
182    }
183
184    #[test]
185    fn test_parse_rename_all_not_present() {
186        let parser = SerdeParser::new();
187        let result = parser.parse_rename_all(r#"skip_serializing_if = "Option::is_none""#);
188
189        assert!(result.is_none());
190    }
191
192    #[test]
193    fn test_parse_rename() {
194        let parser = SerdeParser::new();
195
196        let result = parser.parse_rename(r#"rename = "customName""#);
197        assert_eq!(result, Some("customName".to_string()));
198
199        let result = parser.parse_rename(r#"rename = "id""#);
200        assert_eq!(result, Some("id".to_string()));
201    }
202
203    #[test]
204    fn test_parse_rename_not_rename_all() {
205        let parser = SerdeParser::new();
206
207        // Should not match rename_all
208        let result = parser.parse_rename(r#"rename_all = "camelCase""#);
209        assert!(result.is_none());
210    }
211
212    #[test]
213    fn test_parse_rename_with_rename_all_present() {
214        let parser = SerdeParser::new();
215
216        // Should find "rename" even if rename_all is also present
217        let result = parser.parse_rename(r#"rename_all = "camelCase", rename = "id""#);
218        assert_eq!(result, Some("id".to_string()));
219    }
220
221    #[test]
222    fn test_parse_struct_serde_attrs_with_rename_all() {
223        let parser = SerdeParser::new();
224        let attrs: Vec<Attribute> = vec![parse_quote!(#[serde(rename_all = "camelCase")])];
225
226        let result = parser.parse_struct_serde_attrs(&attrs);
227        assert!(result.rename_all.is_some());
228        assert!(matches!(result.rename_all.unwrap(), RenameRule::CamelCase));
229    }
230
231    #[test]
232    fn test_parse_struct_serde_attrs_no_serde() {
233        let parser = SerdeParser::new();
234        let attrs: Vec<Attribute> = vec![parse_quote!(#[derive(Debug)])];
235
236        let result = parser.parse_struct_serde_attrs(&attrs);
237        assert!(result.rename_all.is_none());
238    }
239
240    #[test]
241    fn test_parse_field_serde_attrs_with_rename() {
242        let parser = SerdeParser::new();
243        let attrs: Vec<Attribute> = vec![parse_quote!(#[serde(rename = "customName")])];
244
245        let result = parser.parse_field_serde_attrs(&attrs);
246        assert_eq!(result.rename, Some("customName".to_string()));
247        assert!(!result.skip);
248    }
249
250    #[test]
251    fn test_parse_field_serde_attrs_with_skip() {
252        let parser = SerdeParser::new();
253        let attrs: Vec<Attribute> = vec![parse_quote!(#[serde(skip)])];
254
255        let result = parser.parse_field_serde_attrs(&attrs);
256        assert!(result.skip);
257        assert!(result.rename.is_none());
258    }
259
260    #[test]
261    fn test_parse_field_serde_attrs_skip_serializing_not_skip() {
262        let parser = SerdeParser::new();
263        let attrs: Vec<Attribute> = vec![parse_quote!(#[serde(skip_serializing)])];
264
265        let result = parser.parse_field_serde_attrs(&attrs);
266        // skip_serializing should not set skip flag
267        assert!(!result.skip);
268    }
269
270    #[test]
271    fn test_parse_field_serde_attrs_multiple() {
272        let parser = SerdeParser::new();
273        let attrs: Vec<Attribute> = vec![
274            parse_quote!(#[serde(rename = "id")]),
275            parse_quote!(#[derive(Debug)]),
276        ];
277
278        let result = parser.parse_field_serde_attrs(&attrs);
279        assert_eq!(result.rename, Some("id".to_string()));
280    }
281
282    #[test]
283    fn test_parse_field_serde_attrs_no_serde() {
284        let parser = SerdeParser::new();
285        let attrs: Vec<Attribute> = vec![parse_quote!(#[derive(Debug)])];
286
287        let result = parser.parse_field_serde_attrs(&attrs);
288        assert!(result.rename.is_none());
289        assert!(!result.skip);
290    }
291
292    #[test]
293    fn test_default_impl() {
294        let parser = SerdeParser;
295        let result = parser.parse_rename(r#"rename = "test""#);
296        assert_eq!(result, Some("test".to_string()));
297    }
298}