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
9fn 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 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}