use async_trait::async_trait;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::error::{ConfigError, ConfigResult};
use crate::filesystem::AsyncFileSystem;
use super::format::ConfigFormat;
use super::traits::ConfigProvider;
use super::value::ConfigValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ConfigSourcePriority {
Default = 0,
Global = 1,
Project = 2,
Environment = 3,
Runtime = 4,
}
#[derive(Debug, Clone)]
pub enum ConfigSource {
File {
path: PathBuf,
format: Option<ConfigFormat>,
priority: ConfigSourcePriority,
},
Environment {
prefix: String,
priority: ConfigSourcePriority,
},
Default {
values: ConfigValue,
priority: ConfigSourcePriority,
},
Memory {
values: HashMap<String, ConfigValue>,
priority: ConfigSourcePriority,
},
}
impl ConfigSource {
pub fn file(path: impl AsRef<Path>, priority: ConfigSourcePriority) -> Self {
Self::File { path: path.as_ref().to_path_buf(), format: None, priority }
}
pub fn file_with_format(
path: impl AsRef<Path>,
format: ConfigFormat,
priority: ConfigSourcePriority,
) -> Self {
Self::File { path: path.as_ref().to_path_buf(), format: Some(format), priority }
}
pub fn environment(prefix: impl Into<String>, priority: ConfigSourcePriority) -> Self {
Self::Environment { prefix: prefix.into(), priority }
}
pub fn defaults(values: ConfigValue) -> Self {
Self::Default { values, priority: ConfigSourcePriority::Default }
}
pub fn memory(values: HashMap<String, ConfigValue>, priority: ConfigSourcePriority) -> Self {
Self::Memory { values, priority }
}
#[must_use]
#[allow(clippy::match_same_arms)] pub fn priority(&self) -> ConfigSourcePriority {
match self {
Self::File { priority, .. } => *priority,
Self::Environment { priority, .. } => *priority,
Self::Default { priority, .. } => *priority,
Self::Memory { priority, .. } => *priority,
}
}
}
pub struct FileProvider<FS: AsyncFileSystem> {
path: PathBuf,
format: ConfigFormat,
priority: ConfigSourcePriority,
fs: FS,
}
impl<FS: AsyncFileSystem> FileProvider<FS> {
pub fn new(
path: PathBuf,
format: ConfigFormat,
priority: ConfigSourcePriority,
fs: FS,
) -> Self {
Self { path, format, priority, fs }
}
}
#[async_trait]
impl<FS: AsyncFileSystem> ConfigProvider for FileProvider<FS> {
async fn load(&self) -> ConfigResult<ConfigValue> {
let content = self.fs.read_file_string(&self.path).await.map_err(|e| {
ConfigError::FileReadError { path: self.path.clone(), message: e.to_string() }
})?;
self.format.parse(&content)
}
async fn save(&self, value: &ConfigValue) -> ConfigResult<()> {
let content = self.format.serialize(value)?;
self.fs.write_file(&self.path, content.as_bytes()).await.map_err(|e| {
ConfigError::FileWriteError { path: self.path.clone(), message: e.to_string() }
})
}
fn name(&self) -> &str {
self.path.to_str().unwrap_or("file")
}
fn priority(&self) -> i32 {
self.priority as i32
}
}
pub struct EnvironmentProvider {
prefix: String,
priority: ConfigSourcePriority,
}
impl EnvironmentProvider {
pub fn new(prefix: String, priority: ConfigSourcePriority) -> Self {
Self { prefix, priority }
}
fn insert_nested_value(
map: &mut HashMap<String, ConfigValue>,
parts: &[&str],
value: ConfigValue,
) {
if parts.is_empty() {
return;
}
if parts.len() == 1 {
map.insert(parts[0].to_string(), value);
return;
}
let key = parts[0].to_string();
let rest = &parts[1..];
let entry = map.entry(key).or_insert_with(|| ConfigValue::Map(HashMap::new()));
if let ConfigValue::Map(nested_map) = entry {
Self::insert_nested_value(nested_map, rest, value);
}
}
}
#[async_trait]
impl ConfigProvider for EnvironmentProvider {
async fn load(&self) -> ConfigResult<ConfigValue> {
let mut map = HashMap::new();
let prefix_with_underscore = format!("{}_", self.prefix);
for (key, value) in std::env::vars() {
if key.starts_with(&prefix_with_underscore) {
let config_key =
key[prefix_with_underscore.len()..].to_lowercase().replace('_', ".");
let config_value = if let Ok(b) = value.parse::<bool>() {
ConfigValue::Boolean(b)
} else if let Ok(i) = value.parse::<i64>() {
ConfigValue::Integer(i)
} else if let Ok(f) = value.parse::<f64>() {
ConfigValue::Float(f)
} else {
ConfigValue::String(value)
};
let parts: Vec<&str> = config_key.split('.').collect();
Self::insert_nested_value(&mut map, &parts, config_value);
}
}
Ok(ConfigValue::Map(map))
}
async fn save(&self, _value: &ConfigValue) -> ConfigResult<()> {
Ok(())
}
fn name(&self) -> &'static str {
"environment"
}
fn supports_save(&self) -> bool {
false
}
fn priority(&self) -> i32 {
self.priority as i32
}
}
pub struct DefaultProvider {
values: ConfigValue,
priority: ConfigSourcePriority,
}
impl DefaultProvider {
pub fn new(values: ConfigValue, priority: ConfigSourcePriority) -> Self {
Self { values, priority }
}
}
#[async_trait]
impl ConfigProvider for DefaultProvider {
async fn load(&self) -> ConfigResult<ConfigValue> {
Ok(self.values.clone())
}
async fn save(&self, _value: &ConfigValue) -> ConfigResult<()> {
Ok(())
}
fn name(&self) -> &'static str {
"defaults"
}
fn supports_save(&self) -> bool {
false
}
fn priority(&self) -> i32 {
self.priority as i32
}
}
pub struct MemoryProvider {
values: HashMap<String, ConfigValue>,
priority: ConfigSourcePriority,
}
impl MemoryProvider {
pub fn new(values: HashMap<String, ConfigValue>, priority: ConfigSourcePriority) -> Self {
Self { values, priority }
}
}
#[async_trait]
impl ConfigProvider for MemoryProvider {
async fn load(&self) -> ConfigResult<ConfigValue> {
Ok(ConfigValue::Map(self.values.clone()))
}
async fn save(&self, _value: &ConfigValue) -> ConfigResult<()> {
Ok(())
}
fn name(&self) -> &'static str {
"memory"
}
fn priority(&self) -> i32 {
self.priority as i32
}
}