use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{format_ident, quote};
use syn::{
Data, DeriveInput, Field, Fields, Lit, LitStr, Meta, Token, Type, parse_macro_input,
punctuated::Punctuated,
};
fn aam_root() -> proc_macro2::TokenStream {
let found = proc_macro_crate::crate_name("aam-rs")
.or_else(|_| proc_macro_crate::crate_name("aam-core"));
let crate_name = match found {
Ok(proc_macro_crate::FoundCrate::Name(name)) => name,
Ok(proc_macro_crate::FoundCrate::Itself) => "aam_core".to_string(),
Err(_) => "aam_core".to_string(),
};
let ident = proc_macro2::Ident::new(&crate_name, Span::call_site());
quote!(::#ident)
}
#[proc_macro_derive(FromAam, attributes(aam))]
pub fn derive_from_aam(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
Fields::Unnamed(_) => {
return syn::Error::new_spanned(&input, "FromAam does not support tuple structs")
.to_compile_error()
.into();
}
Fields::Unit => {
return syn::Error::new_spanned(&input, "FromAam does not support unit structs")
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(&input, "FromAam only supports structs")
.to_compile_error()
.into();
}
};
let field_parsers = fields.iter().map(generate_field_parser);
let root = aam_root();
let expanded = quote! {
impl #root::from_aam::FromAam for #name {
fn from_aam_str(value: &str) -> ::std::result::Result<Self, #root::error::AamlError> {
let fields = #root::from_aam::parse_fields(value)?;
Ok(Self {
#(#field_parsers)*
})
}
}
};
TokenStream::from(expanded)
}
fn generate_field_parser(field: &Field) -> proc_macro2::TokenStream {
let field_ident = field.ident.as_ref().expect("named field");
let ty = &field.ty;
let root = aam_root();
let aam_name = get_aam_name(field);
let is_optional = is_option_type(ty);
let aam_name_lit = LitStr::new(&aam_name, field_ident.span());
if is_optional {
let inner_ty = extract_option_inner(ty);
quote! {
#field_ident: match fields.get(#aam_name_lit) {
Some(v) => Some(<#inner_ty as #root::from_aam::FromAam>::from_aam_str(v)?),
None => None,
},
}
} else if is_vec_type(ty) {
let inner_ty = extract_vec_inner(ty);
quote! {
#field_ident: match fields.get(#aam_name_lit) {
Some(v) => <::std::vec::Vec<#inner_ty> as #root::from_aam::FromAam>::from_aam_str(v)?,
None => ::std::vec::Vec::new(),
},
}
} else {
quote! {
#field_ident: match fields.get(#aam_name_lit) {
Some(v) => <#ty as #root::from_aam::FromAam>::from_aam_str(v)?,
None => return ::std::result::Result::Err(#root::error::AamlError::NotFound {
key: #aam_name_lit.to_string(),
context: "inline object fields".to_string(),
diagnostics: None,
}),
},
}
}
}
fn get_aam_name(field: &Field) -> String {
let raw = field.ident.as_ref().unwrap().to_string();
for attr in &field.attrs {
if !attr.path().is_ident("aam") {
continue;
}
if let Meta::List(list) = &attr.meta {
let parsed: Punctuated<Meta, Token![,]> = list
.parse_args_with(Punctuated::parse_terminated)
.unwrap_or_default();
for meta in &parsed {
if let Meta::NameValue(nv) = meta
&& nv.path.is_ident("rename")
&& let syn::Expr::Lit(lit) = &nv.value
&& let Lit::Str(s) = &lit.lit
{
return s.value();
}
}
}
}
raw.strip_prefix("r#").unwrap_or(&raw).to_string()
}
fn is_option_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty
&& let Some(seg) = type_path.path.segments.last()
{
return seg.ident == "Option";
}
false
}
fn extract_type_inner<'a>(ty: &'a Type, target_ident: &str) -> &'a Type {
if let Type::Path(type_path) = ty
&& let Some(seg) = type_path.path.segments.last()
&& seg.ident == target_ident
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
&& let Some(syn::GenericArgument::Type(inner)) = args.args.first()
{
return inner;
}
ty
}
fn extract_option_inner(ty: &Type) -> &Type {
extract_type_inner(ty, "Option")
}
fn is_vec_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty
&& let Some(seg) = type_path.path.segments.last()
{
return seg.ident == "Vec";
}
false
}
fn extract_vec_inner(ty: &Type) -> &Type {
extract_type_inner(ty, "Vec")
}
fn generate_schema_field_parser(
field_name: &str,
type_name: &str,
optional: bool,
) -> proc_macro2::TokenStream {
let root = aam_root();
let field_ident = sanitize_ident(field_name);
let aam_name_lit = LitStr::new(field_name, field_ident.span());
let base_type_name = if optional {
type_name.strip_suffix('*').unwrap_or(type_name)
} else {
type_name
};
let is_list = base_type_name.starts_with("list<") && base_type_name.ends_with('>');
let inner_list_ty = if is_list {
let inner = base_type_name
.strip_prefix("list<")
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(base_type_name)
.trim();
Some(aam_type_to_rust_type(inner, false))
} else {
None
};
let base_parser = if let Some(inner_ty) = inner_list_ty {
quote! {
<::std::vec::Vec<#inner_ty> as #root::from_aam::FromAam>::from_aam_str(v)?
}
} else {
let rust_ty = aam_type_to_rust_type(base_type_name, false);
quote! { <#rust_ty as #root::from_aam::FromAam>::from_aam_str(v)? }
};
let value_parser = if optional {
quote! {
match fields.get(#aam_name_lit) {
Some(v) if !v.trim().is_empty() => Some(#base_parser),
_ => None,
}
}
} else if is_list {
quote! {
match fields.get(#aam_name_lit) {
Some(v) => <::std::vec::Vec<_> as #root::from_aam::FromAam>::from_aam_str(v)?,
None => ::std::vec::Vec::new(),
}
}
} else {
quote! {
match fields.get(#aam_name_lit) {
Some(v) => #base_parser,
None => return ::std::result::Result::Err(#root::error::AamlError::NotFound {
key: #aam_name_lit.to_string(),
context: "inline object fields".to_string(),
diagnostics: None,
}),
}
}
};
quote! { #field_ident: #value_parser, }
}
#[proc_macro]
pub fn schema_to_struct(input: TokenStream) -> TokenStream {
let schema_str = parse_macro_input!(input as LitStr);
let schema = schema_str.value();
let parsed = match parse_schema_def(&schema) {
Ok(p) => p,
Err(e) => return e.to_compile_error().into(),
};
let struct_name = format_ident!("{}", parsed.name);
let struct_fields: Vec<_> = parsed
.fields
.iter()
.map(|sf| {
let field_ident = sanitize_ident(&sf.name);
let rust_ty = aam_type_to_rust_type(&sf.type_name, sf.optional);
quote! { pub #field_ident: #rust_ty }
})
.collect();
let root = aam_root();
let field_parsers: Vec<_> = parsed
.fields
.iter()
.map(|sf| generate_schema_field_parser(&sf.name, &sf.type_name, sf.optional))
.collect();
let expanded = quote! {
#[derive(Debug, Clone)]
pub struct #struct_name {
#(#struct_fields,)*
}
impl #root::from_aam::FromAam for #struct_name {
fn from_aam_str(value: &str) -> ::std::result::Result<Self, #root::error::AamlError> {
let fields = #root::from_aam::parse_fields(value)?;
Ok(Self {
#(#field_parsers)*
})
}
}
};
TokenStream::from(expanded)
}
fn sanitize_ident(name: &str) -> proc_macro2::Ident {
if is_rust_keyword(name) {
format_ident!("r#{}", name)
} else {
format_ident!("{}", name)
}
}
fn is_rust_keyword(s: &str) -> bool {
matches!(
s,
"as" | "break"
| "const"
| "continue"
| "crate"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "Self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
| "abstract"
| "become"
| "box"
| "do"
| "final"
| "macro"
| "override"
| "priv"
| "typeof"
| "unsized"
| "virtual"
| "yield"
| "async"
| "await"
| "dyn"
)
}
struct SchemaField {
name: String,
type_name: String,
optional: bool,
}
struct ParsedSchema {
name: String,
fields: Vec<SchemaField>,
}
fn strip_schema_prefix(input: &str) -> Result<&str, syn::Error> {
input
.strip_prefix("@schema")
.ok_or_else(|| syn::Error::new(proc_macro2::Span::call_site(), "expected '@schema'"))
.map(str::trim)
}
fn split_schema_name_body(input: &str) -> Result<(&str, &str), syn::Error> {
input.split_once('{').ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
"expected '{' after schema name",
)
})
}
fn extract_schema_body(body_part: &str) -> Result<&str, syn::Error> {
body_part
.rsplit_once('}')
.ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
"expected '}' to close schema",
)
})
.map(|(body, _)| body)
}
fn parse_schema_field(
token: &str,
next_token: Option<&str>,
) -> Result<(SchemaField, usize), syn::Error> {
let (field_raw, ty_part) = token.split_once(':').ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!("bad field '{token}' — missing ':' separator"),
)
})?;
let (ty, consumed) = if ty_part.is_empty() {
(
next_token.ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!("field '{field_raw}:' has no type specified"),
)
})?,
2,
)
} else {
(ty_part, 1)
};
let optional = field_raw.ends_with('*');
let field_name = if optional {
field_raw.trim_end_matches('*')
} else {
field_raw
};
if field_name.is_empty() || ty.is_empty() {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!("bad field '{field_name}:{ty}' — empty name or type"),
));
}
Ok((
SchemaField {
name: field_name.to_string(),
type_name: ty.to_string(),
optional,
},
consumed,
))
}
fn parse_schema_def(input: &str) -> Result<ParsedSchema, syn::Error> {
let input = input.trim();
let after_prefix = strip_schema_prefix(input)?;
let (name_part, body_part) = split_schema_name_body(after_prefix)?;
let name = name_part.trim().to_string();
if name.is_empty() {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"schema name is empty",
));
}
let body = extract_schema_body(body_part)?;
let normalized = body.replace(',', " ");
let tokens: Vec<&str> = normalized.split_whitespace().collect();
let mut fields = Vec::new();
let mut i = 0;
while i < tokens.len() {
let next = if i + 1 < tokens.len() {
Some(tokens[i + 1])
} else {
None
};
let (field, consumed) = parse_schema_field(tokens[i], next)?;
fields.push(field);
i += consumed;
}
Ok(ParsedSchema { name, fields })
}
fn aam_type_to_rust_type(type_name: &str, optional: bool) -> proc_macro2::TokenStream {
let base = match type_name {
"i8" => quote! { i8 },
"i16" => quote! { i16 },
"i32" => quote! { i32 },
"i64" => quote! { i64 },
"u8" => quote! { u8 },
"u16" => quote! { u16 },
"u32" => quote! { u32 },
"u64" => quote! { u64 },
"f32" => quote! { f32 },
"f64" => quote! { f64 },
"string" => quote! { String },
"bool" => quote! { bool },
"color" => quote! { String },
"math::vector2" | "math::vector3" | "math::vector4" | "math::quaternion"
| "math::matrix3x3" | "math::matrix4x4" => quote! { String },
"physics::kilogram" | "physics::meter" => quote! { f64 },
"time::datetime" | "time::duration" | "time::year" | "time::day" | "time::hour"
| "time::minute" => quote! { String },
_ => {
if let Some(inner) = type_name
.strip_prefix("list<")
.and_then(|s| s.strip_suffix('>'))
{
let inner_ty = aam_type_to_rust_type(inner.trim(), false);
return if optional {
quote! { ::std::option::Option<::std::vec::Vec<#inner_ty>> }
} else {
quote! { ::std::vec::Vec<#inner_ty> }
};
}
let ident = format_ident!("{}", type_name);
quote! { #ident }
}
};
if optional {
let ident = format_ident!("{}", "Option");
quote! { #ident<#base> }
} else {
base
}
}