wowlab_engine_macros_impl/
lower.rs1use quote::quote;
2use syn::{Data, DeriveInput, Fields, Ident, Path, PathArguments};
3
4use crate::lower_parse::{LowerFieldMode, parse_lower_container, parse_lower_field_mode};
5
6struct FieldSpec {
7 ident: Ident,
8 mode: LowerFieldMode,
9}
10
11pub(crate) fn expand_lower_data(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
12 let Data::Struct(data_struct) = &input.data else {
13 return Err(syn::Error::new_spanned(
14 &input.ident,
15 "#[derive(LowerData)] can only be applied to structs",
16 ));
17 };
18
19 let Fields::Named(named) = &data_struct.fields else {
20 return Err(syn::Error::new_spanned(
21 &input.ident,
22 "#[derive(LowerData)] requires a struct with named fields",
23 ));
24 };
25
26 let def_ident = &input.ident;
27 let container = parse_lower_container(input)?;
28 let field_specs: Vec<FieldSpec> = named
29 .named
30 .iter()
31 .map(|field| {
32 let ident = field
33 .ident
34 .clone()
35 .ok_or_else(|| syn::Error::new_spanned(field, "field must be named"))?;
36 let mode = parse_lower_field_mode(field)?;
37
38 Ok(FieldSpec { ident, mode })
39 })
40 .collect::<syn::Result<_>>()?;
41
42 reject_duplicate_data_initializers(&field_specs, &container.extra)?;
43
44 let data_ty = &container.data;
45 let data_expr = expression_path(data_ty);
46 let ctx_ty = &container.ctx;
47 let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
48 let data_inits = data_initializers(&field_specs, &container.extra);
49
50 let into_data = quote! {
51 impl #impl_generics #def_ident #type_generics #where_clause {
52 pub(crate) fn into_data(&self, ctx: &mut #ctx_ty) -> #data_ty {
56 #data_expr {
57 #(#data_inits,)*
58 }
59 }
60 }
61 };
62 let into_info = container
63 .info
64 .as_ref()
65 .map_or_else(proc_macro2::TokenStream::new, |info| {
66 info_impl(input, data_ty, info)
67 });
68 let finalize = container.finalize.as_ref().map_or_else(
69 || Ok(proc_macro2::TokenStream::new()),
70 |finalize| finalize_impl(input, &field_specs, finalize),
71 )?;
72
73 Ok(quote! {
74 #into_data
75 #into_info
76 #finalize
77 })
78}
79
80fn data_initializers(
81 fields: &[FieldSpec],
82 extra: &[crate::lower_parse::LowerNamedFn],
83) -> Vec<proc_macro2::TokenStream> {
84 let field_initializers = fields.iter().filter_map(|spec| {
85 let ident = &spec.ident;
86
87 match &spec.mode {
88 LowerFieldMode::Copy => Some(quote!(#ident: self.#ident)),
89 LowerFieldMode::Skip => None,
90 LowerFieldMode::NameIdx(resolve) => Some(quote!(name_idx: #resolve(&self.#ident, ctx))),
91 LowerFieldMode::Convert(func) => Some(quote!(#ident: #func(self, ctx))),
92 }
93 });
94 let extra_initializers = extra.iter().map(|entry| {
95 let field = &entry.field;
96 let func = &entry.func;
97
98 quote!(#field: #func(self, ctx))
99 });
100
101 field_initializers.chain(extra_initializers).collect()
102}
103
104fn info_impl(
105 input: &DeriveInput,
106 data_ty: &Path,
107 info: &crate::lower_parse::LowerInfoSpec,
108) -> proc_macro2::TokenStream {
109 let info_ty = &info.ty;
110 let info_expr = expression_path(info_ty);
111 let info_ctx = &info.ctx;
112 let info_inits = info.fields.iter().map(|named_field| {
113 let field = &named_field.field;
114
115 named_field.func.as_ref().map_or_else(
116 || quote!(#field: self.#field),
117 |func| quote!(#field: #func(self, ctx)),
118 )
119 });
120 let (impl_generics, _, where_clause) = input.generics.split_for_impl();
121
122 quote! {
123 impl #impl_generics #data_ty #where_clause {
124 pub(crate) fn into_info(&self, ctx: &#info_ctx) -> #info_ty {
128 #info_expr {
129 #(#info_inits,)*
130 }
131 }
132 }
133 }
134}
135
136fn finalize_impl(
137 input: &DeriveInput,
138 fields: &[FieldSpec],
139 finalize: &crate::lower_parse::LowerFinalizeSpec,
140) -> syn::Result<proc_macro2::TokenStream> {
141 validate_finalize_fields(fields, finalize)?;
142
143 let source = &finalize.source;
144 let method = &finalize.method;
145 let args = finalize.args.iter().map(|arg| {
146 let field = &arg.field;
147 let ty = &arg.ty;
148
149 quote!(#field: #ty)
150 });
151 let field_inits = fields.iter().map(|spec| {
152 let field = &spec.ident;
153
154 if finalize.args.iter().any(|arg| arg.field == *field) {
155 quote!(#field)
156 } else if let Some(rename) = finalize
157 .renames
158 .iter()
159 .find(|rename| rename.field == *field)
160 {
161 let source_field = &rename.source;
162
163 quote!(#field: source.#source_field)
164 } else {
165 quote!(#field: source.#field)
166 }
167 });
168 let def_ident = &input.ident;
169 let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
170
171 Ok(quote! {
172 impl #impl_generics #def_ident #type_generics #where_clause {
173 pub(crate) fn #method(source: #source, #(#args,)*) -> Self {
177 Self {
178 #(#field_inits,)*
179 }
180 }
181 }
182 })
183}
184
185fn expression_path(path: &Path) -> Path {
186 let mut path = path.clone();
187
188 if let Some(last) = path.segments.last_mut()
189 && let PathArguments::AngleBracketed(arguments) = &mut last.arguments
190 {
191 arguments.colon2_token = Some(syn::token::PathSep::default());
192 }
193
194 path
195}
196
197fn reject_duplicate_data_initializers(
199 fields: &[FieldSpec],
200 extra: &[crate::lower_parse::LowerNamedFn],
201) -> syn::Result<()> {
202 let mut saw_name_idx = false;
203
204 for spec in fields {
205 if matches!(&spec.mode, LowerFieldMode::NameIdx(_)) {
206 if saw_name_idx {
207 return Err(syn::Error::new_spanned(
208 &spec.ident,
209 "multiple `name_idx` modes would generate duplicate `name_idx` initializers",
210 ));
211 }
212
213 saw_name_idx = true;
214 }
215 }
216
217 for entry in extra {
218 let duplicates_field = fields.iter().any(|spec| match &spec.mode {
219 LowerFieldMode::Copy | LowerFieldMode::Convert(_) => spec.ident == entry.field,
220 LowerFieldMode::NameIdx(_) => entry.field == "name_idx",
221 LowerFieldMode::Skip => false,
222 });
223
224 if duplicates_field {
225 return Err(syn::Error::new_spanned(
226 &entry.field,
227 format!(
228 "`extra` initializer `{}` duplicates a field initializer",
229 entry.field
230 ),
231 ));
232 }
233 }
234
235 Ok(())
236}
237
238fn validate_finalize_fields(
239 fields: &[FieldSpec],
240 finalize: &crate::lower_parse::LowerFinalizeSpec,
241) -> syn::Result<()> {
242 for (index, arg) in finalize.args.iter().enumerate() {
243 if !fields.iter().any(|field| field.ident == arg.field) {
244 return Err(syn::Error::new_spanned(
245 &arg.field,
246 "finalize argument is not a target field",
247 ));
248 }
249
250 if finalize
251 .args
252 .iter()
253 .take(index)
254 .any(|earlier| earlier.field == arg.field)
255 {
256 return Err(syn::Error::new_spanned(
257 &arg.field,
258 "duplicate finalize argument",
259 ));
260 }
261 }
262
263 for (index, rename) in finalize.renames.iter().enumerate() {
264 if !fields.iter().any(|field| field.ident == rename.field) {
265 return Err(syn::Error::new_spanned(
266 &rename.field,
267 "finalize rename is not a target field",
268 ));
269 }
270
271 if finalize
272 .renames
273 .iter()
274 .take(index)
275 .any(|earlier| earlier.field == rename.field)
276 || finalize.args.iter().any(|arg| arg.field == rename.field)
277 {
278 return Err(syn::Error::new_spanned(
279 &rename.field,
280 "duplicate finalize initializer",
281 ));
282 }
283 }
284
285 Ok(())
286}
287
288#[cfg(test)]
289mod tests {
290 use googletest::prelude::*;
291
292 use super::*;
293
294 #[gtest]
295 fn lower_data_expansion_copies_declared_fields() -> Result<()> {
296 let input: DeriveInput = syn::parse_quote! {
297 #[lower(data = "RecordData", ctx = "LowerContext")]
298 struct RecordDef {
299 #[lower(copy)]
300 value: i32,
301 }
302 };
303
304 let output = expand_lower_data(&input).or_fail()?;
305 let expected = quote! {
306 impl RecordDef {
307 pub(crate) fn into_data(&self, ctx: &mut LowerContext) -> RecordData {
311 RecordData {
312 value: self.value,
313 }
314 }
315 }
316 };
317
318 verify_that!(output.to_string(), eq(&expected.to_string()))
319 }
320
321 #[gtest]
322 fn lower_data_expansion_preserves_input_generics() -> Result<()> {
323 let input: DeriveInput = syn::parse_quote! {
324 #[lower(data = "RecordData<T>", ctx = "LowerContext")]
325 struct RecordDef<T>
326 where
327 T: Copy,
328 {
329 #[lower(copy)]
330 value: T,
331 }
332 };
333
334 let output = expand_lower_data(&input).or_fail()?;
335 let expected = quote! {
336 impl<T> RecordDef<T>
337 where
338 T: Copy,
339 {
340 pub(crate) fn into_data(&self, ctx: &mut LowerContext) -> RecordData<T> {
344 RecordData::<T> {
345 value: self.value,
346 }
347 }
348 }
349 };
350
351 verify_that!(output.to_string(), eq(&expected.to_string()))
352 }
353
354 #[gtest]
355 fn duplicate_extra_initializer_is_rejected_at_the_duplicate_field() -> Result<()> {
356 let input: DeriveInput = syn::parse_quote! {
357 #[lower(
358 data = "RecordData",
359 ctx = "LowerContext",
360 extra(value = make_value)
361 )]
362 struct RecordDef {
363 #[lower(copy)]
364 value: i32,
365 }
366 };
367
368 let error = expand_lower_data(&input).err().or_fail()?;
369
370 verify_that!(
371 error.to_string(),
372 eq("`extra` initializer `value` duplicates a field initializer")
373 )
374 }
375
376 #[gtest]
377 fn multiple_name_idx_modes_are_rejected_before_generating_duplicate_fields() -> Result<()> {
378 let input: DeriveInput = syn::parse_quote! {
379 #[lower(data = "RecordData", ctx = "LowerContext")]
380 struct RecordDef {
381 #[lower(name_idx = resolve_first)]
382 first_name: String,
383 #[lower(name_idx = resolve_second)]
384 second_name: String,
385 }
386 };
387
388 let error = expand_lower_data(&input).err().or_fail()?;
389
390 verify_that!(
391 error.to_string(),
392 eq("multiple `name_idx` modes would generate duplicate `name_idx` initializers")
393 )
394 }
395
396 #[gtest]
397 fn lower_data_generates_finalize_direction() -> Result<()> {
398 let input: DeriveInput = syn::parse_quote! {
399 #[lower(
400 data = "RecordData",
401 ctx = "LowerContext",
402 finalize = "RecordDraft",
403 finalize_args(id: u32),
404 finalize_rename(value = draft_value)
405 )]
406 struct RecordDef {
407 #[lower(copy)]
408 id: u32,
409 #[lower(copy)]
410 value: i32,
411 #[lower(copy)]
412 ordinary: bool,
413 }
414 };
415
416 let output = expand_lower_data(&input).or_fail()?.to_string();
417
418 verify_that!(output, contains_substring("fn from_draft"))?;
419 verify_that!(output, contains_substring("Self { id ,"))?;
420 verify_that!(output, contains_substring("value : source . draft_value"))?;
421
422 verify_that!(output, contains_substring("ordinary : source . ordinary"))
423 }
424}