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