wowlab_engine_macros_impl/
parse.rs1use quote::ToTokens;
2use syn::{DeriveInput, Ident, ItemStruct, LitStr, Meta, Path, Token, token::Comma};
3
4pub(crate) fn parse_named_field_attr(
6 input: &DeriveInput,
7 attr_name: &str,
8) -> syn::Result<Option<Ident>> {
9 let mut found: Option<Ident> = None;
10
11 for attr in &input.attrs {
12 if !attr.path().is_ident(attr_name) {
13 continue;
14 }
15
16 if found.is_some() {
17 return Err(syn::Error::new_spanned(
18 attr,
19 format!("duplicate #[{attr_name}] attribute"),
20 ));
21 }
22
23 let name_value = attr.meta.require_name_value().map_err(|error| {
24 syn::Error::new_spanned(
25 &attr.meta,
26 format!("#[{attr_name} = \"...\"] must use `name = \"value\"` form: {error}"),
27 )
28 })?;
29 let lit: LitStr = syn::parse2(name_value.value.to_token_stream()).map_err(|error| {
30 syn::Error::new_spanned(
31 &name_value.value,
32 format!("#[{attr_name}] expects a string literal: {error}"),
33 )
34 })?;
35 let raw = lit.value();
36 let mut parsed: Ident = syn::parse_str(&raw).map_err(|error| {
37 syn::Error::new_spanned(
38 &lit,
39 format!("#[{attr_name}] expects a valid Rust identifier, got `{raw}`: {error}"),
40 )
41 })?;
42 parsed.set_span(lit.span());
45 found = Some(parsed);
46 }
47
48 Ok(found)
49}
50
51pub(crate) fn parse_trait_crate_attr(
53 input: &DeriveInput,
54 attr_name: &str,
55) -> syn::Result<Option<Path>> {
56 let mut found: Option<Path> = None;
57
58 for attr in &input.attrs {
59 if !attr.path().is_ident(attr_name) {
60 continue;
61 }
62
63 if found.is_some() {
64 return Err(syn::Error::new_spanned(
65 attr,
66 format!("duplicate #[{attr_name}(...)] attribute"),
67 ));
68 }
69
70 let mut path: Option<Path> = None;
71
72 attr.parse_nested_meta(|meta| {
73 attr_keys!(
74 meta,
75 unknown = format!(
76 "unknown `{attr_name}` attribute, expected `crate = \"...\"`"
77 );
78 "crate" => (
79 path.is_some(),
80 format!("duplicate `{attr_name}` crate path")
81 ) {
82 let value = meta.value()?;
83 let lit: LitStr = value.parse()?;
84 let raw = lit.value();
85 let parsed: Path = syn::parse_str(&raw).map_err(|error| {
86 syn::Error::new_spanned(
87 &lit,
88 format!(
89 "#[{attr_name}(crate = \"...\")] expects a valid Rust path, got `{raw}`: {error}"
90 ),
91 )
92 })?;
93
94 path = Some(parsed);
95 }
96 )
97 })?;
98 found = Some(path.ok_or_else(|| {
99 syn::Error::new_spanned(
100 attr,
101 format!("#[{attr_name}(...)] missing required `crate = \"...\"`"),
102 )
103 })?);
104 }
105
106 Ok(found)
107}
108
109pub(crate) struct DefineSlotInput {
110 pub(crate) item: ItemStruct,
111 pub(crate) impl_body: Option<proc_macro2::TokenStream>,
112}
113
114impl syn::parse::Parse for DefineSlotInput {
115 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
117 let item: ItemStruct = input.parse()?;
118 let mut impl_body = None;
119
120 while !input.is_empty() {
121 if input.peek(Token![impl]) {
122 let impl_token = input.parse::<Token![impl]>()?;
123
124 if impl_body.is_some() {
125 return Err(syn::Error::new_spanned(
126 impl_token,
127 "duplicate `impl` section in define_slot!",
128 ));
129 }
130
131 let content;
132
133 syn::braced!(content in input);
134 impl_body = Some(content.parse()?);
135 } else {
136 let ident: Ident = input.parse()?;
137
138 return Err(syn::Error::new_spanned(
139 &ident,
140 format!("unexpected section `{ident}` in define_slot!, expected `impl`"),
141 ));
142 }
143 }
144
145 Ok(DefineSlotInput { item, impl_body })
146 }
147}
148
149pub(crate) struct SlotAttr {
150 pub(crate) domain: LitStr,
151 pub(crate) kind_ident: Ident,
152 pub(crate) key_domain: Option<LitStr>,
153}
154
155pub(crate) struct ExprAttr {
156 pub(crate) name: LitStr,
157 pub(crate) field_type_ident: Ident,
158 pub(crate) eval_kind_ident: Ident,
159 pub(crate) field_ident: Ident,
160}
161
162fn slot_kind_ident_from_shorthand(ident: &Ident) -> syn::Result<Ident> {
163 match ident.to_string().as_str() {
164 "singleton" => Ok(Ident::new("Singleton", ident.span())),
165 "keyed" => Ok(Ident::new("Keyed", ident.span())),
166 other => Err(syn::Error::new_spanned(
167 ident,
168 format!("unknown slot kind `{other}`, expected `singleton` or `keyed`"),
169 )),
170 }
171}
172
173pub(crate) fn parse_slot_attr(attr: &syn::Attribute) -> syn::Result<SlotAttr> {
174 let mut domain: Option<LitStr> = None;
175 let mut kind_ident: Option<Ident> = None;
176 let mut key_domain: Option<LitStr> = None;
177
178 attr.parse_nested_meta(|meta| {
179 attr_keys!(
180 meta,
181 unknown = "unknown slot attribute, expected `domain`, `kind`, or `key_domain`";
182 "domain" => (domain.is_some(), "duplicate `domain` slot attribute") {
183 domain = Some(meta.value()?.parse()?);
184 },
185 "kind" => (kind_ident.is_some(), "duplicate `kind` slot attribute") {
186 let ident: Ident = meta.value()?.parse()?;
187
188 kind_ident = Some(slot_kind_ident_from_shorthand(&ident)?);
189 },
190 "key_domain" => (
191 key_domain.is_some(),
192 "duplicate `key_domain` slot attribute"
193 ) {
194 key_domain = Some(meta.value()?.parse()?);
195 }
196 )
197 })?;
198
199 let domain = domain.ok_or_else(|| {
200 syn::Error::new_spanned(attr, "#[slot(...)] missing required `domain = \"...\"`")
201 })?;
202 let kind_ident = kind_ident.ok_or_else(|| {
203 syn::Error::new_spanned(attr, "#[slot(...)] missing required `kind = ...`")
204 })?;
205
206 Ok(SlotAttr {
207 domain,
208 kind_ident,
209 key_domain,
210 })
211}
212
213pub(crate) fn parse_expr_attr(attr: &syn::Attribute, field_ident: &Ident) -> syn::Result<ExprAttr> {
214 let tokens = match &attr.meta {
215 Meta::List(list) => list.tokens.clone(),
216 other => {
217 return Err(syn::Error::new_spanned(
218 other,
219 "#[expr(...)] must use parenthesized form",
220 ));
221 }
222 };
223
224 let parsed = syn::parse2::<ExprAttrInner>(tokens)?;
225
226 Ok(ExprAttr {
227 name: parsed.name,
228 field_type_ident: parsed.field_type_ident,
229 eval_kind_ident: parsed.eval_kind_ident,
230 field_ident: field_ident.clone(),
231 })
232}
233
234struct ExprAttrInner {
235 name: LitStr,
236 field_type_ident: Ident,
237 eval_kind_ident: Ident,
238}
239
240impl syn::parse::Parse for ExprAttrInner {
241 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
242 let name: LitStr = input.parse()?;
243
244 input.parse::<Comma>()?;
245 let field_type_ident: Ident = input.parse()?;
246
247 input.parse::<Comma>()?;
248 let eval_kind_ident: Ident = input.parse()?;
249 let _ = input.parse::<Option<Comma>>()?;
250
251 Ok(Self {
254 name,
255 field_type_ident,
256 eval_kind_ident,
257 })
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use googletest::prelude::*;
264
265 use super::*;
266
267 #[gtest]
268 fn define_slot_rejects_a_duplicate_impl_section() -> Result<()> {
269 let error = syn::parse2::<DefineSlotInput>(quote::quote! {
270 struct Slot { value: f64 }
271 impl {}
272 impl {}
273 })
274 .err()
275 .or_fail()?;
276
277 verify_that!(
278 error.to_string(),
279 eq("duplicate `impl` section in define_slot!")
280 )
281 }
282
283 #[gtest]
284 fn slot_attribute_rejects_a_duplicate_domain_key() -> Result<()> {
285 let item: ItemStruct = syn::parse_quote! {
286 #[slot(domain = "first", domain = "second", kind = singleton)]
287 struct Slot { value: f64 }
288 };
289
290 let error = parse_slot_attr(&item.attrs[0]).err().or_fail()?;
291
292 verify_that!(error.to_string(), eq("duplicate `domain` slot attribute"))
293 }
294
295 #[gtest]
296 fn trait_support_attribute_requires_exactly_one_crate_path() -> Result<()> {
297 let missing: DeriveInput = syn::parse_quote! {
298 #[has_id()]
299 struct Record { ID: i32 }
300 };
301 let duplicate: DeriveInput = syn::parse_quote! {
302 #[has_id(crate = "support", crate = "other")]
303 struct Record { ID: i32 }
304 };
305
306 let missing_error = parse_trait_crate_attr(&missing, "has_id").err().or_fail()?;
307 let duplicate_error = parse_trait_crate_attr(&duplicate, "has_id")
308 .err()
309 .or_fail()?;
310
311 verify_that!(
312 missing_error.to_string(),
313 eq("#[has_id(...)] missing required `crate = \"...\"`")
314 )?;
315
316 verify_that!(
317 duplicate_error.to_string(),
318 eq("duplicate `has_id` crate path")
319 )
320 }
321}