Skip to main content

aam_derive/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::{format_ident, quote};
4use syn::{
5    Data, DeriveInput, Field, Fields, Lit, LitStr, Meta, Token, Type, parse_macro_input,
6    punctuated::Punctuated,
7};
8
9/// Returns the path prefix for generated code (`::aam_rs` or `::aam_core`).
10///
11/// This lets the macros work both when invoked through the `aam-rs` facade and
12/// when `aam-core` + `aam-derive` are used directly.
13fn aam_root() -> proc_macro2::TokenStream {
14    let found = proc_macro_crate::crate_name("aam-rs")
15        .or_else(|_| proc_macro_crate::crate_name("aam-core"));
16
17    let crate_name = match found {
18        Ok(proc_macro_crate::FoundCrate::Name(name)) => name,
19        Ok(proc_macro_crate::FoundCrate::Itself) => "aam_core".to_string(),
20        Err(_) => "aam_core".to_string(),
21    };
22
23    let ident = proc_macro2::Ident::new(&crate_name, Span::call_site());
24    quote!(::#ident)
25}
26
27#[proc_macro_derive(FromAam, attributes(aam))]
28pub fn derive_from_aam(input: TokenStream) -> TokenStream {
29    let input = parse_macro_input!(input as DeriveInput);
30    let name = &input.ident;
31
32    let fields = match &input.data {
33        Data::Struct(data) => match &data.fields {
34            Fields::Named(fields) => &fields.named,
35            Fields::Unnamed(_) => {
36                return syn::Error::new_spanned(&input, "FromAam does not support tuple structs")
37                    .to_compile_error()
38                    .into();
39            }
40            Fields::Unit => {
41                return syn::Error::new_spanned(&input, "FromAam does not support unit structs")
42                    .to_compile_error()
43                    .into();
44            }
45        },
46        _ => {
47            return syn::Error::new_spanned(&input, "FromAam only supports structs")
48                .to_compile_error()
49                .into();
50        }
51    };
52
53    let field_parsers = fields.iter().map(generate_field_parser);
54
55    let root = aam_root();
56    let expanded = quote! {
57        impl #root::from_aam::FromAam for #name {
58            fn from_aam_str(value: &str) -> ::std::result::Result<Self, #root::error::AamlError> {
59                let fields = #root::from_aam::parse_fields(value)?;
60                Ok(Self {
61                    #(#field_parsers)*
62                })
63            }
64        }
65    };
66
67    TokenStream::from(expanded)
68}
69
70fn generate_field_parser(field: &Field) -> proc_macro2::TokenStream {
71    let field_ident = field.ident.as_ref().expect("named field");
72    let ty = &field.ty;
73    let root = aam_root();
74
75    let aam_name = get_aam_name(field);
76    let is_optional = is_option_type(ty);
77
78    let aam_name_lit = LitStr::new(&aam_name, field_ident.span());
79
80    if is_optional {
81        let inner_ty = extract_option_inner(ty);
82        quote! {
83            #field_ident: match fields.get(#aam_name_lit) {
84                Some(v) => Some(<#inner_ty as #root::from_aam::FromAam>::from_aam_str(v)?),
85                None => None,
86            },
87        }
88    } else if is_vec_type(ty) {
89        let inner_ty = extract_vec_inner(ty);
90        quote! {
91            #field_ident: match fields.get(#aam_name_lit) {
92                Some(v) => <::std::vec::Vec<#inner_ty> as #root::from_aam::FromAam>::from_aam_str(v)?,
93                None => ::std::vec::Vec::new(),
94            },
95        }
96    } else {
97        quote! {
98            #field_ident: match fields.get(#aam_name_lit) {
99                Some(v) => <#ty as #root::from_aam::FromAam>::from_aam_str(v)?,
100                None => return ::std::result::Result::Err(#root::error::AamlError::NotFound {
101                    key: #aam_name_lit.to_string(),
102                    context: "inline object fields".to_string(),
103                    diagnostics: None,
104                }),
105            },
106        }
107    }
108}
109
110fn get_aam_name(field: &Field) -> String {
111    let raw = field.ident.as_ref().unwrap().to_string();
112    for attr in &field.attrs {
113        if !attr.path().is_ident("aam") {
114            continue;
115        }
116        if let Meta::List(list) = &attr.meta {
117            let parsed: Punctuated<Meta, Token![,]> = list
118                .parse_args_with(Punctuated::parse_terminated)
119                .unwrap_or_default();
120            for meta in &parsed {
121                if let Meta::NameValue(nv) = meta
122                    && nv.path.is_ident("rename")
123                    && let syn::Expr::Lit(lit) = &nv.value
124                    && let Lit::Str(s) = &lit.lit
125                {
126                    return s.value();
127                }
128            }
129        }
130    }
131    raw.strip_prefix("r#").unwrap_or(&raw).to_string()
132}
133
134fn is_option_type(ty: &Type) -> bool {
135    if let Type::Path(type_path) = ty
136        && let Some(seg) = type_path.path.segments.last()
137    {
138        return seg.ident == "Option";
139    }
140    false
141}
142
143fn extract_type_inner<'a>(ty: &'a Type, target_ident: &str) -> &'a Type {
144    if let Type::Path(type_path) = ty
145        && let Some(seg) = type_path.path.segments.last()
146        && seg.ident == target_ident
147        && let syn::PathArguments::AngleBracketed(args) = &seg.arguments
148        && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
149    {
150        return inner;
151    }
152    ty
153}
154
155fn extract_option_inner(ty: &Type) -> &Type {
156    extract_type_inner(ty, "Option")
157}
158
159fn is_vec_type(ty: &Type) -> bool {
160    if let Type::Path(type_path) = ty
161        && let Some(seg) = type_path.path.segments.last()
162    {
163        return seg.ident == "Vec";
164    }
165    false
166}
167
168fn extract_vec_inner(ty: &Type) -> &Type {
169    extract_type_inner(ty, "Vec")
170}
171
172fn generate_schema_field_parser(
173    field_name: &str,
174    type_name: &str,
175    optional: bool,
176) -> proc_macro2::TokenStream {
177    let root = aam_root();
178    let field_ident = sanitize_ident(field_name);
179    let aam_name_lit = LitStr::new(field_name, field_ident.span());
180
181    // Inner type without the Option wrapper.
182    let base_type_name = if optional {
183        type_name.strip_suffix('*').unwrap_or(type_name)
184    } else {
185        type_name
186    };
187
188    let is_list = base_type_name.starts_with("list<") && base_type_name.ends_with('>');
189    let inner_list_ty = if is_list {
190        let inner = base_type_name
191            .strip_prefix("list<")
192            .and_then(|s| s.strip_suffix('>'))
193            .unwrap_or(base_type_name)
194            .trim();
195        Some(aam_type_to_rust_type(inner, false))
196    } else {
197        None
198    };
199
200    let base_parser = if let Some(inner_ty) = inner_list_ty {
201        quote! {
202            <::std::vec::Vec<#inner_ty> as #root::from_aam::FromAam>::from_aam_str(v)?
203        }
204    } else {
205        let rust_ty = aam_type_to_rust_type(base_type_name, false);
206        quote! { <#rust_ty as #root::from_aam::FromAam>::from_aam_str(v)? }
207    };
208
209    let value_parser = if optional {
210        quote! {
211            match fields.get(#aam_name_lit) {
212                Some(v) if !v.trim().is_empty() => Some(#base_parser),
213                _ => None,
214            }
215        }
216    } else if is_list {
217        quote! {
218            match fields.get(#aam_name_lit) {
219                Some(v) => <::std::vec::Vec<_> as #root::from_aam::FromAam>::from_aam_str(v)?,
220                None => ::std::vec::Vec::new(),
221            }
222        }
223    } else {
224        quote! {
225            match fields.get(#aam_name_lit) {
226                Some(v) => #base_parser,
227                None => return ::std::result::Result::Err(#root::error::AamlError::NotFound {
228                    key: #aam_name_lit.to_string(),
229                    context: "inline object fields".to_string(),
230                    diagnostics: None,
231                }),
232            }
233        }
234    };
235
236    quote! { #field_ident: #value_parser, }
237}
238
239#[proc_macro]
240pub fn schema_to_struct(input: TokenStream) -> TokenStream {
241    let schema_str = parse_macro_input!(input as LitStr);
242    let schema = schema_str.value();
243
244    let parsed = match parse_schema_def(&schema) {
245        Ok(p) => p,
246        Err(e) => return e.to_compile_error().into(),
247    };
248
249    let struct_name = format_ident!("{}", parsed.name);
250    let struct_fields: Vec<_> = parsed
251        .fields
252        .iter()
253        .map(|sf| {
254            let field_ident = sanitize_ident(&sf.name);
255            let rust_ty = aam_type_to_rust_type(&sf.type_name, sf.optional);
256            quote! { pub #field_ident: #rust_ty }
257        })
258        .collect();
259
260    let root = aam_root();
261    let field_parsers: Vec<_> = parsed
262        .fields
263        .iter()
264        .map(|sf| generate_schema_field_parser(&sf.name, &sf.type_name, sf.optional))
265        .collect();
266
267    let expanded = quote! {
268        #[derive(Debug, Clone)]
269        pub struct #struct_name {
270            #(#struct_fields,)*
271        }
272
273        impl #root::from_aam::FromAam for #struct_name {
274            fn from_aam_str(value: &str) -> ::std::result::Result<Self, #root::error::AamlError> {
275                let fields = #root::from_aam::parse_fields(value)?;
276                Ok(Self {
277                    #(#field_parsers)*
278                })
279            }
280        }
281    };
282
283    TokenStream::from(expanded)
284}
285
286fn sanitize_ident(name: &str) -> proc_macro2::Ident {
287    if is_rust_keyword(name) {
288        format_ident!("r#{}", name)
289    } else {
290        format_ident!("{}", name)
291    }
292}
293
294fn is_rust_keyword(s: &str) -> bool {
295    matches!(
296        s,
297        "as" | "break"
298            | "const"
299            | "continue"
300            | "crate"
301            | "else"
302            | "enum"
303            | "extern"
304            | "false"
305            | "fn"
306            | "for"
307            | "if"
308            | "impl"
309            | "in"
310            | "let"
311            | "loop"
312            | "match"
313            | "mod"
314            | "move"
315            | "mut"
316            | "pub"
317            | "ref"
318            | "return"
319            | "self"
320            | "Self"
321            | "static"
322            | "struct"
323            | "super"
324            | "trait"
325            | "true"
326            | "type"
327            | "unsafe"
328            | "use"
329            | "where"
330            | "while"
331            | "abstract"
332            | "become"
333            | "box"
334            | "do"
335            | "final"
336            | "macro"
337            | "override"
338            | "priv"
339            | "typeof"
340            | "unsized"
341            | "virtual"
342            | "yield"
343            | "async"
344            | "await"
345            | "dyn"
346    )
347}
348
349struct SchemaField {
350    name: String,
351    type_name: String,
352    optional: bool,
353}
354
355struct ParsedSchema {
356    name: String,
357    fields: Vec<SchemaField>,
358}
359
360fn strip_schema_prefix(input: &str) -> Result<&str, syn::Error> {
361    input
362        .strip_prefix("@schema")
363        .ok_or_else(|| syn::Error::new(proc_macro2::Span::call_site(), "expected '@schema'"))
364        .map(str::trim)
365}
366
367fn split_schema_name_body(input: &str) -> Result<(&str, &str), syn::Error> {
368    input.split_once('{').ok_or_else(|| {
369        syn::Error::new(
370            proc_macro2::Span::call_site(),
371            "expected '{' after schema name",
372        )
373    })
374}
375
376fn extract_schema_body(body_part: &str) -> Result<&str, syn::Error> {
377    body_part
378        .rsplit_once('}')
379        .ok_or_else(|| {
380            syn::Error::new(
381                proc_macro2::Span::call_site(),
382                "expected '}' to close schema",
383            )
384        })
385        .map(|(body, _)| body)
386}
387
388fn parse_schema_field(
389    token: &str,
390    next_token: Option<&str>,
391) -> Result<(SchemaField, usize), syn::Error> {
392    let (field_raw, ty_part) = token.split_once(':').ok_or_else(|| {
393        syn::Error::new(
394            proc_macro2::Span::call_site(),
395            format!("bad field '{token}' — missing ':' separator"),
396        )
397    })?;
398
399    let (ty, consumed) = if ty_part.is_empty() {
400        (
401            next_token.ok_or_else(|| {
402                syn::Error::new(
403                    proc_macro2::Span::call_site(),
404                    format!("field '{field_raw}:' has no type specified"),
405                )
406            })?,
407            2,
408        )
409    } else {
410        (ty_part, 1)
411    };
412
413    let optional = field_raw.ends_with('*');
414    let field_name = if optional {
415        field_raw.trim_end_matches('*')
416    } else {
417        field_raw
418    };
419
420    if field_name.is_empty() || ty.is_empty() {
421        return Err(syn::Error::new(
422            proc_macro2::Span::call_site(),
423            format!("bad field '{field_name}:{ty}' — empty name or type"),
424        ));
425    }
426
427    Ok((
428        SchemaField {
429            name: field_name.to_string(),
430            type_name: ty.to_string(),
431            optional,
432        },
433        consumed,
434    ))
435}
436
437fn parse_schema_def(input: &str) -> Result<ParsedSchema, syn::Error> {
438    let input = input.trim();
439    let after_prefix = strip_schema_prefix(input)?;
440    let (name_part, body_part) = split_schema_name_body(after_prefix)?;
441    let name = name_part.trim().to_string();
442
443    if name.is_empty() {
444        return Err(syn::Error::new(
445            proc_macro2::Span::call_site(),
446            "schema name is empty",
447        ));
448    }
449
450    let body = extract_schema_body(body_part)?;
451    let normalized = body.replace(',', " ");
452    let tokens: Vec<&str> = normalized.split_whitespace().collect();
453    let mut fields = Vec::new();
454
455    let mut i = 0;
456    while i < tokens.len() {
457        let next = if i + 1 < tokens.len() {
458            Some(tokens[i + 1])
459        } else {
460            None
461        };
462        let (field, consumed) = parse_schema_field(tokens[i], next)?;
463        fields.push(field);
464        i += consumed;
465    }
466
467    Ok(ParsedSchema { name, fields })
468}
469
470fn aam_type_to_rust_type(type_name: &str, optional: bool) -> proc_macro2::TokenStream {
471    let base = match type_name {
472        "i8" => quote! { i8 },
473        "i16" => quote! { i16 },
474        "i32" => quote! { i32 },
475        "i64" => quote! { i64 },
476        "u8" => quote! { u8 },
477        "u16" => quote! { u16 },
478        "u32" => quote! { u32 },
479        "u64" => quote! { u64 },
480        "f32" => quote! { f32 },
481        "f64" => quote! { f64 },
482        "string" => quote! { String },
483        "bool" => quote! { bool },
484        "color" => quote! { String },
485        "math::vector2" | "math::vector3" | "math::vector4" | "math::quaternion"
486        | "math::matrix3x3" | "math::matrix4x4" => quote! { String },
487        "physics::kilogram" | "physics::meter" => quote! { f64 },
488        "time::datetime" | "time::duration" | "time::year" | "time::day" | "time::hour"
489        | "time::minute" => quote! { String },
490        _ => {
491            if let Some(inner) = type_name
492                .strip_prefix("list<")
493                .and_then(|s| s.strip_suffix('>'))
494            {
495                let inner_ty = aam_type_to_rust_type(inner.trim(), false);
496                return if optional {
497                    quote! { ::std::option::Option<::std::vec::Vec<#inner_ty>> }
498                } else {
499                    quote! { ::std::vec::Vec<#inner_ty> }
500                };
501            }
502            let ident = format_ident!("{}", type_name);
503            quote! { #ident }
504        }
505    };
506
507    if optional {
508        let ident = format_ident!("{}", "Option");
509        quote! { #ident<#base> }
510    } else {
511        base
512    }
513}