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}