kv-parser 0.3.0

A simple parser of key-value-files as hash maps
Documentation
#![deny(missing_docs)]

//! A simple parser for key-value strings and files.
//!
//! This crate provides functions to parse key-value pairs from
//! strings or text files into a hash map. Each line should contain a key
//! and value separated by whitespace.
//!
//! # Examples
//!
//! ```
//! use kv_parser::text_to_key_value_map;
//!
//! let config = text_to_key_value_map("key1 value1\nkey2 value2")?;
//! assert_eq!(config.get("key1").map(AsRef::as_ref), Some("value1"));
//! # Ok::<(), kv_parser::Error>(())
//! ```

use std::{
    collections::HashMap,
    fmt::Display,
    fs::File,
    io::{BufReader, Read},
    path::Path,
};

/// Errors that can occur during key-value file parsing.
#[derive(Debug)]
pub enum Error {
    /// Failed to open the specified file.
    OpeningFile,
    /// I/O error occurred while reading the file.
    ReadingFile,
    /// Duplicate key found in the file.
    MultipleKeys(Box<str>),
}

impl Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::OpeningFile => write!(f, "Failed to open file"),
            Self::ReadingFile => write!(f, "Error reading file"),
            Self::MultipleKeys(key) => write!(f, "Duplicate key found: {key}"),
        }
    }
}

impl std::error::Error for Error {}

/// Parses a string containing key-value pairs into a hash map.
///
/// Each non-empty line of the text should contain a key-value pair separated by
/// whitespace. The first whitespace character on each line separates the key from
/// the value.
///
/// # Format Rules
/// - Keys cannot contain whitespace
/// - Values can contain any characters (leading/trailing whitespace is trimmed)
/// - Empty lines and lines without whitespace are ignored
/// - Keys must be unique (duplicate keys result in an error)
///
/// # Errors
///
/// Returns `Error::MultipleKeys` if a duplicate key is found.
pub fn text_to_key_value_map(text: &str) -> Result<HashMap<Box<str>, Box<str>>, Error> {
    let mut map = HashMap::new();

    for line in text.lines() {
        if line.trim().is_empty() {
            continue;
        }

        let Some(index) = line.find(|c: char| c.is_whitespace()) else {
            continue;
        };

        #[expect(clippy::string_slice)]
        let key = line[0..index].trim();
        if map.contains_key(key) {
            return Err(Error::MultipleKeys(key.into()));
        }
        #[expect(clippy::string_slice)]
        let value = line[(index + 1)..].trim();
        map.insert(key.into(), value.into());
    }

    Ok(map)
}

/// Parses a key-value file into a hash map.
///
/// Reads the file at `path` and delegates to [`text_to_key_value_map`] for parsing.
///
/// # Errors
///
/// This function will return an error if:
/// - The file cannot be opened (`Error::OpeningFile`)
/// - An I/O error occurs while reading (`Error::ReadingFile`)
/// - A duplicate key is found in the file (`Error::MultipleKeys`)
pub fn file_to_key_value_map(path: &Path) -> Result<HashMap<Box<str>, Box<str>>, Error> {
    let Ok(file) = File::open(path) else {
        return Err(Error::OpeningFile);
    };

    let mut text = String::new();
    if BufReader::new(file).read_to_string(&mut text).is_err() {
        return Err(Error::ReadingFile);
    }

    text_to_key_value_map(&text)
}