kv_parser/lib.rs
1#![deny(missing_docs)]
2
3//! A simple parser for key-value strings and files.
4//!
5//! This crate provides functions to parse key-value pairs from
6//! strings or text files into a hash map. Each line should contain a key
7//! and value separated by whitespace.
8//!
9//! # Examples
10//!
11//! ```
12//! use kv_parser::text_to_key_value_map;
13//!
14//! let config = text_to_key_value_map("key1 value1\nkey2 value2")?;
15//! assert_eq!(config.get("key1").map(AsRef::as_ref), Some("value1"));
16//! # Ok::<(), kv_parser::Error>(())
17//! ```
18
19use std::{
20 collections::HashMap,
21 fmt::Display,
22 fs::File,
23 io::{BufReader, Read},
24 path::Path,
25};
26
27/// Errors that can occur during key-value file parsing.
28#[derive(Debug)]
29pub enum Error {
30 /// Failed to open the specified file.
31 OpeningFile,
32 /// I/O error occurred while reading the file.
33 ReadingFile,
34 /// Duplicate key found in the file.
35 MultipleKeys(Box<str>),
36}
37
38impl Display for Error {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 Self::OpeningFile => write!(f, "Failed to open file"),
42 Self::ReadingFile => write!(f, "Error reading file"),
43 Self::MultipleKeys(key) => write!(f, "Duplicate key found: {key}"),
44 }
45 }
46}
47
48impl std::error::Error for Error {}
49
50/// Parses a string containing key-value pairs into a hash map.
51///
52/// Each non-empty line of the text should contain a key-value pair separated by
53/// whitespace. The first whitespace character on each line separates the key from
54/// the value.
55///
56/// # Format Rules
57/// - Keys cannot contain whitespace
58/// - Values can contain any characters (leading/trailing whitespace is trimmed)
59/// - Empty lines and lines without whitespace are ignored
60/// - Keys must be unique (duplicate keys result in an error)
61///
62/// # Errors
63///
64/// Returns `Error::MultipleKeys` if a duplicate key is found.
65pub fn text_to_key_value_map(text: &str) -> Result<HashMap<Box<str>, Box<str>>, Error> {
66 let mut map = HashMap::new();
67
68 for line in text.lines() {
69 if line.trim().is_empty() {
70 continue;
71 }
72
73 let Some(index) = line.find(|c: char| c.is_whitespace()) else {
74 continue;
75 };
76
77 #[expect(clippy::string_slice)]
78 let key = line[0..index].trim();
79 if map.contains_key(key) {
80 return Err(Error::MultipleKeys(key.into()));
81 }
82 #[expect(clippy::string_slice)]
83 let value = line[(index + 1)..].trim();
84 map.insert(key.into(), value.into());
85 }
86
87 Ok(map)
88}
89
90/// Parses a key-value file into a hash map.
91///
92/// Reads the file at `path` and delegates to [`text_to_key_value_map`] for parsing.
93///
94/// # Errors
95///
96/// This function will return an error if:
97/// - The file cannot be opened (`Error::OpeningFile`)
98/// - An I/O error occurs while reading (`Error::ReadingFile`)
99/// - A duplicate key is found in the file (`Error::MultipleKeys`)
100pub fn file_to_key_value_map(path: &Path) -> Result<HashMap<Box<str>, Box<str>>, Error> {
101 let Ok(file) = File::open(path) else {
102 return Err(Error::OpeningFile);
103 };
104
105 let mut text = String::new();
106 if BufReader::new(file).read_to_string(&mut text).is_err() {
107 return Err(Error::ReadingFile);
108 }
109
110 text_to_key_value_map(&text)
111}