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 ®ular_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}