reasonkit-core 0.1.8

The Reasoning Engine — Auditable Reasoning for Production AI | Rust-Native | Turn Prompts into Protocols
//! Enhanced Configuration Management
//!
//! This module provides advanced configuration layering using figment
//! with superior error provenance and multiple source support.
//!
//! # Features
//! - Multiple config sources (TOML, YAML, JSON, ENV)
//! - Error provenance (know exactly where a value came from)
//! - Profile-based configuration
//! - Content-addressable caching via cacache
//!
//! Enable with: `cargo build --features config-enhanced`

use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;

// Re-exports
pub use cacache;
pub use figment;

use figment::{
    providers::{Env, Format, Json, Toml, Yaml},
    Figment,
};

/// Configuration profile
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ConfigProfile {
    /// Development settings
    #[default]
    Development,
    /// Testing settings
    Testing,
    /// Staging settings
    Staging,
    /// Production settings
    Production,
}

impl std::fmt::Display for ConfigProfile {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Development => write!(f, "development"),
            Self::Testing => write!(f, "testing"),
            Self::Staging => write!(f, "staging"),
            Self::Production => write!(f, "production"),
        }
    }
}

/// Configuration source information
#[derive(Debug, Clone)]
pub struct ConfigSource {
    /// Source type (file, env, default)
    pub source_type: String,
    /// Source path or name
    pub source_name: String,
}

/// Enhanced configuration loader
pub struct ConfigLoader {
    figment: Figment,
    profile: ConfigProfile,
}

impl ConfigLoader {
    /// Create a new configuration loader
    pub fn new(profile: ConfigProfile) -> Self {
        Self {
            figment: Figment::new(),
            profile,
        }
    }

    /// Add a TOML configuration file
    pub fn with_toml(mut self, path: impl AsRef<Path>) -> Self {
        let path = path.as_ref();
        if path.exists() {
            self.figment = self.figment.merge(Toml::file(path));
        }
        self
    }

    /// Add a profile-specific TOML file (e.g., config.development.toml)
    pub fn with_profile_toml(mut self, base_path: impl AsRef<Path>) -> Self {
        let base = base_path.as_ref();
        let stem = base.file_stem().unwrap_or_default().to_string_lossy();
        let ext = base.extension().unwrap_or_default().to_string_lossy();
        let dir = base.parent().unwrap_or(Path::new("."));

        let profile_path = dir.join(format!("{}.{}.{}", stem, self.profile, ext));
        if profile_path.exists() {
            self.figment = self.figment.merge(Toml::file(profile_path));
        }
        self
    }

    /// Add a YAML configuration file
    pub fn with_yaml(mut self, path: impl AsRef<Path>) -> Self {
        let path = path.as_ref();
        if path.exists() {
            self.figment = self.figment.merge(Yaml::file(path));
        }
        self
    }

    /// Add a JSON configuration file
    pub fn with_json(mut self, path: impl AsRef<Path>) -> Self {
        let path = path.as_ref();
        if path.exists() {
            self.figment = self.figment.merge(Json::file(path));
        }
        self
    }

    /// Add environment variables with a prefix
    pub fn with_env(mut self, prefix: &str) -> Self {
        self.figment = self.figment.merge(Env::prefixed(prefix).split("_"));
        self
    }

    /// Add default values
    pub fn with_defaults<T: Serialize>(mut self, defaults: T) -> Self {
        self.figment = self
            .figment
            .merge(figment::providers::Serialized::defaults(defaults));
        self
    }

    /// Extract configuration into a type
    pub fn extract<T: for<'de> Deserialize<'de>>(self) -> Result<T> {
        self.figment
            .extract()
            .map_err(|e| anyhow::anyhow!("Configuration error: {}", e))
    }

    /// Get the profile
    pub fn profile(&self) -> ConfigProfile {
        self.profile
    }
}

/// Content cache using cacache
pub struct ContentCache {
    cache_path: std::path::PathBuf,
}

