zonfig 0.1.0

A small dynamic configuration loader with file watching and hot reload support.
Documentation
use std::path::Path;

use serde::de::DeserializeOwned;

use crate::error::{Error, Result};

/// Supported configuration file formats.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
    /// JavaScript Object Notation.
    Json,
    /// YAML Ain't Markup Language.
    Yaml,
    /// Tom's Obvious Minimal Language.
    Toml,
}

impl Format {
    /// Detects a configuration format from a file extension.
    pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref();
        match path.extension().and_then(|ext| ext.to_str()) {
            Some(ext) if ext.eq_ignore_ascii_case("json") => Ok(Self::Json),
            Some(ext) if ext.eq_ignore_ascii_case("yaml") || ext.eq_ignore_ascii_case("yml") => {
                Ok(Self::Yaml)
            }
            Some(ext) if ext.eq_ignore_ascii_case("toml") => Ok(Self::Toml),
            _ => Err(Error::UnknownFormat {
                path: path.to_path_buf(),
            }),
        }
    }

    /// Returns a human-readable format name for errors and logs.
    pub const fn name(self) -> &'static str {
        match self {
            Self::Json => "json",
            Self::Yaml => "yaml",
            Self::Toml => "toml",
        }
    }

    pub(crate) fn parse<T>(self, path: &Path, content: &str) -> Result<T>
    where
        T: DeserializeOwned,
    {
        match self {
            Self::Json => serde_json::from_str(content).map_err(|source| Error::Parse {
                path: path.to_path_buf(),
                format: self.name(),
                source: Box::new(source),
            }),
            Self::Yaml => serde_yaml::from_str(content).map_err(|source| Error::Parse {
                path: path.to_path_buf(),
                format: self.name(),
                source: Box::new(source),
            }),
            Self::Toml => toml::from_str(content).map_err(|source| Error::Parse {
                path: path.to_path_buf(),
                format: self.name(),
                source: Box::new(source),
            }),
        }
    }
}

impl std::fmt::Display for Format {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(self.name())
    }
}