Skip to main content

typewriter_engine/
parser.rs

1//! Parser: converts Rust AST (`syn`) into typewriter IR (`TypeDef`).
2
3use syn::{Attribute, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, Type};
4use typewriter_core::ir::*;
5
6/// Parse a `syn::DeriveInput` into a `TypeDef`.
7pub 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
48/// Parse the `#[sync_to(typescript, python, ...)]` attribute.
49pub 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
76/// Parse optional type-level Zod override from tw(zod) or tw(zod = false).
77pub 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
102/// Detect if item attributes include `#[derive(... TypeWriter ...)]`.
103pub 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
131/// Parse struct/variant fields into `Vec<FieldDef>`.
132fn 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
164/// Parse a `syn::Type` into a `TypeKind`.
165fn 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}