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}