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
140fn 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
158fn 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}