cfgmatic-files 2.2.0

Configuration file discovery and reading with multiple format support
Documentation
//! Configuration file formats.

use crate::error::{FileError, Result};
use serde::de::DeserializeOwned;
use std::path::Path;

/// Supported configuration file formats.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Format {
    /// TOML format.
    #[default]
    Toml,
    /// JSON format.
    Json,
    /// YAML format.
    Yaml,
}

impl Format {
    /// Get the file extension for this format.
    #[must_use]
    pub const fn extension(self) -> &'static str {
        match self {
            Self::Toml => "toml",
            Self::Json => "json",
            Self::Yaml => "yaml",
        }
    }

    /// Get all supported formats.
    #[must_use]
    pub const fn all() -> &'static [Self] {
        &[Self::Toml, Self::Json, Self::Yaml]
    }

    /// Detect format from file extension.
    pub fn from_path(path: &Path) -> Option<Self> {
        path.extension()
            .and_then(|ext| ext.to_str())
            .and_then(Self::from_extension)
    }

    /// Detect format from extension string.
    #[must_use]
    pub fn from_extension(ext: &str) -> Option<Self> {
        match ext.to_lowercase().as_str() {
            "toml" => Some(Self::Toml),
            "json" => Some(Self::Json),
            "yaml" | "yml" => Some(Self::Yaml),
            _ => None,
        }
    }

    /// Parse content into a value of type T.
    ///
    /// # Errors
    ///
    /// Returns an error if the content cannot be parsed as the specified format.
    pub fn parse<T: DeserializeOwned>(self, content: &str, path: &Path) -> Result<T> {
        match self {
            #[cfg(feature = "toml")]
            Self::Toml => toml::from_str(content).map_err(|e| FileError::Parse {
                path: path.to_path_buf(),
                format: "TOML",
                source: Box::new(e),
            }),
            #[cfg(not(feature = "toml"))]
            Self::Toml => Err(FileError::UnsupportedFormat {
                format: "toml".to_string(),
            }),

            #[cfg(feature = "json")]
            Self::Json => serde_json::from_str(content).map_err(|e| FileError::Parse {
                path: path.to_path_buf(),
                format: "JSON",
                source: Box::new(e),
            }),
            #[cfg(not(feature = "json"))]
            Self::Json => Err(FileError::UnsupportedFormat {
                format: "json".to_string(),
            }),

            // NOTE: serde_yaml is deprecated by the author (see https://github.com/dtolnay/serde-yaml/issues/344)
            // YAML support is considered legacy and may be removed in future versions.
            #[cfg(feature = "yaml")]
            Self::Yaml => serde_yaml::from_str(content).map_err(|e| FileError::Parse {
                path: path.to_path_buf(),
                format: "YAML",
                source: Box::new(e),
            }),
            #[cfg(not(feature = "yaml"))]
            Self::Yaml => Err(FileError::UnsupportedFormat {
                format: "yaml".to_string(),
            }),
        }
    }
}

impl std::fmt::Display for Format {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.extension())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_extension() {
        assert_eq!(Format::Toml.extension(), "toml");
        assert_eq!(Format::Json.extension(), "json");
        assert_eq!(Format::Yaml.extension(), "yaml");
    }

    #[test]
    fn test_format_from_extension() {
        assert_eq!(Format::from_extension("toml"), Some(Format::Toml));
        assert_eq!(Format::from_extension("json"), Some(Format::Json));
        assert_eq!(Format::from_extension("yaml"), Some(Format::Yaml));
        assert_eq!(Format::from_extension("yml"), Some(Format::Yaml));
        assert_eq!(Format::from_extension("unknown"), None);
    }

    #[test]
    fn test_format_from_path() {
        assert_eq!(
            Format::from_path(Path::new("config.toml")),
            Some(Format::Toml)
        );
        assert_eq!(
            Format::from_path(Path::new("/path/to/config.json")),
            Some(Format::Json)
        );
        assert_eq!(Format::from_path(Path::new("config")), None);
    }
}