schema_bridge_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Lit, Meta};
4
5#[proc_macro_derive(SchemaBridge, attributes(schema_bridge, serde))]
6pub fn derive_schema_bridge(input: TokenStream) -> TokenStream {
7    let input = parse_macro_input!(input as DeriveInput);
8    let name = &input.ident;
9
10    let ts_impl = impl_to_ts(&input);
11    let schema_impl = impl_to_schema(name, &input.data);
12
13    // Check for string_conversion attribute
14    let string_conversion = has_string_conversion(&input.attrs);
15
16    let mut expanded = quote! {
17        impl ::schema_bridge::SchemaBridge for #name {
18            fn to_ts() -> String {
19                #ts_impl
20            }
21
22            fn to_schema() -> ::schema_bridge::Schema {
23                #schema_impl
24            }
25        }
26    };
27
28    // Generate Display and FromStr if requested
29    if string_conversion {
30        if let Data::Enum(_) = &input.data {
31            let display_impl = impl_display(&input);
32            let fromstr_impl = impl_fromstr(&input);
33
34            expanded = quote! {
35                #expanded
36
37                #display_impl
38
39                #fromstr_impl
40            };
41        }
42    }
43
44    TokenStream::from(expanded)
45}
46
47/// Check if #[schema_bridge(string_conversion)] attribute is present
48fn has_string_conversion(attrs: &[syn::Attribute]) -> bool {
49    for attr in attrs {
50        if attr.path().is_ident("schema_bridge") {
51            if let Meta::List(meta_list) = &attr.meta {
52                if let Ok(Meta::Path(path)) = syn::parse2(meta_list.tokens.clone()) {
53                    if path.is_ident("string_conversion") {
54                        return true;
55                    }
56                }
57            }
58        }
59    }
60    false
61}
62
63fn impl_to_ts(input: &DeriveInput) -> proc_macro2::TokenStream {
64    match &input.data {
65        Data::Struct(data) => {
66            match &data.fields {
67                Fields::Named(fields) => {
68                    let fields_ts = fields.named.iter().map(|f| {
69                        let field_name = &f.ident;
70                        let ty = &f.ty;
71                        quote! {
72                            format!("{}: {};", stringify!(#field_name), <#ty as ::schema_bridge::SchemaBridge>::to_ts())
73                        }
74                    });
75
76                    quote! {
77                        let fields = vec![#(#fields_ts),*];
78                        format!("{{ {} }}", fields.join(" "))
79                    }
80                }
81                Fields::Unnamed(fields) => {
82                    // Support for tuple structs, especially newtype pattern
83                    if fields.unnamed.len() == 1 {
84                        // Newtype pattern: delegate to the inner type
85                        let inner_ty = &fields.unnamed[0].ty;
86                        quote! {
87                            <#inner_ty as ::schema_bridge::SchemaBridge>::to_ts()
88                        }
89                    } else {
90                        // Multiple field tuple struct - represent as tuple
91                        let field_types = fields.unnamed.iter().map(|f| {
92                            let ty = &f.ty;
93                            quote! {
94                                <#ty as ::schema_bridge::SchemaBridge>::to_ts()
95                            }
96                        });
97
98                        quote! {
99                            let types = vec![#(#field_types),*];
100                            format!("[{}]", types.join(", "))
101                        }
102                    }
103                }
104                Fields::Unit => quote! { "null".to_string() },
105            }
106        }
107        Data::Enum(data) => {
108            // Check for serde rename_all attribute
109            let rename_all = get_serde_rename_all(&input.attrs);
110
111            let variants = data.variants.iter().map(|v| {
112                let variant_name = &v.ident;
113                let variant_str = variant_name.to_string();
114
115                // Apply rename_all transformation if present
116                let ts_name = if let Some(ref rule) = rename_all {
117                    apply_rename_rule(&variant_str, rule)
118                } else {
119                    variant_str
120                };
121
122                quote! {
123                    format!("'{}'", #ts_name)
124                }
125            });
126
127            quote! {
128                let variants = vec![#(#variants),*];
129                variants.join(" | ")
130            }
131        }
132        _ => quote! { "any".to_string() },
133    }
134}
135
136/// Extract rename_all from #[serde(rename_all = "...")]
137fn get_serde_rename_all(attrs: &[syn::Attribute]) -> Option<String> {
138    for attr in attrs {
139        if attr.path().is_ident("serde") {
140            if let Meta::List(meta_list) = &attr.meta {
141                // Parse the meta list
142                let nested: Result<Meta, _> = syn::parse2(meta_list.tokens.clone());
143                if let Ok(Meta::NameValue(nv)) = nested {
144                    if nv.path.is_ident("rename_all") {
145                        if let syn::Expr::Lit(expr_lit) = &nv.value {
146                            if let Lit::Str(lit_str) = &expr_lit.lit {
147                                return Some(lit_str.value());
148                            }
149                        }
150                    }
151                }
152            }
153        }
154    }
155    None
156}
157
158/// Apply serde rename_all transformation
159fn apply_rename_rule(name: &str, rule: &str) -> String {
160    match rule {
161        "lowercase" => name.to_lowercase(),
162        "UPPERCASE" => name.to_uppercase(),
163        "PascalCase" => name.to_string(), // Already PascalCase
164        "camelCase" => {
165            let mut chars = name.chars();
166            match chars.next() {
167                None => String::new(),
168                Some(first) => first.to_lowercase().chain(chars).collect(),
169            }
170        }
171        "snake_case" => {
172            let mut result = String::new();
173            for (i, ch) in name.chars().enumerate() {
174                if ch.is_uppercase() && i > 0 {
175                    result.push('_');
176                }
177                result.push(ch.to_lowercase().next().unwrap());
178            }
179            result
180        }
181        "SCREAMING_SNAKE_CASE" => {
182            let mut result = String::new();
183            for (i, ch) in name.chars().enumerate() {
184                if ch.is_uppercase() && i > 0 {
185                    result.push('_');
186                }
187                result.push(ch.to_uppercase().next().unwrap());
188            }
189            result
190        }
191        "kebab-case" => {
192            let mut result = String::new();
193            for (i, ch) in name.chars().enumerate() {
194                if ch.is_uppercase() && i > 0 {
195                    result.push('-');
196                }
197                result.push(ch.to_lowercase().next().unwrap());
198            }
199            result
200        }
201        _ => name.to_string(), // Unknown rule, keep as-is
202    }
203}
204
205fn impl_to_schema(_name: &Ident, _data: &Data) -> proc_macro2::TokenStream {
206    // Placeholder for now, focusing on TS generation first
207    quote! {
208        ::schema_bridge::Schema::Any
209    }
210}
211
212/// Generate Display implementation for enum
213fn impl_display(input: &DeriveInput) -> proc_macro2::TokenStream {
214    let name = &input.ident;
215
216    if let Data::Enum(data) = &input.data {
217        let rename_all = get_serde_rename_all(&input.attrs);
218
219        let match_arms = data.variants.iter().map(|v| {
220            let variant_name = &v.ident;
221            let variant_str = variant_name.to_string();
222
223            let display_str = if let Some(ref rule) = rename_all {
224                apply_rename_rule(&variant_str, rule)
225            } else {
226                variant_str
227            };
228
229            quote! {
230                #name::#variant_name => write!(f, "{}", #display_str)
231            }
232        });
233
234        quote! {
235            impl ::std::fmt::Display for #name {
236                fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
237                    match self {
238                        #(#match_arms),*
239                    }
240                }
241            }
242        }
243    } else {
244        quote! {}
245    }
246}
247
248/// Generate FromStr implementation for enum
249fn impl_fromstr(input: &DeriveInput) -> proc_macro2::TokenStream {
250    let name = &input.ident;
251
252    if let Data::Enum(data) = &input.data {
253        let rename_all = get_serde_rename_all(&input.attrs);
254
255        let match_arms = data.variants.iter().map(|v| {
256            let variant_name = &v.ident;
257            let variant_str = variant_name.to_string();
258
259            let pattern_str = if let Some(ref rule) = rename_all {
260                apply_rename_rule(&variant_str, rule)
261            } else {
262                variant_str
263            };
264
265            quote! {
266                #pattern_str => ::std::result::Result::Ok(#name::#variant_name)
267            }
268        });
269
270        quote! {
271            impl ::std::str::FromStr for #name {
272                type Err = String;
273
274                fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
275                    match s {
276                        #(#match_arms,)*
277                        _ => ::std::result::Result::Err(format!("Unknown {}: {}", stringify!(#name), s))
278                    }
279                }
280            }
281        }
282    } else {
283        quote! {}
284    }
285}