lust_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use syn::{
5    parse_macro_input, spanned::Spanned, Attribute, Data, DeriveInput, Fields, Generics, Ident,
6    Lit, LitStr, Path, Result as SynResult,
7};
8
9#[proc_macro_derive(LustStructView, attributes(lust))]
10pub fn derive_lust_struct_view(input: TokenStream) -> TokenStream {
11    match expand_lust_struct_view(parse_macro_input!(input as DeriveInput)) {
12        Ok(stream) => stream,
13        Err(err) => err.to_compile_error().into(),
14    }
15}
16
17fn expand_lust_struct_view(input: DeriveInput) -> SynResult<TokenStream> {
18    let data = match &input.data {
19        Data::Struct(data) => data,
20        _ => {
21            return Err(syn::Error::new(
22                input.ident.span(),
23                "LustStructView can only be derived for structs",
24            ))
25        }
26    };
27
28    let fields = match &data.fields {
29        Fields::Named(named) => &named.named,
30        Fields::Unnamed(_) | Fields::Unit => {
31            return Err(syn::Error::new(
32                data.fields.span(),
33                "LustStructView requires a struct with named fields",
34            ))
35        }
36    };
37
38    let mut type_name: Option<String> = None;
39    let mut crate_path = parse_path_literal("::lust", Span::call_site())?;
40    let mut lifetime = syn::parse_str::<syn::Lifetime>("'a").unwrap();
41
42    for attr in &input.attrs {
43        if !attr.path().is_ident("lust") {
44            continue;
45        }
46        for (key, lit) in parse_kv_attr(attr)? {
47            match key.as_str() {
48                "struct" | "type" => {
49                    let lit_str = expect_lit_str(&lit, "struct/type")?;
50                    type_name = Some(lit_str.value());
51                }
52                "crate" => {
53                    let lit_str = expect_lit_str(&lit, "crate")?;
54                    crate_path = parse_path_literal(&lit_str.value(), lit_str.span())?;
55                }
56                "lifetime" => {
57                    let lit_str = expect_lit_str(&lit, "lifetime")?;
58                    lifetime = syn::parse_str::<syn::Lifetime>(&lit_str.value()).map_err(|_| {
59                        syn::Error::new(
60                            lit_str.span(),
61                            "lifetime attribute must be a valid lifetime (e.g. \"'a\")",
62                        )
63                    })?;
64                }
65                other => {
66                    return Err(syn::Error::new(
67                        lit.span(),
68                        format!("unknown LustStructView attribute key '{}'", other),
69                    ));
70                }
71            }
72        }
73    }
74
75    let type_name = type_name.ok_or_else(|| {
76        syn::Error::new(
77            input.ident.span(),
78            "LustStructView derive requires #[lust(struct = \"module.Type\")]",
79        )
80    })?;
81
82    ensure_lifetime(&input.generics, &lifetime)?;
83
84    let field_inits = fields
85        .iter()
86        .map(|field| expand_field(field, &crate_path, &lifetime))
87        .collect::<SynResult<Vec<_>>>()?;
88
89    let struct_ident = &input.ident;
90    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
91    let lifetime_param = &lifetime;
92    let type_name_lit = LitStr::new(&type_name, Span::call_site());
93
94    Ok(TokenStream::from(quote! {
95        impl #impl_generics #crate_path::embed::LustStructView<#lifetime_param> for #struct_ident #ty_generics #where_clause {
96            const TYPE_NAME: &'static str = #type_name_lit;
97
98            fn from_handle(__handle: &#lifetime_param #crate_path::embed::StructHandle) -> #crate_path::Result<Self> {
99                __handle.ensure_type(Self::TYPE_NAME)?;
100                Ok(Self {
101                    #(#field_inits),*
102                })
103            }
104        }
105    }))
106}
107
108fn expand_field(
109    field: &syn::Field,
110    crate_path: &Path,
111    lifetime_param: &syn::Lifetime,
112) -> SynResult<proc_macro2::TokenStream> {
113    let ident = field
114        .ident
115        .clone()
116        .ok_or_else(|| syn::Error::new(field.span(), "expected named field"))?;
117    let mut field_name = ident.to_string();
118
119    for attr in &field.attrs {
120        if !attr.path().is_ident("lust") {
121            continue;
122        }
123
124        for (key, lit) in parse_kv_attr(attr)? {
125            match key.as_str() {
126                "field" | "name" => {
127                    let lit_str = expect_lit_str(&lit, "field/name")?;
128                    field_name = lit_str.value();
129                }
130                other => {
131                    return Err(syn::Error::new(
132                        lit.span(),
133                        format!("unknown LustStructView field attribute '{}'", other),
134                    ));
135                }
136            }
137        }
138    }
139
140    let field_ty = &field.ty;
141    let field_name_lit = LitStr::new(&field_name, ident.span());
142
143    Ok(quote! {
144        #ident: {
145            let __value = __handle.borrow_field(#field_name_lit)?;
146            <#field_ty as #crate_path::embed::FromStructField<#lifetime_param>>::from_value(#field_name_lit, __value)?
147        }
148    })
149}
150
151fn parse_kv_attr(attr: &Attribute) -> SynResult<Vec<(String, Lit)>> {
152    attr.parse_args_with(|input: syn::parse::ParseStream| {
153        let mut pairs = Vec::new();
154        while !input.is_empty() {
155            let key = if input.peek(syn::Token![type]) {
156                input.parse::<syn::Token![type]>()?;
157                "type".to_string()
158            } else if input.peek(syn::Token![struct]) {
159                input.parse::<syn::Token![struct]>()?;
160                "struct".to_string()
161            } else if input.peek(syn::Token![crate]) {
162                input.parse::<syn::Token![crate]>()?;
163                "crate".to_string()
164            } else {
165                let ident: Ident = input.parse()?;
166                ident.to_string()
167            };
168            input.parse::<syn::Token![=]>()?;
169            let value: Lit = input.parse()?;
170            pairs.push((key, value));
171            if input.peek(syn::Token![,]) {
172                input.parse::<syn::Token![,]>()?;
173            }
174        }
175        Ok(pairs)
176    })
177}
178
179fn expect_lit_str<'a>(lit: &'a Lit, context: &str) -> SynResult<&'a LitStr> {
180    match lit {
181        Lit::Str(s) => Ok(s),
182        _ => Err(syn::Error::new(
183            lit.span(),
184            format!("{context} attribute expects a string literal"),
185        )),
186    }
187}
188
189fn parse_path_literal(src: &str, span: Span) -> SynResult<Path> {
190    syn::parse_str(src)
191        .map_err(|_| syn::Error::new(span, format!("unable to parse '{}' as a path literal", src)))
192}
193
194fn ensure_lifetime(generics: &Generics, lifetime: &syn::Lifetime) -> SynResult<()> {
195    let expected = lifetime.ident.to_string();
196    let found = generics
197        .lifetimes()
198        .any(|lt| lt.lifetime.ident == lifetime.ident);
199    if found {
200        Ok(())
201    } else {
202        Err(syn::Error::new(
203            generics.span(),
204            format!(
205                "LustStructView derive expects the struct to declare lifetime {}",
206                expected
207            ),
208        ))
209    }
210}