Skip to main content

wowlab_engine_macros_impl/
resolved_store.rs

1use quote::quote;
2use syn::{Data, DeriveInput, Expr, Fields};
3
4const DUPLICATE_INITIALIZER: &str = "duplicate `#[resolved(...)]` initializer on field";
5
6enum FieldInit {
7    Default,
8    Neutral(Expr),
9    Value(Expr),
10}
11
12fn parse_field_init(field: &syn::Field) -> syn::Result<FieldInit> {
13    let mut init = None;
14    let mut saw_attr = false;
15
16    for attr in &field.attrs {
17        if !attr.path().is_ident("resolved") {
18            continue;
19        }
20
21        if saw_attr {
22            return Err(syn::Error::new_spanned(
23                attr,
24                "duplicate #[resolved(...)] field attribute",
25            ));
26        }
27
28        saw_attr = true;
29        attr.parse_nested_meta(|meta| {
30            attr_keys!(
31                meta,
32                unknown = "unknown `resolved` field key, expected `neutral` or `default`";
33                "neutral" => (
34                    init.is_some(),
35                    DUPLICATE_INITIALIZER
36                ) {
37                    init = Some(FieldInit::Neutral(meta.value()?.parse()?));
38                },
39                "default" => (
40                    init.is_some(),
41                    DUPLICATE_INITIALIZER
42                ) {
43                    init = Some(FieldInit::Value(meta.value()?.parse()?));
44                }
45            )
46        })?;
47    }
48
49    Ok(init.unwrap_or(FieldInit::Default))
50}
51
52pub(crate) fn expand_resolved_store(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
53    let fields = match &input.data {
54        Data::Struct(data) => match &data.fields {
55            Fields::Named(fields) => &fields.named,
56            _ => {
57                return Err(syn::Error::new_spanned(
58                    &input.ident,
59                    "ResolvedStore requires a named-field struct",
60                ));
61            }
62        },
63        _ => {
64            return Err(syn::Error::new_spanned(
65                &input.ident,
66                "ResolvedStore can only be derived for structs",
67            ));
68        }
69    };
70
71    let initializers = fields
72        .iter()
73        .map(|field| {
74            let ident = field.ident.as_ref().ok_or_else(|| {
75                syn::Error::new_spanned(field, "ResolvedStore requires named fields")
76            })?;
77            let ty = &field.ty;
78            let value = match parse_field_init(field)? {
79                FieldInit::Default => quote!(::core::default::Default::default()),
80                FieldInit::Neutral(value) => quote!(<#ty>::new(#value)),
81                FieldInit::Value(value) => quote!(#value),
82            };
83
84            Ok(quote!(#ident: #value))
85        })
86        .collect::<syn::Result<Vec<_>>>()?;
87
88    let ident = &input.ident;
89    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
90
91    Ok(quote! {
92        impl #impl_generics ::core::default::Default for #ident #type_generics #where_clause {
93            fn default() -> Self {
94                Self {
95                    #(#initializers,)*
96                }
97            }
98        }
99    })
100}
101
102#[cfg(test)]
103mod tests {
104    use googletest::prelude::*;
105    use quote::quote;
106    use syn::parse_quote;
107
108    use super::*;
109
110    #[gtest]
111    fn expansion_uses_declared_neutral_and_value_initializers() -> Result<()> {
112        let input: DeriveInput = parse_quote! {
113            struct Store {
114                #[resolved(neutral = 7)]
115                values: ResolvedMap<u32, i32>,
116                ordinary: Vec<i32>,
117                #[resolved(default = 1.0)]
118                multiplier: f64,
119            }
120        };
121
122        let output = expand_resolved_store(&input)?;
123        let expected = quote! {
124            impl ::core::default::Default for Store {
125                fn default() -> Self {
126                    Self {
127                        values: <ResolvedMap<u32, i32> >::new(7),
128                        ordinary: ::core::default::Default::default(),
129                        multiplier: 1.0,
130                    }
131                }
132            }
133        }
134        .to_string();
135
136        verify_that!(output.to_string(), eq(&expected))
137    }
138
139    #[gtest]
140    fn duplicate_initializer_is_rejected() -> Result<()> {
141        let input: DeriveInput = parse_quote! {
142            struct Store {
143                #[resolved(neutral = 0, default = ResolvedMap::default())]
144                values: ResolvedMap<u32, i32>,
145            }
146        };
147
148        let error = expand_resolved_store(&input).err().or_fail()?;
149
150        verify_that!(error.to_string(), eq(DUPLICATE_INITIALIZER))
151    }
152}