Skip to main content

pkgsrc_kv_derive/
lib.rs

1/*
2 * Copyright (c) 2025 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17//! Derive macro for parsing `KEY=VALUE` formats.
18//!
19//! This crate provides [`macro@Kv`] for automatically implementing parsers
20//! for structs from `KEY=VALUE` formatted input.
21//!
22//! # Field Types
23//!
24//! | Rust Type | Attribute | Behavior |
25//! |-----------|-----------|----------|
26//! | `T` | | Required single value |
27//! | `Option<T>` | | Optional single value |
28//! | `Option<T>` | `#[kv(lenient)]` | Optional single value; an unparseable value becomes `None` instead of erroring |
29//! | `Vec<T>` | | Whitespace-separated values on single line |
30//! | `Option<Vec<T>>` | | Optional whitespace-separated values |
31//! | `Vec<T>` | `#[kv(multiline)]` | Multiple lines collected into Vec |
32//! | `Option<Vec<T>>` | `#[kv(multiline)]` | Optional multiple lines |
33//! | `HashMap<String, String>` | `#[kv(collect)]` | Collects unhandled keys |
34//! | `Vec<KvWarning>` | `#[kv(warnings)]` | Collects parse failures from `lenient` fields |
35//!
36//! # Container Attributes
37//!
38//! - `#[kv(allow_unknown)]` - Ignore unknown keys instead of returning an error
39//!
40//! # Field Attributes
41//!
42//! - `#[kv(variable = "KEY")]` - Use custom key name instead of uppercased field name
43//! - `#[kv(multiline)]` - Collect multiple lines with the same key into a `Vec`
44//! - `#[kv(collect)]` - Collect all unhandled keys into this `HashMap<String, String>`
45//! - `#[kv(lenient)]` - For an `Option<T>` field, treat a value that fails to parse as `None` rather than erroring (recorded in a `#[kv(warnings)]` field if one is present)
46//! - `#[kv(warnings)]` - Collect parse failures from `lenient` fields into this `Vec<KvWarning>`
47//!
48//! # Duplicate Key Behavior
49//!
50//! For non-multiline fields, duplicate keys overwrite the previous value.
51//! For multiline fields, each occurrence appends to the `Vec`.
52//!
53//! # Examples
54//!
55//! These examples are written against the [`pkgsrc-kv`] crate, which
56//! re-exports this macro alongside the runtime it targets. They are marked
57//! `ignore` here only because this engine crate does not depend on the
58//! runtime; they run as written once `pkgsrc-kv` is a dependency.
59//!
60//! [`pkgsrc-kv`]: https://docs.rs/pkgsrc-kv
61//!
62//! ```ignore
63//! use indoc::indoc;
64//! use pkgsrc_kv::{Kv, KvError};
65//!
66//! #[derive(Kv)]
67//! pub struct Package {
68//!     pkgname: String,
69//!     #[kv(variable = "SIZE_PKG")]
70//!     size: u64,
71//!     #[kv(multiline)]
72//!     description: Vec<String>,
73//!     homepage: Option<String>,
74//! }
75//!
76//! let input = indoc! {"
77//!     PKGNAME=foo-1.0
78//!     SIZE_PKG=1234
79//!     DESCRIPTION=A package that does
80//!     DESCRIPTION=many interesting things.
81//! "};
82//! let pkg = Package::parse(input)?;
83//! assert_eq!(pkg.pkgname, "foo-1.0");
84//! assert_eq!(pkg.size, 1234);
85//! assert_eq!(pkg.description, vec!["A package that does", "many interesting things."]);
86//! assert_eq!(pkg.homepage, None);
87//!
88//! /* Missing required fields return an error. */
89//! assert!(Package::parse("PKGNAME=bar-1.0\n").is_err());
90//! # Ok::<(), KvError>(())
91//! ```
92//!
93//! Use `collect` to collect unhandled keys into a `HashMap`, for example
94//! when parsing `+BUILD_INFO` where arbitrary variables will be present:
95//!
96//! ```ignore
97//! use indoc::indoc;
98//! use std::collections::HashMap;
99//! use pkgsrc_kv::{Kv, KvError};
100//!
101//! #[derive(Kv)]
102//! pub struct BuildInfo {
103//!     build_host: Option<String>,
104//!     machine_arch: Option<String>,
105//!     #[kv(collect)]
106//!     vars: HashMap<String, String>,
107//! }
108//!
109//! let input = indoc! {"
110//!     BUILD_DATE=2025-01-15 10:30:00 +0000
111//!     BUILD_HOST=builder.example.com
112//!     MACHINE_ARCH=x86_64
113//!     PKGPATH=devel/example
114//! "};
115//! let info = BuildInfo::parse(input)?;
116//! assert_eq!(info.build_host, Some("builder.example.com".to_string()));
117//! assert_eq!(info.machine_arch, Some("x86_64".to_string()));
118//! assert_eq!(info.vars.get("PKGPATH"), Some(&"devel/example".to_string()));
119//! assert_eq!(info.vars.get("VARBASE"), None);
120//! # Ok::<(), KvError>(())
121//! ```
122
123#![deny(missing_docs)]
124#![deny(unsafe_code)]
125
126use proc_macro::TokenStream;
127use proc_macro2::TokenStream as TokenStream2;
128use proc_macro_crate::{FoundCrate, crate_name};
129use quote::{format_ident, quote};
130use syn::{
131    Attribute, Data, DeriveInput, Field, Fields, GenericArgument, Ident,
132    Path, PathArguments, Type, parse_macro_input,
133};
134
135/*
136 * Resolve the path to the `pkgsrc-kv` crate as named in the consumer's
137 * dependency graph. Generated code references the runtime through this path
138 * rather than hardcoding a crate name, so a renamed dependency still works.
139 * Since `pkgsrc-kv` re-exports this macro, anything that can name the derive
140 * can also name the runtime. A `#[kv(crate = "...")]` container attribute
141 * overrides the lookup for unusual setups.
142 */
143fn kv_crate_path(container_attrs: &ContainerAttrs) -> TokenStream2 {
144    if let Some(path) = &container_attrs.crate_path {
145        return quote! { #path };
146    }
147    match crate_name("pkgsrc-kv") {
148        Ok(FoundCrate::Itself) => quote! { crate },
149        Ok(FoundCrate::Name(name)) => {
150            let ident = format_ident!("{}", name);
151            quote! { ::#ident }
152        }
153        Err(_) => quote! { ::pkgsrc_kv },
154    }
155}
156
157/// Derive macro for parsing `KEY=VALUE` formatted input.
158///
159/// Generates a `parse` method that parses the struct from a string
160/// containing `KEY=VALUE` pairs separated by newlines.
161///
162/// See the [module documentation](crate) for detailed usage.
163#[proc_macro_derive(Kv, attributes(kv))]
164pub fn derive_kv(input: TokenStream) -> TokenStream {
165    let input = parse_macro_input!(input as DeriveInput);
166
167    match generate_impl(&input) {
168        Ok(tokens) => tokens.into(),
169        Err(err) => err.to_compile_error().into(),
170    }
171}
172
173/// Main implementation generator.
174fn generate_impl(input: &DeriveInput) -> syn::Result<TokenStream2> {
175    let name = &input.ident;
176    let container_attrs = ContainerAttrs::parse(&input.attrs)?;
177    let kv = kv_crate_path(&container_attrs);
178
179    let fields = extract_named_fields(input)?;
180
181    let parsed_fields: Vec<ParsedField> = fields
182        .iter()
183        .map(ParsedField::from_field)
184        .collect::<syn::Result<_>>()?;
185
186    let collect_field =
187        parsed_fields.iter().find(|f| f.kind == FieldKind::Collect);
188    let warnings_field =
189        parsed_fields.iter().find(|f| f.kind == FieldKind::Warnings);
190    let regular_fields: Vec<_> = parsed_fields
191        .iter()
192        .filter(|f| {
193            f.kind != FieldKind::Collect && f.kind != FieldKind::Warnings
194        })
195        .collect();
196
197    let field_decls = generate_field_declarations(&parsed_fields);
198    let warnings_ident = warnings_field.map(|f| &f.ident);
199    let match_arms = generate_match_arms(&regular_fields, warnings_ident, &kv);
200    let unknown_handling =
201        generate_unknown_handling(&container_attrs, collect_field, &kv);
202    let field_extracts: Vec<_> = parsed_fields
203        .iter()
204        .map(|f| f.extract_expr(&kv))
205        .collect();
206    let field_names: Vec<_> = parsed_fields.iter().map(|f| &f.ident).collect();
207
208    let serde_impl = generate_serde_impl(name, &parsed_fields);
209
210    Ok(quote! {
211        impl #name {
212            /// Parses from `KEY=VALUE` formatted input.
213            ///
214            /// # Errors
215            ///
216            /// Returns an error if:
217            /// - A line doesn't contain `=`
218            /// - A required field is missing
219            /// - A value fails to parse into its target type (unless the
220            ///   field is marked `#[kv(lenient)]`)
221            /// - An unknown key is encountered (unless `allow_unknown` is set)
222            pub fn parse(input: &str) -> std::result::Result<Self, #kv::KvError> {
223                use #kv::FromKv;
224
225                #(#field_decls)*
226
227                let input_start = input.as_ptr() as usize;
228
229                for line in input.lines() {
230                    if line.is_empty() {
231                        continue;
232                    }
233
234                    // Use pointer arithmetic to compute the line offset.
235                    // This correctly handles both LF and CRLF line endings.
236                    let line_offset = line.as_ptr() as usize - input_start;
237
238                    let eq_pos = match line.find('=') {
239                        Some(p) => p,
240                        None => {
241                            return Err(#kv::KvError::ParseLine(#kv::Span {
242                                offset: line_offset,
243                                len: line.len(),
244                            }));
245                        }
246                    };
247
248                    let key = &line[..eq_pos];
249                    let value = &line[eq_pos + 1..];
250                    let value_offset = line_offset + eq_pos + 1;
251                    let value_span = #kv::Span {
252                        offset: value_offset,
253                        len: value.len(),
254                    };
255
256                    match key {
257                        #(#match_arms)*
258                        #unknown_handling
259                    }
260                }
261
262                Ok(#name {
263                    #(#field_names: #field_extracts,)*
264                })
265            }
266        }
267
268        #serde_impl
269    })
270}
271
272/// Extracts named fields from a struct, returning an error for other types.
273fn extract_named_fields(
274    input: &DeriveInput,
275) -> syn::Result<&syn::punctuated::Punctuated<Field, syn::token::Comma>> {
276    let Data::Struct(data) = &input.data else {
277        return Err(syn::Error::new_spanned(
278            input,
279            "Kv derive only supports structs",
280        ));
281    };
282    let Fields::Named(fields) = &data.fields else {
283        return Err(syn::Error::new_spanned(
284            input,
285            "Kv derive only supports structs with named fields",
286        ));
287    };
288    Ok(&fields.named)
289}
290
291/// Generates variable declarations for parsing state.
292fn generate_field_declarations(fields: &[ParsedField]) -> Vec<TokenStream2> {
293    fields
294        .iter()
295        .map(|f| {
296            let ident = &f.ident;
297            let state_ty = f.state_type();
298            match f.kind {
299                FieldKind::Collect => {
300                    quote! { let mut #ident: #state_ty = std::collections::HashMap::new(); }
301                }
302                FieldKind::Warnings => {
303                    quote! { let mut #ident: #state_ty = Vec::new(); }
304                }
305                _ => quote! { let mut #ident: #state_ty = None; },
306            }
307        })
308        .collect()
309}
310
311/// Generates match arms for known keys.
312fn generate_match_arms(
313    fields: &[&ParsedField],
314    warnings_ident: Option<&Ident>,
315    kv: &TokenStream2,
316) -> Vec<TokenStream2> {
317    fields
318        .iter()
319        .map(|f| {
320            let ident = &f.ident;
321            let key_name = &f.key_name;
322            if f.lenient {
323                let inner = &f.inner_type;
324                match warnings_ident {
325                    Some(warnings) => quote! {
326                        #key_name => {
327                            match <#inner as FromKv>::from_kv(value, value_span) {
328                                Ok(parsed) => #ident = Some(parsed),
329                                Err(_) => {
330                                    #ident = None;
331                                    #warnings.push(#kv::KvWarning {
332                                        variable: key.to_string(),
333                                        value: value.to_string(),
334                                        span: value_span,
335                                    });
336                                }
337                            }
338                        }
339                    },
340                    None => quote! {
341                        #key_name => {
342                            #ident = <#inner as FromKv>::from_kv(value, value_span).ok();
343                        }
344                    },
345                }
346            } else {
347                let merge_expr = f.merge_expr(kv);
348                quote! {
349                    #key_name => {
350                        #ident = Some(#merge_expr);
351                    }
352                }
353            }
354        })
355        .collect()
356}
357
358/// Generates the fallback arm for unknown keys.
359fn generate_unknown_handling(
360    container_attrs: &ContainerAttrs,
361    collect_field: Option<&ParsedField>,
362    kv: &TokenStream2,
363) -> TokenStream2 {
364    match collect_field {
365        Some(field) => {
366            let ident = &field.ident;
367            quote! {
368                _ => {
369                    #ident.insert(key.to_string(), value.to_string());
370                }
371            }
372        }
373        None if container_attrs.allow_unknown => {
374            quote! { _ => {} }
375        }
376        None => {
377            quote! {
378                unknown => {
379                    return Err(#kv::KvError::UnknownVariable {
380                        variable: unknown.to_string(),
381                        span: #kv::Span {
382                            offset: line_offset,
383                            len: unknown.len(),
384                        },
385                    });
386                }
387            }
388        }
389    }
390}
391
392/// Generates serde Serialize/Deserialize implementations.
393///
394/// These are feature-gated with `#[cfg(feature = "serde")]`.
395fn generate_serde_impl(name: &Ident, fields: &[ParsedField]) -> TokenStream2 {
396    // The warnings sink is a parse-time diagnostic, not part of the data
397    // model, so it is excluded from the serde helper and defaulted on
398    // deserialize.
399    let helper_fields: Vec<&ParsedField> = fields
400        .iter()
401        .filter(|f| f.kind != FieldKind::Warnings)
402        .collect();
403
404    let field_defs: Vec<_> = helper_fields
405        .iter()
406        .map(|f| {
407            let ident = &f.ident;
408            let ty = &f.original_type;
409            let key_name = &f.key_name;
410
411            let serde_attrs = match f.kind {
412                FieldKind::Required | FieldKind::Vec | FieldKind::MultiLine => {
413                    quote! {
414                        #[serde(rename = #key_name)]
415                    }
416                }
417                FieldKind::Optional | FieldKind::OptionVec | FieldKind::OptionMultiLine => {
418                    quote! {
419                        #[serde(rename = #key_name, default, skip_serializing_if = "Option::is_none")]
420                    }
421                }
422                FieldKind::Collect => {
423                    quote! {
424                        #[serde(flatten)]
425                    }
426                }
427                FieldKind::Warnings => quote! {},
428            };
429
430            quote! {
431                #serde_attrs
432                #ident: #ty
433            }
434        })
435        .collect();
436
437    /*
438     * For serialization we build a helper of borrowed fields rather than
439     * cloning the whole struct. Optional fields become `Option<&T>` (not
440     * `&Option<T>`) so that `skip_serializing_if = "Option::is_none"` still
441     * resolves against `Option`.
442     */
443    let ser_field_defs: Vec<_> = helper_fields
444        .iter()
445        .map(|f| {
446            let ident = &f.ident;
447            let key_name = &f.key_name;
448            match f.kind {
449                FieldKind::Required | FieldKind::Vec | FieldKind::MultiLine => {
450                    let ty = &f.original_type;
451                    quote! {
452                        #[serde(rename = #key_name)]
453                        #ident: &'a #ty
454                    }
455                }
456                FieldKind::Optional
457                | FieldKind::OptionVec
458                | FieldKind::OptionMultiLine => {
459                    let inner = extract_type_param(&f.original_type, "Option")
460                        .expect("optional field always has an Option<...> type");
461                    quote! {
462                        #[serde(rename = #key_name, skip_serializing_if = "Option::is_none")]
463                        #ident: Option<&'a #inner>
464                    }
465                }
466                FieldKind::Collect => {
467                    let ty = &f.original_type;
468                    quote! {
469                        #[serde(flatten)]
470                        #ident: &'a #ty
471                    }
472                }
473                /* Filtered out of `helper_fields` above. */
474                FieldKind::Warnings => unreachable!(),
475            }
476        })
477        .collect();
478
479    let ser_to_fields: Vec<_> = helper_fields
480        .iter()
481        .map(|f| {
482            let ident = &f.ident;
483            match f.kind {
484                FieldKind::Optional
485                | FieldKind::OptionVec
486                | FieldKind::OptionMultiLine => {
487                    quote! { #ident: self.#ident.as_ref() }
488                }
489                _ => quote! { #ident: &self.#ident },
490            }
491        })
492        .collect();
493
494    /*
495     * The lifetime is only valid if the helper actually borrows something;
496     * a struct whose only field is the warnings sink has an empty helper.
497     */
498    let ser_lifetime = if helper_fields.is_empty() {
499        quote! {}
500    } else {
501        quote! { <'a> }
502    };
503
504    let from_fields: Vec<_> = fields
505        .iter()
506        .map(|f| {
507            let ident = &f.ident;
508            if f.kind == FieldKind::Warnings {
509                quote! { #ident: Default::default() }
510            } else {
511                quote! { #ident: helper.#ident }
512            }
513        })
514        .collect();
515
516    quote! {
517        #[cfg(feature = "serde")]
518        impl serde::Serialize for #name {
519            fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
520            where
521                S: serde::Serializer,
522            {
523                #[derive(serde::Serialize)]
524                struct Helper #ser_lifetime {
525                    #(#ser_field_defs,)*
526                }
527
528                let helper = Helper {
529                    #(#ser_to_fields,)*
530                };
531                helper.serialize(serializer)
532            }
533        }
534
535        #[cfg(feature = "serde")]
536        impl<'de> serde::Deserialize<'de> for #name {
537            fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
538            where
539                D: serde::Deserializer<'de>,
540            {
541                #[derive(serde::Deserialize)]
542                struct Helper {
543                    #(#field_defs,)*
544                }
545
546                let helper = Helper::deserialize(deserializer)?;
547                Ok(Self {
548                    #(#from_fields,)*
549                })
550            }
551        }
552    }
553}
554
555/// Container-level attributes parsed from `#[kv(...)]`.
556#[derive(Default)]
557struct ContainerAttrs {
558    /// If true, unknown keys are silently ignored.
559    allow_unknown: bool,
560    /** Override for the path to the `pkgsrc-kv` crate. */
561    crate_path: Option<Path>,
562}
563
564impl ContainerAttrs {
565    /// Parses container attributes from a slice of attributes.
566    fn parse(attrs: &[Attribute]) -> syn::Result<Self> {
567        let mut result = Self::default();
568
569        for attr in attrs {
570            if !attr.path().is_ident("kv") {
571                continue;
572            }
573
574            attr.parse_nested_meta(|meta| {
575                if meta.path.is_ident("allow_unknown") {
576                    result.allow_unknown = true;
577                    Ok(())
578                } else if meta.path.is_ident("crate") {
579                    let lit: syn::LitStr = meta.value()?.parse()?;
580                    result.crate_path = Some(lit.parse()?);
581                    Ok(())
582                } else {
583                    Err(meta.error(
584                        "unknown container attribute; expected `allow_unknown` or `crate`",
585                    ))
586                }
587            })?;
588        }
589
590        Ok(result)
591    }
592}
593
594/// Field-level attributes parsed from `#[kv(...)]`.
595#[derive(Default)]
596struct FieldAttrs {
597    /// Custom key name override.
598    variable: Option<String>,
599    /// Whether this field collects multiple lines.
600    multiline: bool,
601    /// Whether this field collects unhandled keys.
602    collect: bool,
603    /// Whether an unparseable value becomes `None` instead of erroring.
604    lenient: bool,
605    /// Whether this field collects parse warnings from `lenient` fields.
606    warnings: bool,
607}
608
609impl FieldAttrs {
610    /// Parses field attributes from a slice of attributes.
611    fn parse(attrs: &[Attribute]) -> syn::Result<Self> {
612        let mut result = Self::default();
613
614        for attr in attrs {
615            if !attr.path().is_ident("kv") {
616                continue;
617            }
618
619            attr.parse_nested_meta(|meta| {
620                if meta.path.is_ident("variable") {
621                    let lit: syn::LitStr = meta.value()?.parse()?;
622                    result.variable = Some(lit.value());
623                    Ok(())
624                } else if meta.path.is_ident("multiline") {
625                    result.multiline = true;
626                    Ok(())
627                } else if meta.path.is_ident("collect") {
628                    result.collect = true;
629                    Ok(())
630                } else if meta.path.is_ident("lenient") {
631                    result.lenient = true;
632                    Ok(())
633                } else if meta.path.is_ident("warnings") {
634                    result.warnings = true;
635                    Ok(())
636                } else {
637                    Err(meta.error(
638                        "unknown field attribute; expected `variable`, `multiline`, `collect`, `lenient`, or `warnings`",
639                    ))
640                }
641            })?;
642        }
643
644        Ok(result)
645    }
646}
647
648/// Classification of how a field should be parsed.
649#[derive(Debug, Clone, Copy, PartialEq, Eq)]
650enum FieldKind {
651    /// `T` - required single value.
652    Required,
653    /// `Option<T>` - optional single value.
654    Optional,
655    /// `Vec<T>` - whitespace-separated values on one line.
656    Vec,
657    /// `Option<Vec<T>>` - optional whitespace-separated values.
658    OptionVec,
659    /// `Vec<T>` with `multiline` - multiple lines appended.
660    MultiLine,
661    /// `Option<Vec<T>>` with `multiline` - optional multiple lines.
662    OptionMultiLine,
663    /// `HashMap<String, String>` with `collect` - collects unhandled keys.
664    Collect,
665    /// `Vec<KvWarning>` with `warnings` - collects `lenient` parse failures.
666    Warnings,
667}
668
669/// A parsed and analyzed struct field.
670struct ParsedField {
671    /// The field identifier.
672    ident: Ident,
673    /// The key name used in KEY=VALUE format.
674    key_name: String,
675    /// How this field should be parsed.
676    kind: FieldKind,
677    /// The inner type (e.g., `T` from `Vec<T>`).
678    inner_type: Type,
679    /// The original declared type.
680    original_type: Type,
681    /// Whether an unparseable value becomes `None` instead of erroring.
682    lenient: bool,
683}
684
685impl ParsedField {
686    /// Analyzes a field and extracts parsing metadata.
687    fn from_field(field: &Field) -> syn::Result<Self> {
688        let ident = field.ident.clone().ok_or_else(|| {
689            syn::Error::new_spanned(field, "expected named field")
690        })?;
691
692        let attrs = FieldAttrs::parse(&field.attrs)?;
693
694        // `lenient` only applies to optional single-value fields.
695        if attrs.lenient
696            && (extract_type_param(&field.ty, "Option").is_none()
697                || extract_option_vec_inner(&field.ty).is_some())
698        {
699            return Err(syn::Error::new_spanned(
700                &field.ty,
701                "`lenient` attribute requires an `Option<T>` field",
702            ));
703        }
704
705        // Validate collect field type
706        if attrs.collect {
707            validate_collect_type(&field.ty, field)?;
708            return Ok(Self {
709                ident,
710                key_name: String::new(),
711                kind: FieldKind::Collect,
712                inner_type: field.ty.clone(),
713                original_type: field.ty.clone(),
714                lenient: false,
715            });
716        }
717
718        // Validate warnings sink field type
719        if attrs.warnings {
720            validate_warnings_type(&field.ty, field)?;
721            return Ok(Self {
722                ident,
723                key_name: String::new(),
724                kind: FieldKind::Warnings,
725                inner_type: field.ty.clone(),
726                original_type: field.ty.clone(),
727                lenient: false,
728            });
729        }
730
731        // Validate multiline is only used with Vec types
732        if attrs.multiline
733            && extract_type_param(&field.ty, "Vec").is_none()
734            && extract_option_vec_inner(&field.ty).is_none()
735        {
736            return Err(syn::Error::new_spanned(
737                &field.ty,
738                "`multiline` attribute requires `Vec<T>` or `Option<Vec<T>>` type",
739            ));
740        }
741
742        let key_name = attrs
743            .variable
744            .unwrap_or_else(|| ident.to_string().to_uppercase());
745
746        let (kind, inner_type) = analyze_type(&field.ty, attrs.multiline);
747
748        Ok(Self {
749            ident,
750            key_name,
751            kind,
752            inner_type,
753            original_type: field.ty.clone(),
754            lenient: attrs.lenient,
755        })
756    }
757
758    /// Returns the type used during parsing to accumulate values.
759    fn state_type(&self) -> TokenStream2 {
760        let inner = &self.inner_type;
761        match self.kind {
762            FieldKind::Required | FieldKind::Optional => {
763                quote! { Option<#inner> }
764            }
765            FieldKind::Vec
766            | FieldKind::OptionVec
767            | FieldKind::MultiLine
768            | FieldKind::OptionMultiLine => {
769                quote! { Option<Vec<#inner>> }
770            }
771            FieldKind::Collect => {
772                quote! { std::collections::HashMap<String, String> }
773            }
774            FieldKind::Warnings => {
775                let ty = &self.original_type;
776                quote! { #ty }
777            }
778        }
779    }
780
781    /// Generates an expression to merge a new value into the accumulator.
782    fn merge_expr(&self, kv: &TokenStream2) -> TokenStream2 {
783        let inner = &self.inner_type;
784        let ident = &self.ident;
785
786        match self.kind {
787            FieldKind::Required | FieldKind::Optional => {
788                quote! {
789                    <#inner as FromKv>::from_kv(value, value_span)?
790                }
791            }
792            FieldKind::Vec | FieldKind::OptionVec => {
793                quote! {
794                    {
795                        let mut items = Vec::new();
796                        for (word, word_span) in #kv::words_with_spans(value, value_offset) {
797                            items.push(<#inner as FromKv>::from_kv(word, word_span)?);
798                        }
799                        items
800                    }
801                }
802            }
803            FieldKind::MultiLine | FieldKind::OptionMultiLine => {
804                quote! {
805                    {
806                        let mut vec = #ident.unwrap_or_default();
807                        vec.push(<#inner as FromKv>::from_kv(value, value_span)?);
808                        vec
809                    }
810                }
811            }
812            FieldKind::Collect => {
813                // Handled separately in unknown_handling
814                quote! { unreachable!() }
815            }
816            FieldKind::Warnings => {
817                // Handled separately in generate_match_arms
818                quote! { unreachable!() }
819            }
820        }
821    }
822
823    /// Generates an expression to extract the final value from the accumulator.
824    fn extract_expr(&self, kv: &TokenStream2) -> TokenStream2 {
825        let ident = &self.ident;
826        let key_name = &self.key_name;
827
828        match self.kind {
829            FieldKind::Required | FieldKind::Vec | FieldKind::MultiLine => {
830                quote! {
831                    #ident.ok_or_else(|| #kv::KvError::Incomplete(#key_name.to_string()))?
832                }
833            }
834            FieldKind::Optional
835            | FieldKind::OptionVec
836            | FieldKind::OptionMultiLine
837            | FieldKind::Collect
838            | FieldKind::Warnings => {
839                quote! { #ident }
840            }
841        }
842    }
843}
844
845/// Validates that a collect field has the correct type.
846fn validate_collect_type(ty: &Type, field: &Field) -> syn::Result<()> {
847    let err = || {
848        syn::Error::new_spanned(
849            field,
850            "`collect` attribute requires `HashMap<String, String>` type",
851        )
852    };
853    let Type::Path(type_path) = ty else {
854        return Err(err());
855    };
856    let Some(segment) = type_path.path.segments.last() else {
857        return Err(err());
858    };
859    if segment.ident != "HashMap" {
860        return Err(err());
861    }
862    let PathArguments::AngleBracketed(args) = &segment.arguments else {
863        return Err(err());
864    };
865    let mut arg_iter = args.args.iter();
866    let is_valid = matches!(
867        (arg_iter.next(), arg_iter.next(), arg_iter.next()),
868        (
869            Some(GenericArgument::Type(Type::Path(k))),
870            Some(GenericArgument::Type(Type::Path(v))),
871            None
872        ) if k.path.is_ident("String") && v.path.is_ident("String")
873    );
874    if is_valid { Ok(()) } else { Err(err()) }
875}
876
877/// Validates that a warnings sink field is a `Vec<KvWarning>`.
878fn validate_warnings_type(ty: &Type, field: &Field) -> syn::Result<()> {
879    let err = || {
880        syn::Error::new_spanned(
881            field,
882            "`warnings` attribute requires a `Vec<KvWarning>` type",
883        )
884    };
885    let Some(inner) = extract_type_param(ty, "Vec") else {
886        return Err(err());
887    };
888    let Type::Path(type_path) = &inner else {
889        return Err(err());
890    };
891    match type_path.path.segments.last() {
892        Some(segment) if segment.ident == "KvWarning" => Ok(()),
893        _ => Err(err()),
894    }
895}
896
897/// Analyzes a type to determine its field kind and inner type.
898fn analyze_type(ty: &Type, multiline: bool) -> (FieldKind, Type) {
899    // Check for Option<Vec<T>>
900    if let Some(vec_inner) = extract_option_vec_inner(ty) {
901        let kind = if multiline {
902            FieldKind::OptionMultiLine
903        } else {
904            FieldKind::OptionVec
905        };
906        return (kind, vec_inner);
907    }
908
909    // Check for Option<T>
910    if let Some(inner) = extract_type_param(ty, "Option") {
911        return (FieldKind::Optional, inner);
912    }
913
914    // Check for Vec<T>
915    if let Some(inner) = extract_type_param(ty, "Vec") {
916        let kind = if multiline {
917            FieldKind::MultiLine
918        } else {
919            FieldKind::Vec
920        };
921        return (kind, inner);
922    }
923
924    // Plain T
925    (FieldKind::Required, ty.clone())
926}
927
928/// Extracts the inner type from `Option<Vec<T>>`.
929fn extract_option_vec_inner(ty: &Type) -> Option<Type> {
930    let option_inner = extract_type_param(ty, "Option")?;
931    extract_type_param(&option_inner, "Vec")
932}
933
934/// Extracts the type parameter from a generic type like `Wrapper<T>`.
935fn extract_type_param(ty: &Type, wrapper: &str) -> Option<Type> {
936    let Type::Path(type_path) = ty else {
937        return None;
938    };
939    let segment = type_path.path.segments.last()?;
940    if segment.ident != wrapper {
941        return None;
942    }
943    let PathArguments::AngleBracketed(args) = &segment.arguments else {
944        return None;
945    };
946    let GenericArgument::Type(inner) = args.args.first()? else {
947        return None;
948    };
949    Some(inner.clone())
950}