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/// Detect if item attributes include `#[derive(... TypeWriter ...)]`.
77pub 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
105/// Parse struct/variant fields into `Vec<FieldDef>`.
106fn 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
138/// Parse a `syn::Type` into a `TypeKind`.
139fn 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}