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}