Skip to main content

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}