schema_bridge_macro/
lib.rs1use 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 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 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
47fn 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 if fields.unnamed.len() == 1 {
84 let inner_ty = &fields.unnamed[0].ty;
86 quote! {
87 <#inner_ty as ::schema_bridge::SchemaBridge>::to_ts()
88 }
89 } else {
90 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 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 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
136fn 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 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
158fn 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(), "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(), }
203}
204
205fn impl_to_schema(_name: &Ident, _data: &Data) -> proc_macro2::TokenStream {
206 quote! {
208 ::schema_bridge::Schema::Any
209 }
210}
211
212fn 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
248fn 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}