use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
pub use cacache;
pub use figment;
use figment::{
providers::{Env, Format, Json, Toml, Yaml},
Figment,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ConfigProfile {
#[default]
Development,
Testing,
Staging,
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"),
}
}
}
#[derive(Debug, Clone)]
pub struct ConfigSource {
pub source_type: String,
pub source_name: String,
}
pub struct ConfigLoader {
figment: Figment,
profile: ConfigProfile,
}
impl ConfigLoader {
pub fn new(profile: ConfigProfile) -> Self {
Self {
figment: Figment::new(),
profile,
}
}
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
}
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
}
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
}
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
}
pub fn with_env(mut self, prefix: &str) -> Self {
self.figment = self.figment.merge(Env::prefixed(prefix).split("_"));
self
}
pub fn with_defaults<T: Serialize>(mut self, defaults: T) -> Self {
self.figment = self
.figment
.merge(figment::providers::Serialized::defaults(defaults));
self
}
pub fn extract<T: for<'de> Deserialize<'de>>(self) -> Result<T> {
self.figment
.extract()
.map_err(|e| anyhow::anyhow!("Configuration error: {}", e))
}
pub fn profile(&self) -> ConfigProfile {
self.profile
}
}
pub struct ContentCache {
cache_path: std::path::PathBuf,
}
impl ContentCache {
pub fn new(cache_path: impl Into<std::path::PathBuf>) -> Self {
Self {
cache_path: cache_path.into(),
}
}
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))
}
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())
}
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()),
}
}
pub async fn has(&self, key: &str) -> bool {
cacache::metadata(&self.cache_path, key).await.is_ok()
}
pub async fn remove(&self, key: &str) -> Result<()> {
cacache::remove(&self.cache_path, key).await?;
Ok(())
}
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(())
}
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,
})
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub total_size: u64,
pub entry_count: usize,
}
impl CacheStats {
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, entry_count: 100,
};
assert!(stats.size_human().contains("MB"));
}
}