1use syn::{Attribute, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, Type};
4use typewriter_core::ir::*;
5
6pub fn parse_type_def(input: &DeriveInput) -> syn::Result<TypeDef> {
8 let name = input.ident.to_string();
9 let doc = extract_doc_comment(&input.attrs);
10 let generics: Vec<String> = input
11 .generics
12 .type_params()
13 .map(|p| p.ident.to_string())
14 .collect();
15
16 match &input.data {
17 Data::Struct(data) => {
18 let fields = parse_fields(&data.fields)?;
19 Ok(TypeDef::Struct(StructDef {
20 name,
21 fields,
22 doc,
23 generics,
24 }))
25 }
26 Data::Enum(data) => {
27 let repr = parse_enum_repr(&input.attrs);
28 let variants = data
29 .variants
30 .iter()
31 .map(parse_variant)
32 .collect::<syn::Result<Vec<_>>>()?;
33
34 Ok(TypeDef::Enum(EnumDef {
35 name,
36 variants,
37 representation: repr,
38 doc,
39 }))
40 }
41 Data::Union(_) => Err(syn::Error::new_spanned(
42 &input.ident,
43 "typewriter: unions are not supported. Use structs or enums.",
44 )),
45 }
46}
47
48pub fn parse_sync_to_attr(input: &DeriveInput) -> syn::Result<Vec<Language>> {
50 let mut targets = Vec::new();
51
52 for attr in &input.attrs {
53 if !attr.path().is_ident("sync_to") {
54 continue;
55 }
56
57 attr.parse_nested_meta(|meta| {
58 if let Some(ident) = meta.path.get_ident() {
59 let lang_str = ident.to_string();
60 if let Some(language) = Language::from_str(&lang_str) {
61 targets.push(language);
62 } else {
63 return Err(meta.error(format!(
64 "typewriter: unknown language '{}'. Supported: typescript, python, go, swift, kotlin",
65 lang_str
66 )));
67 }
68 }
69 Ok(())
70 })?;
71 }
72
73 Ok(targets)
74}
75
76pub fn has_typewriter_derive(attrs: &[Attribute]) -> bool {
78 for attr in attrs {
79 if !attr.path().is_ident("derive") {
80 continue;
81 }
82
83 let mut found = false;
84 let _ = attr.parse_nested_meta(|meta| {
85 if meta
86 .path
87 .segments
88 .last()
89 .map(|s| s.ident == "TypeWriter")
90 .unwrap_or(false)
91 {
92 found = true;
93 }
94 Ok(())
95 });
96
97 if found {
98 return true;
99 }
100 }
101
102 false
103}
104
105fn parse_fields(fields: &Fields) -> syn::Result<Vec<FieldDef>> {
107 match fields {
108 Fields::Named(named) => named
109 .named
110 .iter()
111 .map(|f| {
112 let name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
113 let ty = parse_type(&f.ty);
114 let optional =
115 matches!(&ty, TypeKind::Option(_)) || has_tw_attr(&f.attrs, "optional");
116 let rename = get_rename(&f.attrs);
117 let skip = has_serde_skip(&f.attrs) || has_tw_attr(&f.attrs, "skip");
118 let flatten = has_serde_flatten(&f.attrs);
119 let doc = extract_doc_comment(&f.attrs);
120 let type_override = get_tw_type_override(&f.attrs);
121
122 Ok(FieldDef {
123 name,
124 ty,
125 optional,
126 rename,
127 doc,
128 skip,
129 flatten,
130 type_override,
131 })
132 })
133 .collect(),
134 Fields::Unnamed(_) | Fields::Unit => Ok(vec![]),
135 }
136}
137
138fn parse_type(ty: &Type) -> TypeKind {
140 match ty {
141 Type::Path(type_path) => {
142 let path = &type_path.path;
143
144 if let Some(segment) = path.segments.last() {
145 let ident = segment.ident.to_string();
146
147 match ident.as_str() {
148 "Option" => {
149 if let Some(inner) = extract_single_generic_arg(segment) {
150 return TypeKind::Option(Box::new(parse_type(&inner)));
151 }
152 }
153 "Vec" => {
154 if let Some(inner) = extract_single_generic_arg(segment) {
155 return TypeKind::Vec(Box::new(parse_type(&inner)));
156 }
157 }
158 "HashMap" | "BTreeMap" => {
159 if let Some((k, v)) = extract_double_generic_arg(segment) {
160 return TypeKind::HashMap(
161 Box::new(parse_type(&k)),
162 Box::new(parse_type(&v)),
163 );
164 }
165 }
166 "Box" | "Arc" | "Rc" => {
167 if let Some(inner) = extract_single_generic_arg(segment) {
168 return parse_type(&inner);
169 }
170 }
171 _ => {}
172 }
173
174 if let Some(prim) = map_primitive_name(&ident) {
175 return TypeKind::Primitive(prim);
176 }
177
178 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
179 let type_args: Vec<TypeKind> = args
180 .args
181 .iter()
182 .filter_map(|arg| {
183 if let syn::GenericArgument::Type(ty) = arg {
184 Some(parse_type(ty))
185 } else {
186 None
187 }
188 })
189 .collect();
190
191 if !type_args.is_empty() {
192 return TypeKind::Generic(ident, type_args);
193 }
194 }
195
196 TypeKind::Named(ident)
197 } else {
198 TypeKind::Unit
199 }
200 }
201 Type::Tuple(tuple) => {
202 if tuple.elems.is_empty() {
203 TypeKind::Unit
204 } else {
205 let elements: Vec<TypeKind> = tuple.elems.iter().map(parse_type).collect();
206 TypeKind::Tuple(elements)
207 }
208 }
209 Type::Reference(reference) => parse_type(&reference.elem),
210 _ => TypeKind::Unit,
211 }
212}
213
214fn map_primitive_name(name: &str) -> Option<PrimitiveType> {
215 match name {
216 "String" | "str" => Some(PrimitiveType::String),
217 "bool" => Some(PrimitiveType::Bool),
218 "u8" => Some(PrimitiveType::U8),
219 "u16" => Some(PrimitiveType::U16),
220 "u32" => Some(PrimitiveType::U32),
221 "u64" => Some(PrimitiveType::U64),
222 "u128" => Some(PrimitiveType::U128),
223 "i8" => Some(PrimitiveType::I8),
224 "i16" => Some(PrimitiveType::I16),
225 "i32" => Some(PrimitiveType::I32),
226 "i64" => Some(PrimitiveType::I64),
227 "i128" => Some(PrimitiveType::I128),
228 "f32" => Some(PrimitiveType::F32),
229 "f64" => Some(PrimitiveType::F64),
230 "Uuid" => Some(PrimitiveType::Uuid),
231 "DateTime" => Some(PrimitiveType::DateTime),
232 "NaiveDate" => Some(PrimitiveType::NaiveDate),
233 "Value" => Some(PrimitiveType::JsonValue),
234 _ => None,
235 }
236}
237
238fn extract_single_generic_arg(segment: &syn::PathSegment) -> Option<Type> {
239 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
240 if let Some(syn::GenericArgument::Type(ty)) = args.args.first() {
241 return Some(ty.clone());
242 }
243 }
244 None
245}
246
247fn extract_double_generic_arg(segment: &syn::PathSegment) -> Option<(Type, Type)> {
248 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
249 let mut iter = args.args.iter();
250 if let (Some(syn::GenericArgument::Type(k)), Some(syn::GenericArgument::Type(v))) =
251 (iter.next(), iter.next())
252 {
253 return Some((k.clone(), v.clone()));
254 }
255 }
256 None
257}
258
259fn parse_variant(variant: &syn::Variant) -> syn::Result<VariantDef> {
260 let name = variant.ident.to_string();
261 let rename = get_rename(&variant.attrs);
262 let doc = extract_doc_comment(&variant.attrs);
263
264 let kind = match &variant.fields {
265 Fields::Unit => VariantKind::Unit,
266 Fields::Named(named) => {
267 let fields = named
268 .named
269 .iter()
270 .map(|f| {
271 let fname = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
272 let ty = parse_type(&f.ty);
273 let optional = matches!(&ty, TypeKind::Option(_));
274 let field_rename = get_rename(&f.attrs);
275 let skip = has_serde_skip(&f.attrs) || has_tw_attr(&f.attrs, "skip");
276 let fdoc = extract_doc_comment(&f.attrs);
277 let type_override = get_tw_type_override(&f.attrs);
278
279 FieldDef {
280 name: fname,
281 ty,
282 optional,
283 rename: field_rename,
284 doc: fdoc,
285 skip,
286 flatten: false,
287 type_override,
288 }
289 })
290 .collect();
291 VariantKind::Struct(fields)
292 }
293 Fields::Unnamed(unnamed) => {
294 let types: Vec<TypeKind> = unnamed.unnamed.iter().map(|f| parse_type(&f.ty)).collect();
295 VariantKind::Tuple(types)
296 }
297 };
298
299 Ok(VariantDef {
300 name,
301 rename,
302 kind,
303 doc,
304 })
305}
306
307fn parse_enum_repr(attrs: &[Attribute]) -> EnumRepr {
308 let mut tag = None;
309 let mut content = None;
310 let mut untagged = false;
311
312 for attr in attrs {
313 if !attr.path().is_ident("serde") {
314 continue;
315 }
316
317 let _ = attr.parse_nested_meta(|meta| {
318 if meta.path.is_ident("tag") {
319 let value = meta.value()?;
320 let s: syn::LitStr = value.parse()?;
321 tag = Some(s.value());
322 } else if meta.path.is_ident("content") {
323 let value = meta.value()?;
324 let s: syn::LitStr = value.parse()?;
325 content = Some(s.value());
326 } else if meta.path.is_ident("untagged") {
327 untagged = true;
328 } else if meta.path.is_ident("rename_all") {
329 let value = meta.value()?;
330 let _s: syn::LitStr = value.parse()?;
331 }
332 Ok(())
333 });
334 }
335
336 if untagged {
337 return EnumRepr::Untagged;
338 }
339
340 match (tag, content) {
341 (Some(t), Some(c)) => EnumRepr::Adjacent { tag: t, content: c },
342 (Some(t), None) => EnumRepr::Internal { tag: t },
343 _ => EnumRepr::External,
344 }
345}
346
347fn get_rename(attrs: &[Attribute]) -> Option<String> {
348 for attr in attrs {
349 if attr.path().is_ident("tw") {
350 let mut rename_val = None;
351 let _ = attr.parse_nested_meta(|meta| {
352 if meta.path.is_ident("rename") {
353 let value = meta.value()?;
354 let s: syn::LitStr = value.parse()?;
355 rename_val = Some(s.value());
356 }
357 Ok(())
358 });
359 if rename_val.is_some() {
360 return rename_val;
361 }
362 }
363 }
364
365 for attr in attrs {
366 if attr.path().is_ident("serde") {
367 let mut rename_val = None;
368 let _ = attr.parse_nested_meta(|meta| {
369 if meta.path.is_ident("rename") {
370 let value = meta.value()?;
371 let s: syn::LitStr = value.parse()?;
372 rename_val = Some(s.value());
373 }
374 Ok(())
375 });
376 if rename_val.is_some() {
377 return rename_val;
378 }
379 }
380 }
381
382 None
383}
384
385fn has_serde_skip(attrs: &[Attribute]) -> bool {
386 for attr in attrs {
387 if attr.path().is_ident("serde") {
388 let mut found = false;
389 let _ = attr.parse_nested_meta(|meta| {
390 if meta.path.is_ident("skip") || meta.path.is_ident("skip_serializing") {
391 found = true;
392 }
393 Ok(())
394 });
395 if found {
396 return true;
397 }
398 }
399 }
400 false
401}
402
403fn has_serde_flatten(attrs: &[Attribute]) -> bool {
404 for attr in attrs {
405 if attr.path().is_ident("serde") {
406 let mut found = false;
407 let _ = attr.parse_nested_meta(|meta| {
408 if meta.path.is_ident("flatten") {
409 found = true;
410 }
411 Ok(())
412 });
413 if found {
414 return true;
415 }
416 }
417 }
418 false
419}
420
421fn has_tw_attr(attrs: &[Attribute], attr_name: &str) -> bool {
422 for attr in attrs {
423 if attr.path().is_ident("tw") {
424 let mut found = false;
425 let _ = attr.parse_nested_meta(|meta| {
426 if meta.path.is_ident(attr_name) {
427 found = true;
428 }
429 Ok(())
430 });
431 if found {
432 return true;
433 }
434 }
435 }
436 false
437}
438
439fn get_tw_type_override(attrs: &[Attribute]) -> Option<String> {
440 for attr in attrs {
441 if attr.path().is_ident("tw") {
442 let mut type_val = None;
443 let _ = attr.parse_nested_meta(|meta| {
444 if meta.path.is_ident("type") {
445 let value = meta.value()?;
446 let s: syn::LitStr = value.parse()?;
447 type_val = Some(s.value());
448 }
449 Ok(())
450 });
451 if type_val.is_some() {
452 return type_val;
453 }
454 }
455 }
456 None
457}
458
459fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
460 let docs: Vec<String> = attrs
461 .iter()
462 .filter_map(|attr| {
463 if attr.path().is_ident("doc") {
464 if let Meta::NameValue(nv) = &attr.meta {
465 if let Expr::Lit(ExprLit {
466 lit: Lit::Str(s), ..
467 }) = &nv.value
468 {
469 return Some(s.value().trim().to_string());
470 }
471 }
472 }
473 None
474 })
475 .collect();
476
477 if docs.is_empty() {
478 None
479 } else {
480 Some(docs.join("\n"))
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487
488 #[test]
489 fn detects_typewriter_derive() {
490 let input: syn::DeriveInput = syn::parse_quote! {
491 #[derive(Debug, TypeWriter)]
492 #[sync_to(typescript)]
493 struct User { id: String }
494 };
495
496 assert!(has_typewriter_derive(&input.attrs));
497 }
498
499 #[test]
500 fn parses_sync_targets() {
501 let input: syn::DeriveInput = syn::parse_quote! {
502 #[derive(TypeWriter)]
503 #[sync_to(typescript, python)]
504 struct User { id: String }
505 };
506
507 let targets = parse_sync_to_attr(&input).unwrap();
508 assert_eq!(targets, vec![Language::TypeScript, Language::Python]);
509 }
510}