impl ContentCache {
    /// Create a new content cache
    pub fn new(cache_path: impl Into<std::path::PathBuf>) -> Self {
        Self {
            cache_path: cache_path.into(),
        }
    }

    /// Create a cache in the default location
    pub fn default_location() -> Result<Self> {
        let cache_dir = dirs::cache_dir()
            .ok_or_else(|| anyhow::anyhow!("Could not determine cache directory"))?
            .join("reasonkit")
            .join("content-cache");

        std::fs::create_dir_all(&cache_dir)?;
        Ok(Self::new(cache_dir))
    }

    /// Put content into the cache
    pub async fn put(&self, key: &str, data: &[u8]) -> Result<String> {
        let integrity = cacache::write(&self.cache_path, key, data).await?;
        Ok(integrity.to_string())
    }

    /// Get content from the cache
    pub async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
        match cacache::read(&self.cache_path, key).await {
            Ok(data) => Ok(Some(data)),
            Err(cacache::Error::EntryNotFound(_, _)) => Ok(None),
            Err(e) => Err(e.into()),
        }
    }

    /// Check if content exists
    pub async fn has(&self, key: &str) -> bool {
        cacache::metadata(&self.cache_path, key).await.is_ok()
    }

    /// Remove content from the cache
    pub async fn remove(&self, key: &str) -> Result<()> {
        cacache::remove(&self.cache_path, key).await?;
        Ok(())
    }

    /// Clear all cached content
    pub fn clear(&self) -> Result<()> {
        if self.cache_path.exists() {
            std::fs::remove_dir_all(&self.cache_path)?;
            std::fs::create_dir_all(&self.cache_path)?;
        }
        Ok(())
    }

    /// Get cache statistics
    pub fn stats(&self) -> Result<CacheStats> {
        let mut total_size = 0u64;
        let mut entry_count = 0usize;

        if self.cache_path.exists() {
            for entry in walkdir::WalkDir::new(&self.cache_path)
                .into_iter()
                .flatten()
            {
                if entry.file_type().is_file() {
                    if let Ok(meta) = entry.metadata() {
                        total_size += meta.len();
                        entry_count += 1;
                    }
                }
            }
        }

        Ok(CacheStats {
            total_size,
            entry_count,
        })
    }
}

/// Cache statistics
#[derive(Debug, Clone)]
pub struct CacheStats {
    /// Total cache size in bytes
    pub total_size: u64,
    /// Number of entries
    pub entry_count: usize,
}

impl CacheStats {
    /// Format size as human-readable
    pub fn size_human(&self) -> String {
        const KB: u64 = 1024;
        const MB: u64 = KB * 1024;
        const GB: u64 = MB * 1024;

        if self.total_size >= GB {
            format!("{:.2} GB", self.total_size as f64 / GB as f64)
        } else if self.total_size >= MB {
            format!("{:.2} MB", self.total_size as f64 / MB as f64)
        } else if self.total_size >= KB {
            format!("{:.2} KB", self.total_size as f64 / KB as f64)
        } else {
            format!("{} bytes", self.total_size)
        }
    }
}

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

    #[derive(Debug, Deserialize, Serialize, Default)]
    struct TestConfig {
        name: String,
        port: u16,
        debug: bool,
    }

    #[test]
    fn test_config_loader() {
        let defaults = TestConfig {
            name: "test".to_string(),
            port: 8080,
            debug: true,
        };

        let config: TestConfig = ConfigLoader::new(ConfigProfile::Development)
            .with_defaults(defaults)
            .extract()
            .unwrap();

        assert_eq!(config.name, "test");
        assert_eq!(config.port, 8080);
    }

    #[test]
    fn test_profile_display() {
        assert_eq!(ConfigProfile::Production.to_string(), "production");
        assert_eq!(ConfigProfile::Development.to_string(), "development");
    }

    #[test]
    fn test_cache_stats() {
        let stats = CacheStats {
            total_size: 1024 * 1024 * 5, // 5 MB
            entry_count: 100,
        };

        assert!(stats.size_human().contains("MB"));
    }
}