Skip to main content

wowlab_engine_macros_impl/
lower_parse.rs

1use syn::{
2    DeriveInput, Field, Ident, LitStr, Path, Token, Type,
3    parse::{Parse, ParseStream},
4    punctuated::Punctuated,
5};
6
7pub(crate) enum LowerFieldMode {
8    Copy,
9    NameIdx(Path),
10    Convert(Path),
11    Skip,
12}
13
14pub(crate) struct LowerNamedFn {
15    pub(crate) field: Ident,
16    pub(crate) func: Path,
17}
18
19impl Parse for LowerNamedFn {
20    fn parse(input: ParseStream) -> syn::Result<Self> {
21        let field: Ident = input.parse()?;
22
23        input.parse::<Token![=]>()?;
24        let func: Path = input.parse()?;
25
26        Ok(Self { field, func })
27    }
28}
29
30pub(crate) struct LowerInfoField {
31    pub(crate) field: Ident,
32    pub(crate) func: Option<Path>,
33}
34
35impl Parse for LowerInfoField {
36    fn parse(input: ParseStream) -> syn::Result<Self> {
37        let field: Ident = input.parse()?;
38        let func = if input.peek(Token![=]) {
39            input.parse::<Token![=]>()?;
40
41            Some(input.parse()?)
42        } else {
43            None
44        };
45
46        Ok(Self { field, func })
47    }
48}
49
50pub(crate) struct LowerContainer {
51    pub(crate) data: Path,
52    pub(crate) ctx: Path,
53    pub(crate) extra: Vec<LowerNamedFn>,
54    pub(crate) info: Option<LowerInfoSpec>,
55    pub(crate) finalize: Option<LowerFinalizeSpec>,
56}
57
58pub(crate) struct LowerInfoSpec {
59    pub(crate) ty: Path,
60    pub(crate) ctx: Path,
61    pub(crate) fields: Vec<LowerInfoField>,
62}
63
64pub(crate) struct LowerFinalizeSpec {
65    pub(crate) source: Path,
66    pub(crate) method: Ident,
67    pub(crate) args: Vec<LowerFinalizeArg>,
68    pub(crate) renames: Vec<LowerFieldRename>,
69}
70
71pub(crate) struct LowerFinalizeArg {
72    pub(crate) field: Ident,
73    pub(crate) ty: Type,
74}
75
76impl Parse for LowerFinalizeArg {
77    fn parse(input: ParseStream) -> syn::Result<Self> {
78        let field = input.parse()?;
79
80        input.parse::<Token![:]>()?;
81
82        Ok(Self {
83            field,
84            ty: input.parse()?,
85        })
86    }
87}
88
89pub(crate) struct LowerFieldRename {
90    pub(crate) field: Ident,
91    pub(crate) source: Ident,
92}
93
94impl Parse for LowerFieldRename {
95    fn parse(input: ParseStream) -> syn::Result<Self> {
96        let field = input.parse()?;
97
98        input.parse::<Token![=]>()?;
99
100        Ok(Self {
101            field,
102            source: input.parse()?,
103        })
104    }
105}
106
107#[derive(Default)]
108struct LowerContainerDraft {
109    data: Option<Path>,
110    ctx: Option<Path>,
111    extra: Option<Vec<LowerNamedFn>>,
112    info_ty: Option<Path>,
113    info_ctx: Option<Path>,
114    info_fields: Option<Vec<LowerInfoField>>,
115    finalize_source: Option<Path>,
116    finalize_method: Option<Ident>,
117    finalize_args: Option<Vec<LowerFinalizeArg>>,
118    finalize_renames: Option<Vec<LowerFieldRename>>,
119}
120
121trait ParseNestedMetaListExt {
122    fn parse_nested_meta_list<T>(&self) -> syn::Result<Punctuated<T, Token![,]>>
123    where
124        T: Parse;
125}
126
127impl ParseNestedMetaListExt for syn::meta::ParseNestedMeta<'_> {
128    fn parse_nested_meta_list<T>(&self) -> syn::Result<Punctuated<T, Token![,]>>
129    where
130        T: Parse,
131    {
132        let content;
133
134        syn::parenthesized!(content in self.input);
135
136        content.parse_terminated(T::parse, Token![,])
137    }
138}
139
140// #t(fn: rust_alloc_in_loop) diagnostics allocate only while rejecting invalid macro input.
141fn reject_duplicate_named_fns(entries: &[LowerNamedFn], label: &str) -> syn::Result<()> {
142    for (index, entry) in entries.iter().enumerate() {
143        if entries
144            .iter()
145            .take(index)
146            .any(|earlier| earlier.field == entry.field)
147        {
148            return Err(syn::Error::new_spanned(
149                &entry.field,
150                format!("duplicate `{}` in `{label}`", entry.field),
151            ));
152        }
153    }
154
155    Ok(())
156}
157
158// #t(fn: rust_alloc_in_loop) diagnostics allocate only while rejecting invalid macro input.
159// #t(rust_similar_fns) Info-field duplicate diagnostics intentionally mirror named-function diagnostics but operate on a distinct parsed entry type and label contract.
160fn reject_duplicate_info_fields(entries: &[LowerInfoField]) -> syn::Result<()> {
161    for (index, entry) in entries.iter().enumerate() {
162        if entries
163            .iter()
164            .take(index)
165            .any(|earlier| earlier.field == entry.field)
166        {
167            return Err(syn::Error::new_spanned(
168                &entry.field,
169                format!("duplicate `{}` in `info_fields`", entry.field),
170            ));
171        }
172    }
173
174    Ok(())
175}
176
177pub(crate) fn parse_lower_container(input: &DeriveInput) -> syn::Result<LowerContainer> {
178    let mut draft = LowerContainerDraft::default();
179    let mut saw_lower_attr = false;
180
181    for attr in &input.attrs {
182        if !attr.path().is_ident("lower") {
183            continue;
184        }
185
186        if saw_lower_attr {
187            return Err(syn::Error::new_spanned(
188                attr,
189                "duplicate #[lower(...)] container attribute",
190            ));
191        }
192
193        saw_lower_attr = true;
194        attr.parse_nested_meta(|meta| {
195            attr_keys!(
196                meta,
197                unknown = "unknown `lower` container key, expected `data`, `ctx`, `extra`, \
198                    `info`, `info_ctx`, `info_fields`, `finalize`, `finalize_method`, \
199                    `finalize_args`, or `finalize_rename`";
200                "data" => (draft.data.is_some(), "duplicate `data` lower container key") {
201                let lit: LitStr = meta.value()?.parse()?;
202                draft.data = Some(lit.parse()?);
203                },
204                "ctx" => (draft.ctx.is_some(), "duplicate `ctx` lower container key") {
205                let lit: LitStr = meta.value()?.parse()?;
206                draft.ctx = Some(lit.parse()?);
207                },
208                "info" => (draft.info_ty.is_some(), "duplicate `info` lower container key") {
209                let lit: LitStr = meta.value()?.parse()?;
210                draft.info_ty = Some(lit.parse()?);
211                },
212                "info_ctx" => (
213                    draft.info_ctx.is_some(),
214                    "duplicate `info_ctx` lower container key"
215                ) {
216                let lit: LitStr = meta.value()?.parse()?;
217                draft.info_ctx = Some(lit.parse()?);
218                },
219                "extra" => (draft.extra.is_some(), "duplicate `extra` lower container key") {
220                let entries = meta
221                    .parse_nested_meta_list::<LowerNamedFn>()?
222                    .into_iter()
223                    .collect::<Vec<_>>();
224
225                reject_duplicate_named_fns(&entries, "extra")?;
226                draft.extra = Some(entries);
227                },
228                "info_fields" => (
229                    draft.info_fields.is_some(),
230                    "duplicate `info_fields` lower container key"
231                ) {
232                let entries = meta
233                    .parse_nested_meta_list::<LowerInfoField>()?
234                    .into_iter()
235                    .collect::<Vec<_>>();
236
237                    reject_duplicate_info_fields(&entries)?;
238                    draft.info_fields = Some(entries);
239                },
240                "finalize" => (
241                    draft.finalize_source.is_some(),
242                    "duplicate `finalize` lower container key"
243                ) {
244                    let lit: LitStr = meta.value()?.parse()?;
245                    draft.finalize_source = Some(lit.parse()?);
246                },
247                "finalize_method" => (
248                    draft.finalize_method.is_some(),
249                    "duplicate `finalize_method` lower container key"
250                ) {
251                    let lit: LitStr = meta.value()?.parse()?;
252                    draft.finalize_method = Some(lit.parse()?);
253                },
254                "finalize_args" => (
255                    draft.finalize_args.is_some(),
256                    "duplicate `finalize_args` lower container key"
257                ) {
258                    draft.finalize_args = Some(
259                        meta.parse_nested_meta_list::<LowerFinalizeArg>()?
260                            .into_iter()
261                            .collect()
262                    );
263                },
264                "finalize_rename" => (
265                    draft.finalize_renames.is_some(),
266                    "duplicate `finalize_rename` lower container key"
267                ) {
268                    draft.finalize_renames = Some(
269                        meta.parse_nested_meta_list::<LowerFieldRename>()?
270                            .into_iter()
271                            .collect()
272                    );
273                }
274            )
275        })?;
276    }
277
278    finish_lower_container(input, saw_lower_attr, draft)
279}
280
281fn finish_lower_container(
282    input: &DeriveInput,
283    saw_lower_attr: bool,
284    mut draft: LowerContainerDraft,
285) -> syn::Result<LowerContainer> {
286    if !saw_lower_attr {
287        return Err(syn::Error::new_spanned(
288            &input.ident,
289            "#[derive(LowerData)] requires a #[lower(data = \"...\", ctx = \"...\")] attribute",
290        ));
291    }
292
293    let data = draft.data.take().ok_or_else(|| {
294        syn::Error::new_spanned(
295            &input.ident,
296            "#[lower(...)] missing required `data = \"...\"`",
297        )
298    })?;
299    let ctx = draft.ctx.take().ok_or_else(|| {
300        syn::Error::new_spanned(
301            &input.ident,
302            "#[lower(...)] missing required `ctx = \"...\"`",
303        )
304    })?;
305    let info = finish_info(input, &mut draft)?;
306    let finalize = finish_finalize(input, &mut draft)?;
307
308    Ok(LowerContainer {
309        data,
310        ctx,
311        extra: draft.extra.unwrap_or_default(),
312        info,
313        finalize,
314    })
315}
316
317fn finish_info(
318    input: &DeriveInput,
319    draft: &mut LowerContainerDraft,
320) -> syn::Result<Option<LowerInfoSpec>> {
321    let info = match (draft.info_ty.take(), draft.info_ctx.take()) {
322        (Some(ty), Some(ctx)) => Some(LowerInfoSpec {
323            ty,
324            ctx,
325            fields: draft.info_fields.take().unwrap_or_default(),
326        }),
327        (Some(_), None) => {
328            return Err(syn::Error::new_spanned(
329                &input.ident,
330                "#[lower(info = \"...\")] requires `info_ctx = \"...\"`",
331            ));
332        }
333        (None, Some(_)) => {
334            return Err(syn::Error::new_spanned(
335                &input.ident,
336                "#[lower(info_ctx = \"...\")] requires `info = \"...\"`",
337            ));
338        }
339        (None, None) => {
340            if draft.info_fields.is_some() {
341                return Err(syn::Error::new_spanned(
342                    &input.ident,
343                    "#[lower(info_fields(...))] requires `info = \"...\"` and `info_ctx = \"...\"`",
344                ));
345            }
346
347            None
348        }
349    };
350
351    Ok(info)
352}
353
354fn finish_finalize(
355    input: &DeriveInput,
356    draft: &mut LowerContainerDraft,
357) -> syn::Result<Option<LowerFinalizeSpec>> {
358    if let Some(source) = draft.finalize_source.take() {
359        return Ok(Some(LowerFinalizeSpec {
360            source,
361            method: draft
362                .finalize_method
363                .take()
364                .unwrap_or_else(|| Ident::new("from_draft", input.ident.span())),
365            args: draft.finalize_args.take().unwrap_or_default(),
366            renames: draft.finalize_renames.take().unwrap_or_default(),
367        }));
368    }
369
370    if draft.finalize_method.is_some()
371        || draft.finalize_args.is_some()
372        || draft.finalize_renames.is_some()
373    {
374        return Err(syn::Error::new_spanned(
375            &input.ident,
376            "finalize configuration requires `finalize = \"SourceType\"`",
377        ));
378    }
379
380    Ok(None)
381}
382
383pub(crate) fn parse_lower_field_mode(field: &Field) -> syn::Result<LowerFieldMode> {
384    let mut mode: Option<LowerFieldMode> = None;
385    let mut saw_lower = false;
386
387    for attr in &field.attrs {
388        if !attr.path().is_ident("lower") {
389            continue;
390        }
391
392        saw_lower = true;
393        attr.parse_nested_meta(|meta| {
394            attr_keys!(
395                meta,
396                unknown = "unknown `lower` field mode, expected `copy`, `name_idx = fn`, \
397                    `convert = fn`, or `skip`";
398                "copy" => (
399                    mode.is_some(),
400                    "duplicate `#[lower(...)]` mode on field"
401                ) {
402                    mode = Some(LowerFieldMode::Copy);
403                },
404                "skip" => (
405                    mode.is_some(),
406                    "duplicate `#[lower(...)]` mode on field"
407                ) {
408                    mode = Some(LowerFieldMode::Skip);
409                },
410                "name_idx" => (
411                    mode.is_some(),
412                    "duplicate `#[lower(...)]` mode on field"
413                ) {
414                    mode = Some(LowerFieldMode::NameIdx(meta.value()?.parse()?));
415                },
416                "convert" => (
417                    mode.is_some(),
418                    "duplicate `#[lower(...)]` mode on field"
419                ) {
420                    mode = Some(LowerFieldMode::Convert(meta.value()?.parse()?));
421                }
422            )
423        })?;
424    }
425
426    if !saw_lower {
427        return Err(syn::Error::new_spanned(
428            field,
429            "every field must carry a `#[lower(copy | name_idx = fn | convert = fn | skip)]` \
430             attribute (a new field without one is a deliberate compile error)",
431        ));
432    }
433
434    mode.ok_or_else(|| {
435        syn::Error::new_spanned(field, "empty `#[lower()]` attribute: specify a field mode")
436    })
437}
438
439#[cfg(test)]
440mod tests {
441    use googletest::prelude::*;
442
443    use super::*;
444
445    #[gtest]
446    fn lower_container_rejects_info_fields_without_info_contract() -> Result<()> {
447        let input: DeriveInput = syn::parse_quote! {
448            #[lower(
449                data = "RecordData",
450                ctx = "LowerContext",
451                info_fields(value)
452            )]
453            struct RecordDef {
454                #[lower(copy)]
455                value: i32,
456            }
457        };
458
459        let error = parse_lower_container(&input).err().or_fail()?;
460
461        verify_that!(
462            error.to_string(),
463            eq("#[lower(info_fields(...))] requires `info = \"...\"` and `info_ctx = \"...\"`")
464        )
465    }
466
467    #[gtest]
468    fn lower_container_rejects_duplicate_extra_targets() -> Result<()> {
469        let input: DeriveInput = syn::parse_quote! {
470            #[lower(
471                data = "RecordData",
472                ctx = "LowerContext",
473                extra(value = first, value = second)
474            )]
475            struct RecordDef {
476                #[lower(skip)]
477                source: i32,
478            }
479        };
480
481        let error = parse_lower_container(&input).err().or_fail()?;
482
483        verify_that!(error.to_string(), eq("duplicate `value` in `extra`"))
484    }
485}