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 parse_tw_zod_attr(input: &DeriveInput) -> syn::Result<Option<bool>> {
78 let mut zod = None;
79
80 for attr in &input.attrs {
81 if !attr.path().is_ident("tw") {
82 continue;
83 }
84
85 attr.parse_nested_meta(|meta| {
86 if meta.path.is_ident("zod") {
87 if meta.input.is_empty() {
88 zod = Some(true);
89 } else {
90 let value = meta.value()?;
91 let enabled: syn::LitBool = value.parse()?;
92 zod = Some(enabled.value());
93 }
94 }
95 Ok(())
96 })?;
97 }
98
99 Ok(zod)
100}
101
102pub fn has_typewriter_derive(attrs: &[Attribute]) -> bool {
104 for attr in attrs {
105 if !attr.path().is_ident("derive") {
106 continue;
107 }
108
109 let mut found = false;
110 let _ = attr.parse_nested_meta(|meta| {
111 if meta
112 .path
113 .segments
114 .last()
115 .map(|s| s.ident == "TypeWriter")
116 .unwrap_or(false)
117 {
118 found = true;
119 }
120 Ok(())
121 });
122
123 if found {
124 return true;
125 }
126 }
127
128 false
129}
130
131fn parse_fields(fields: &Fields) -> syn::Result<Vec<FieldDef>> {
133 match fields {
134 Fields::Named(named) => named
135 .named
136 .iter()
137 .map(|f| {
138 let name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
139 let ty = parse_type(&f.ty);
140 let optional =
141 matches!(&ty, TypeKind::Option(_)) || has_tw_attr(&f.attrs, "optional");
142 let rename = get_rename(&f.attrs);
143 let skip = has_serde_skip(&f.attrs) || has_tw_attr(&f.attrs, "skip");
144 let flatten = has_serde_flatten(&f.attrs);
145 let doc = extract_doc_comment(&f.attrs);
146 let type_override = get_tw_type_override(&f.attrs);
147
148 Ok(FieldDef {
149 name,
150 ty,
151 optional,
152 rename,
153 doc,
154 skip,
155 flatten,
156 type_override,
157 })
158 })
159 .collect(),
160 Fields::Unnamed(_) | Fields::Unit => Ok(vec![]),
161 }
162}
163
164fn parse_type(ty: &Type) -> TypeKind {
166 match ty {
167 Type::Path(type_path) => {
168 let path = &type_path.path;
169
170 if let Some(segment) = path.segments.last() {
171 let ident = segment.ident.to_string();
172
173 match ident.as_str() {
174 "Option" => {
175 if let Some(inner) = extract_single_generic_arg(segment) {
176 return TypeKind::Option(Box::new(parse_type(&inner)));
177 }
178 }
179 "Vec" => {
180 if let Some(inner) = extract_single_generic_arg(segment) {
181 return TypeKind::Vec(Box::new(parse_type(&inner)));
182 }
183 }
184 "HashMap" | "BTreeMap" => {
185 if let Some((k, v)) = extract_double_generic_arg(segment) {
186 return TypeKind::HashMap(
187 Box::new(parse_type(&k)),
188 Box::new(parse_type(&v)),
189 );
190 }
191 }
192 "Box" | "Arc" | "Rc" => {
193 if let Some(inner) = extract_single_generic_arg(segment) {
194 return parse_type(&inner);
195 }
196 }
197 _ => {}
198 }
199
200 if let Some(prim) = map_primitive_name(&ident) {
201 return TypeKind::Primitive(prim);
202 }
203
204 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
205 let type_args: Vec<TypeKind> = args
206 .args
207 .iter()
208 .filter_map(|arg| {
209 if let syn::GenericArgument::Type(ty) = arg {
210 Some(parse_type(ty))
211 } else {
212 None
213 }
214 })
215 .collect();
216
217 if !type_args.is_empty() {
218 return TypeKind::Generic(ident, type_args);
219 }
220 }
221
222 TypeKind::Named(ident)
223 } else {
224 TypeKind::Unit
225 }
226 }
227 Type::Tuple(tuple) => {
228 if tuple.elems.is_empty() {
229 TypeKind::Unit
230 } else {
231 let elements: Vec<TypeKind> = tuple.elems.iter().map(parse_type).collect();
232 TypeKind::Tuple(elements)
233 }
234 }
235 Type::Reference(reference) => parse_type(&reference.elem),
236 _ => TypeKind::Unit,
237 }
238}
239
240fn map_primitive_name(name: &str) -> Option<PrimitiveType> {
241 match name {
242 "String" | "str" => Some(PrimitiveType::String),
243 "bool" => Some(PrimitiveType::Bool),
244 "u8" => Some(PrimitiveType::U8),
245 "u16" => Some(PrimitiveType::U16),
246 "u32" => Some(PrimitiveType::U32),
247 "u64" => Some(PrimitiveType::U64),
248 "u128" => Some(PrimitiveType::U128),
249 "i8" => Some(PrimitiveType::I8),
250 "i16" => Some(PrimitiveType::I16),
251 "i32" => Some(PrimitiveType::I32),
252 "i64" => Some(PrimitiveType::I64),
253 "i128" => Some(PrimitiveType::I128),
254 "f32" => Some(PrimitiveType::F32),
255 "f64" => Some(PrimitiveType::F64),
256 "Uuid" => Some(PrimitiveType::Uuid),
257 "DateTime" => Some(PrimitiveType::DateTime),
258 "NaiveDate" => Some(PrimitiveType::NaiveDate),
259 "Value" => Some(PrimitiveType::JsonValue),
260 _ => None,
261 }
262}
263
264fn extract_single_generic_arg(segment: &syn::PathSegment) -> Option<Type> {
265 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
266 if let Some(syn::GenericArgument::Type(ty)) = args.args.first() {
267 return Some(ty.clone());
268 }
269 }
270 None
271}
272
273fn extract_double_generic_arg(segment: &syn::PathSegment) -> Option<(Type, Type)> {
274 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
275 let mut iter = args.args.iter();
276 if let (Some(syn::GenericArgument::Type(k)), Some(syn::GenericArgument::Type(v))) =
277 (iter.next(), iter.next())
278 {
279 return Some((k.clone(), v.clone()));
280 }
281 }
282 None
283}
284
285fn parse_variant(variant: &syn::Variant) -> syn::Result<VariantDef> {
286 let name = variant.ident.to_string();
287 let rename = get_rename(&variant.attrs);
288 let doc = extract_doc_comment(&variant.attrs);
289
290 let kind = match &variant.fields {
291 Fields::Unit => VariantKind::Unit,
292 Fields::Named(named) => {
293 let fields = named
294 .named
295 .iter()
296 .map(|f| {
297 let fname = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
298 let ty = parse_type(&f.ty);
299 let optional = matches!(&ty, TypeKind::Option(_));
300 let field_rename = get_rename(&f.attrs);
301 let skip = has_serde_skip(&f.attrs) || has_tw_attr(&f.attrs, "skip");
302 let fdoc = extract_doc_comment(&f.attrs);
303 let type_override = get_tw_type_override(&f.attrs);
304
305 FieldDef {
306 name: fname,
307 ty,
308 optional,
309 rename: field_rename,
310 doc: fdoc,
311 skip,
312 flatten: false,
313 type_override,
314 }
315 })
316 .collect();
317 VariantKind::Struct(fields)
318 }
319 Fields::Unnamed(unnamed) => {
320 let types: Vec<TypeKind> = unnamed.unnamed.iter().map(|f| parse_type(&f.ty)).collect();
321 VariantKind::Tuple(types)
322 }
323 };
324
325 Ok(VariantDef {
326 name,
327 rename,
328 kind,
329 doc,
330 })
331}
332
333fn parse_enum_repr(attrs: &[Attribute]) -> EnumRepr {
334 let mut tag = None;
335 let mut content = None;
336 let mut untagged = false;
337
338 for attr in attrs {
339 if !attr.path().is_ident("serde") {
340 continue;
341 }
342
343 let _ = attr.parse_nested_meta(|meta| {
344 if meta.path.is_ident("tag") {
345 let value = meta.value()?;
346 let s: syn::LitStr = value.parse()?;
347 tag = Some(s.value());
348 } else if meta.path.is_ident("content") {
349 let value = meta.value()?;
350 let s: syn::LitStr = value.parse()?;
351 content = Some(s.value());
352 } else if meta.path.is_ident("untagged") {
353 untagged = true;
354 } else if meta.path.is_ident("rename_all") {
355 let value = meta.value()?;
356 let _s: syn::LitStr = value.parse()?;
357 }
358 Ok(())
359 });
360 }
361
362 if untagged {
363 return EnumRepr::Untagged;
364 }
365
366 match (tag, content) {
367 (Some(t), Some(c)) => EnumRepr::Adjacent { tag: t, content: c },
368 (Some(t), None) => EnumRepr::Internal { tag: t },
369 _ => EnumRepr::External,
370 }
371}
372
373fn get_rename(attrs: &[Attribute]) -> Option<String> {
374 for attr in attrs {
375 if attr.path().is_ident("tw") {
376 let mut rename_val = None;
377 let _ = attr.parse_nested_meta(|meta| {
378 if meta.path.is_ident("rename") {
379 let value = meta.value()?;
380 let s: syn::LitStr = value.parse()?;
381 rename_val = Some(s.value());
382 }
383 Ok(())
384 });
385 if rename_val.is_some() {
386 return rename_val;
387 }
388 }
389 }
390
391 for attr in attrs {
392 if attr.path().is_ident("serde") {
393 let mut rename_val = None;
394 let _ = attr.parse_nested_meta(|meta| {
395 if meta.path.is_ident("rename") {
396 let value = meta.value()?;
397 let s: syn::LitStr = value.parse()?;
398 rename_val = Some(s.value());
399 }
400 Ok(())
401 });
402 if rename_val.is_some() {
403 return rename_val;
404 }
405 }
406 }
407
408 None
409}
410
411fn has_serde_skip(attrs: &[Attribute]) -> bool {
412 for attr in attrs {
413 if attr.path().is_ident("serde") {
414 let mut found = false;
415 let _ = attr.parse_nested_meta(|meta| {
416 if meta.path.is_ident("skip") || meta.path.is_ident("skip_serializing") {
417 found = true;
418 }
419 Ok(())
420 });
421 if found {
422 return true;
423 }
424 }
425 }
426 false
427}
428
429fn has_serde_flatten(attrs: &[Attribute]) -> bool {
430 for attr in attrs {
431 if attr.path().is_ident("serde") {
432 let mut found = false;
433 let _ = attr.parse_nested_meta(|meta| {
434 if meta.path.is_ident("flatten") {
435 found = true;
436 }
437 Ok(())
438 });
439 if found {
440 return true;
441 }
442 }
443 }
444 false
445}
446
447fn has_tw_attr(attrs: &[Attribute], attr_name: &str) -> bool {
448 for attr in attrs {
449 if attr.path().is_ident("tw") {
450 let mut found = false;
451 let _ = attr.parse_nested_meta(|meta| {
452 if meta.path.is_ident(attr_name) {
453 found = true;
454 }
455 Ok(())
456 });
457 if found {
458 return true;
459 }
460 }
461 }
462 false
463}
464
465fn get_tw_type_override(attrs: &[Attribute]) -> Option<String> {
466 for attr in attrs {
467 if attr.path().is_ident("tw") {
468 let mut type_val = None;
469 let _ = attr.parse_nested_meta(|meta| {
470 if meta.path.is_ident("type") {
471 let value = meta.value()?;
472 let s: syn::LitStr = value.parse()?;
473 type_val = Some(s.value());
474 }
475 Ok(())
476 });
477 if type_val.is_some() {
478 return type_val;
479 }
480 }
481 }
482 None
483}
484
485fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
486 let docs: Vec<String> = attrs
487 .iter()
488 .filter_map(|attr| {
489 if attr.path().is_ident("doc") {
490 if let Meta::NameValue(nv) = &attr.meta {
491 if let Expr::Lit(ExprLit {
492 lit: Lit::Str(s), ..
493 }) = &nv.value
494 {
495 return Some(s.value().trim().to_string());
496 }
497 }
498 }
499 None
500 })
501 .collect();
502
503 if docs.is_empty() {
504 None
505 } else {
506 Some(docs.join("\n"))
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn detects_typewriter_derive() {
516 let input: syn::DeriveInput = syn::parse_quote! {
517 #[derive(Debug, TypeWriter)]
518 #[sync_to(typescript)]
519 struct User { id: String }
520 };
521
522 assert!(has_typewriter_derive(&input.attrs));
523 }
524
525 #[test]
526 fn parses_sync_targets() {
527 let input: syn::DeriveInput = syn::parse_quote! {
528 #[derive(TypeWriter)]
529 #[sync_to(typescript, python)]
530 struct User { id: String }
531 };
532
533 let targets = parse_sync_to_attr(&input).unwrap();
534 assert_eq!(targets, vec![Language::TypeScript, Language::Python]);
535 }
536
537 #[test]
538 fn parses_tw_zod_attr_absent() {
539 let input: syn::DeriveInput = syn::parse_quote! {
540 #[derive(TypeWriter)]
541 #[sync_to(typescript)]
542 struct User { id: String }
543 };
544
545 assert_eq!(parse_tw_zod_attr(&input).unwrap(), None);
546 }
547
548 #[test]
549 fn parses_tw_zod_attr_flag() {
550 let input: syn::DeriveInput = syn::parse_quote! {
551 #[derive(TypeWriter)]
552 #[sync_to(typescript)]
553 #[tw(zod)]
554 struct User { id: String }
555 };
556
557 assert_eq!(parse_tw_zod_attr(&input).unwrap(), Some(true));
558 }
559
560 #[test]
561 fn parses_tw_zod_attr_explicit_false() {
562 let input: syn::DeriveInput = syn::parse_quote! {
563 #[derive(TypeWriter)]
564 #[sync_to(typescript)]
565 #[tw(zod = false)]
566 struct User { id: String }
567 };
568
569 assert_eq!(parse_tw_zod_attr(&input).unwrap(), Some(false));
570 }
571
572 #[test]
573 fn rejects_invalid_tw_zod_attr() {
574 let input: syn::DeriveInput = syn::parse_quote! {
575 #[derive(TypeWriter)]
576 #[sync_to(typescript)]
577 #[tw(zod = "no")]
578 struct User { id: String }
579 };
580
581 assert!(parse_tw_zod_attr(&input).is_err());
582 }
583}