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 * collected into a struct's `#[kv(warnings)]` field so that a caller can
98 * report the bad input without the whole record failing.
99 */
100#[derive(Clone, Debug, Eq, Hash, PartialEq)]
101pub struct KvWarning {
102    /** The variable (key) whose value could not be parsed. */
103    pub variable: String,
104    /** The raw value that failed to parse. */
105    pub value: String,
106    /** Location of the value within the input. */
107    pub span: Span,
108}
109
110impl std::fmt::Display for KvWarning {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        write!(f, "invalid value {:?} for {}", self.value, self.variable)
113    }
114}
115
116/** Errors that can occur during parsing. */
117#[derive(Debug, Error)]
118pub enum KvError {
119    /** A line was not in `KEY=VALUE` format. */
120    #[error("line is not in KEY=VALUE format")]
121    ParseLine(Span),
122
123    /** A required field was missing from the input. */
124    #[error("missing required field '{0}'")]
125    Incomplete(String),
126
127    /** An unknown variable was encountered. */
128    #[error("unknown variable '{variable}'")]
129    UnknownVariable {
130        /** The name of the unknown variable. */
131        variable: String,
132        /** Location of the variable name in the input. */
133        span: Span,
134    },
135
136    /** Failed to parse an integer value. */
137    #[error("failed to parse integer")]
138    ParseInt {
139        /** The underlying parse error. */
140        #[source]
141        source: ParseIntError,
142        /** Location of the invalid value in the input. */
143        span: Span,
144    },
145
146    /** Failed to parse a value. */
147    #[error("{message}")]
148    Parse {
149        /** Description of the parse error. */
150        message: String,
151        /** Location of the invalid value in the input. */
152        span: Span,
153    },
154}
155
156impl KvError {
157    /** Returns the [`Span`] for this error, if available. */
158    #[must_use]
159    pub const fn span(&self) -> Option<Span> {
160        match self {
161            Self::ParseLine(span)
162            | Self::UnknownVariable { span, .. }
163            | Self::ParseInt { span, .. }
164            | Self::Parse { span, .. } => Some(*span),
165            Self::Incomplete(_) => None,
166        }
167    }
168}
169
170/** A [`Result`](std::result::Result) type alias using [`KvError`]. */
171pub type Result<T> = std::result::Result<T, KvError>;
172
173/**
174 * Trait for types that can be parsed from a KEY=VALUE string.
175 *
176 * This is the extension point for custom types. Implement this trait to
177 * allow your type to be used in a `#[derive(Kv)]` struct.
178 *
179 * The `span` parameter indicates where in the input the value is located,
180 * for error reporting.
181 *
182 * # Example
183 *
184 * ```
185 * use pkgsrc_kv::{FromKv, KvError, Span};
186 *
187 * struct MyId(u32);
188 *
189 * impl FromKv for MyId {
190 *     fn from_kv(value: &str, span: Span) -> Result<Self, KvError> {
191 *         value.parse::<u32>()
192 *             .map(MyId)
193 *             .map_err(|e| KvError::Parse {
194 *                 message: e.to_string(),
195 *                 span,
196 *             })
197 *     }
198 * }
199 * ```
200 */
201pub trait FromKv: Sized {
202    /**
203     * Parse a value from a string.
204     *
205     * # Errors
206     *
207     * Returns an error if the value cannot be parsed into the target type.
208     */
209    fn from_kv(value: &str, span: Span) -> Result<Self>;
210}
211
212/* Implementation for String - always succeeds */
213impl FromKv for String {
214    fn from_kv(value: &str, _span: Span) -> Result<Self> {
215        Ok(value.to_string())
216    }
217}
218
219/* Implementation for numeric types */
220macro_rules! impl_fromkv_for_int {
221    ($($t:ty),*) => {
222        $(
223            impl FromKv for $t {
224                fn from_kv(value: &str, span: Span) -> Result<Self> {
225                    value.parse().map_err(|source: ParseIntError| KvError::ParseInt {
226                        source,
227                        span,
228                    })
229                }
230            }
231        )*
232    };
233}
234
235impl_fromkv_for_int!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
236
237/* Implementation for PathBuf */
238impl FromKv for PathBuf {
239    fn from_kv(value: &str, _span: Span) -> Result<Self> {
240        Ok(Self::from(value))
241    }
242}
243
244/* Implementation for bool (common patterns: yes/no, true/false, 1/0) */
245impl FromKv for bool {
246    fn from_kv(value: &str, span: Span) -> Result<Self> {
247        match value.to_lowercase().as_str() {
248            "true" | "yes" | "1" => Ok(true),
249            "false" | "no" | "0" => Ok(false),
250            _ => Err(KvError::Parse {
251                message: format!("invalid boolean: {value}"),
252                span,
253            }),
254        }
255    }
256}
257
258/**
259 * Splits `value` on whitespace, yielding each word with its [`Span`] in the
260 * original input. `base` is the byte offset of `value` within that input, so
261 * each yielded span points at the word's true location rather than at the
262 * whole value.
263 *
264 * This is an implementation detail shared by the [`Vec`] parser and the code
265 * generated by the `Kv` derive macro; it is not part of the stable API.
266 */
267#[doc(hidden)]
268pub fn words_with_spans(
269    value: &str,
270    base: usize,
271) -> impl Iterator<Item = (&str, Span)> {
272    let value_start = value.as_ptr() as usize;
273    value.split_whitespace().map(move |word| {
274        /*
275         * Each word is a subslice of `value`, so the pointer difference is
276         * its byte offset within `value`; add `base` for the absolute offset.
277         */
278        let offset = base + (word.as_ptr() as usize - value_start);
279        let span = Span {
280            offset,
281            len: word.len(),
282        };
283        (word, span)
284    })
285}
286
287impl<T: FromKv> FromKv for Vec<T> {
288    fn from_kv(value: &str, span: Span) -> Result<Self> {
289        words_with_spans(value, span.offset)
290            .map(|(word, word_span)| T::from_kv(word, word_span))
291            .collect()
292    }
293}