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