Skip to main content

pkgsrc_kv/
lib.rs

1/*
2 * Copyright (c) 2026 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 * Type-safe `KEY=VALUE` parsing.
19 *
20 * This crate provides the runtime types for parsing `KEY=VALUE` formatted
21 * input — [`Span`], [`KvError`], [`KvWarning`], and the [`FromKv`] extension
22 * trait — together with the [`macro@Kv`] derive macro (enabled by the default
23 * `derive` feature), which generates a `parse` method for a struct.
24 *
25 * Because the derive macro and the runtime it targets live in the same crate,
26 * depending on `pkgsrc-kv` is all that is required to derive `Kv`: there is no
27 * separate runtime crate to add.
28 *
29 * ```ignore
30 * use pkgsrc_kv::Kv;
31 *
32 * #[derive(Kv)]
33 * struct Package {
34 *     pkgname: String,
35 *     #[kv(variable = "SIZE_PKG")]
36 *     size: u64,
37 *     #[kv(multiline)]
38 *     description: Vec<String>,
39 *     homepage: Option<String>,
40 * }
41 *
42 * let pkg = Package::parse("PKGNAME=foo-1.0\nSIZE_PKG=42\n")?;
43 * # Ok::<(), pkgsrc_kv::KvError>(())
44 * ```
45 */
46
47#![deny(missing_docs)]
48#![deny(unsafe_code)]
49
50use std::num::ParseIntError;
51use std::path::PathBuf;
52use thiserror::Error;
53
54/**
55 * Derive macro for parsing `KEY=VALUE` formatted input into a struct.
56 *
57 * Available when the default `derive` feature is enabled. See the
58 * [crate-level documentation](crate) and the macro's own documentation for
59 * usage.
60 */
61#[cfg(feature = "derive")]
62pub use pkgsrc_kv_derive::Kv;
63
64/**
65 * A byte offset and length in the input, for error reporting.
66 *
67 * `Span` tracks the location of errors within the original input string,
68 * enabling precise error messages for diagnostic tools.
69 *
70 * ```
71 * use pkgsrc_kv::Span;
72 *
73 * let span = Span { offset: 10, len: 5 };
74 * let range: std::ops::Range<usize> = span.into();
75 * assert_eq!(range, 10..15);
76 * ```
77 */
78#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80pub struct Span {
81    /** Byte offset where this span starts. */
82    pub offset: usize,
83    /** Length in bytes. */
84    pub len: usize,
85}
86
87impl From<Span> for std::ops::Range<usize> {
88    fn from(span: Span) -> Self {
89        span.offset..span.offset + span.len
90    }
91}
92
93/**
94 * A non-fatal problem encountered while parsing.
95 *
96 * Produced for a `#[kv(lenient)]` field whose value failed to parse, and
97 * appended to a caller-owned `Vec<KvWarning>` by the generated
98 * `parse_with_warnings` method so that a caller can record the bad input
99 * without the whole record failing.
100 */
101#[derive(Clone, Debug, Eq, Hash, PartialEq)]
102pub struct KvWarning {
103    /** The variable (key) whose value could not be parsed. */
104    pub variable: String,
105    /** The raw value that failed to parse. */
106    pub value: String,
107    /** Location of the value within the input. */
108    pub span: Span,
109}
110
111impl std::fmt::Display for KvWarning {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        write!(f, "invalid value {:?} for {}", self.value, self.variable)
114    }
115}
116
117/** Errors that can occur during parsing. */
118#[derive(Debug, Error)]
119pub enum KvError {
120    /** A line was not in `KEY=VALUE` format. */
121    #[error("line is not in KEY=VALUE format")]
122    ParseLine(Span),
123
124    /** A required field was missing from the input. */
125    #[error("missing required field '{0}'")]
126    Incomplete(String),
127
128    /** An unknown variable was encountered. */
129    #[error("unknown variable '{variable}'")]
130    UnknownVariable {
131        /** The name of the unknown variable. */
132        variable: String,
133        /** Location of the variable name in the input. */
134        span: Span,
135    },
136
137    /** Failed to parse an integer value. */
138    #[error("failed to parse integer")]
139    ParseInt {
140        /** The underlying parse error. */
141        #[source]
142        source: ParseIntError,
143        /** Location of the invalid value in the input. */
144        span: Span,
145    },
146
147    /** Failed to parse a value. */
148    #[error("{message}")]
149    Parse {
150        /** Description of the parse error. */
151        message: String,
152        /** Location of the invalid value in the input. */
153        span: Span,
154    },
155}
156
157impl KvError {
158    /** Returns the [`Span`] for this error, if available. */
159    #[must_use]
160    pub const fn span(&self) -> Option<Span> {
161        match self {
162            Self::ParseLine(span)
163            | Self::UnknownVariable { span, .. }
164            | Self::ParseInt { span, .. }
165            | Self::Parse { span, .. } => Some(*span),
166            Self::Incomplete(_) => None,
167        }
168    }
169}
170
171/** A [`Result`](std::result::Result) type alias using [`KvError`]. */
172pub type Result<T> = std::result::Result<T, KvError>;
173
174/**
175 * Trait for types that can be parsed from a KEY=VALUE string.
176 *
177 * This is the extension point for custom types. Implement this trait to
178 * allow your type to be used in a `#[derive(Kv)]` struct.
179 *
180 * The `span` parameter indicates where in the input the value is located,
181 * for error reporting.
182 *
183 * # Example
184 *
185 * ```
186 * use pkgsrc_kv::{FromKv, KvError, Span};
187 *
188 * struct MyId(u32);
189 *
190 * impl FromKv for MyId {
191 *     fn from_kv(value: &str, span: Span) -> Result<Self, KvError> {
192 *         value.parse::<u32>()
193 *             .map(MyId)
194 *             .map_err(|e| KvError::Parse {
195 *                 message: e.to_string(),
196 *                 span,
197 *             })
198 *     }
199 * }
200 * ```
201 */
202pub trait FromKv: Sized {
203    /**
204     * Parse a value from a string.
205     *
206     * # Errors
207     *
208     * Returns an error if the value cannot be parsed into the target type.
209     */
210    fn from_kv(value: &str, span: Span) -> Result<Self>;
211}
212
213/* Implementation for String - always succeeds */
214impl FromKv for String {
215    fn from_kv(value: &str, _span: Span) -> Result<Self> {
216        Ok(value.to_string())
217    }
218}
219
220/* Implementation for numeric types */
221macro_rules! impl_fromkv_for_int {
222    ($($t:ty),*) => {
223        $(
224            impl FromKv for $t {
225                fn from_kv(value: &str, span: Span) -> Result<Self> {
226                    value.parse().map_err(|source: ParseIntError| KvError::ParseInt {
227                        source,
228                        span,
229                    })
230                }
231            }
232        )*
233    };
234}
235
236impl_fromkv_for_int!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
237
238/* Implementation for PathBuf */
239impl FromKv for PathBuf {
240    fn from_kv(value: &str, _span: Span) -> Result<Self> {
241        Ok(Self::from(value))
242    }
243}
244
245/* Implementation for bool (common patterns: yes/no, true/false, 1/0) */
246impl FromKv for bool {
247    fn from_kv(value: &str, span: Span) -> Result<Self> {
248        match value.to_lowercase().as_str() {
249            "true" | "yes" | "1" => Ok(true),
250            "false" | "no" | "0" => Ok(false),
251            _ => Err(KvError::Parse {
252                message: format!("invalid boolean: {value}"),
253                span,
254            }),
255        }
256    }
257}
258
259/**
260 * Splits `value` on whitespace, yielding each word with its [`Span`] in the
261 * original input. `base` is the byte offset of `value` within that input, so
262 * each yielded span points at the word's true location rather than at the
263 * whole value.
264 *
265 * This is an implementation detail shared by the [`Vec`] parser and the code
266 * generated by the `Kv` derive macro; it is not part of the stable API.
267 */
268#[doc(hidden)]
269pub fn words_with_spans(
270    value: &str,
271    base: usize,
272) -> impl Iterator<Item = (&str, Span)> {
273    let value_start = value.as_ptr() as usize;
274    value.split_whitespace().map(move |word| {
275        /*
276         * Each word is a subslice of `value`, so the pointer difference is
277         * its byte offset within `value`; add `base` for the absolute offset.
278         */
279        let offset = base + (word.as_ptr() as usize - value_start);
280        let span = Span {
281            offset,
282            len: word.len(),
283        };
284        (word, span)
285    })
286}
287
288impl<T: FromKv> FromKv for Vec<T> {
289    fn from_kv(value: &str, span: Span) -> Result<Self> {
290        words_with_spans(value, span.offset)
291            .map(|(word, word_span)| T::from_kv(word, word_span))
292            .collect()
293    }
294}