wowlab_engine_macros_impl/
resolved_store.rs1use 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}