use crate::{Error, Result, Value};
use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
struct FastCache {
hot_values: HashMap<String, Value>,
hits: u64,
misses: u64,
}
impl FastCache {
fn new() -> Self {
Self {
hot_values: HashMap::new(),
hits: 0,
misses: 0,
}
}
fn get(&mut self, key: &str) -> Option<&Value> {
if let Some(value) = self.hot_values.get(key) {
self.hits += 1;
Some(value)
} else {
self.misses += 1;
None
}
}
fn insert(&mut self, key: String, value: Value) {
if self.hot_values.len() >= 100 {
let keys_to_remove: Vec<_> = self.hot_values.keys().take(20).cloned().collect();
for k in keys_to_remove {
self.hot_values.remove(&k);
}
}
self.hot_values.insert(key, value);
}
}
#[derive(Debug)]
pub struct EnterpriseConfig {
fast_cache: Arc<RwLock<FastCache>>,
cache: Arc<RwLock<BTreeMap<String, Value>>>,
defaults: Arc<RwLock<BTreeMap<String, Value>>>,
file_path: Option<String>,
format: String,
read_only: bool,
}
#[derive(Debug, Default)]
pub struct ConfigManager {
configs: Arc<RwLock<HashMap<String, EnterpriseConfig>>>,
}
impl Default for EnterpriseConfig {
fn default() -> Self {
Self::new()
}
}
impl EnterpriseConfig {
#[inline(always)]
pub fn new() -> Self {
Self {
fast_cache: Arc::new(RwLock::new(FastCache::new())),
cache: Arc::new(RwLock::new(BTreeMap::new())),
defaults: Arc::new(RwLock::new(BTreeMap::new())),
file_path: None,
format: "conf".to_string(),
read_only: false,
}
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path_str = path.as_ref().to_string_lossy().to_string();
let content = std::fs::read_to_string(&path)?;
let format = Self::detect_format(&path_str);
let value = Self::parse_content(&content, &format)?;
let mut config = Self::new();
config.file_path = Some(path_str);
config.format = format;
if let Value::Table(table) = value {
if let Ok(mut cache) = config.cache.write() {
*cache = table;
}
}
Ok(config)
}
pub fn from_string(content: &str, format: Option<&str>) -> Result<Self> {
let format = format.unwrap_or("conf").to_string();
let value = Self::parse_content(content, &format)?;
let mut config = Self::new();
config.format = format;
if let Value::Table(table) = value {
if let Ok(mut cache) = config.cache.write() {
*cache = table;
}
}
Ok(config)
}
#[inline(always)]
pub fn get(&self, key: &str) -> Option<Value> {
if let Ok(mut fast_cache) = self.fast_cache.write() {
if let Some(value) = fast_cache.get(key) {
return Some(value.clone());
}
}
if let Ok(cache) = self.cache.read() {
if let Some(value) = self.get_nested(&cache, key) {
let value_clone = value.clone();
if let Ok(mut fast_cache) = self.fast_cache.write() {
fast_cache.insert(key.to_string(), value_clone.clone());
}
return Some(value_clone);
}
}
if let Ok(defaults) = self.defaults.read() {
if let Some(value) = self.get_nested(&defaults, key) {
let value_clone = value.clone();
if let Ok(mut fast_cache) = self.fast_cache.write() {
fast_cache.insert(key.to_string(), value_clone.clone());
}
return Some(value_clone);
}
}
None
}
pub fn get_or<T>(&self, key: &str, default: T) -> T
where
T: From<Value> + Clone,
{
if let Some(value) = self.get(key) {
T::from(value)
} else {
default
}
}
#[inline(always)]
pub fn get_or_default(&self, key: &str) -> Option<Value> {
if let Some(value) = self.get(key) {
Some(value)
} else {
if let Ok(defaults) = self.defaults.read() {
self.get_nested(&defaults, key).cloned()
} else {
None
}
}
}
#[inline(always)]
pub fn exists(&self, key: &str) -> bool {
if let Ok(cache) = self.cache.read() {
if self.get_nested(&cache, key).is_some() {
return true;
}
}
if let Ok(defaults) = self.defaults.read() {
self.get_nested(&defaults, key).is_some()
} else {
false
}
}
pub fn set(&mut self, key: &str, value: Value) -> Result<()> {
if let Ok(mut cache) = self.cache.write() {
self.set_nested(&mut cache, key, value.clone());
if let Ok(mut fast_cache) = self.fast_cache.write() {
fast_cache.hot_values.remove(key);
fast_cache.insert(key.to_string(), value);
}
Ok(())
} else {
Err(Error::general(
"Failed to acquire cache lock for write operation",
))
}
}
pub fn cache_stats(&self) -> (u64, u64, f64) {
if let Ok(fast_cache) = self.fast_cache.read() {
let hit_ratio = if fast_cache.hits + fast_cache.misses > 0 {
fast_cache.hits as f64 / (fast_cache.hits + fast_cache.misses) as f64
} else {
0.0
};
(fast_cache.hits, fast_cache.misses, hit_ratio)
} else {
(0, 0, 0.0)
}
}
pub fn set_default(&mut self, key: &str, value: Value) {
if let Ok(mut defaults) = self.defaults.write() {
self.set_nested(&mut defaults, key, value);
}
}
pub fn save(&self) -> Result<()> {
if let Some(ref path) = self.file_path {
if let Ok(cache) = self.cache.read() {
let content = self.serialize_to_format(&cache, &self.format)?;
std::fs::write(path, content)?;
Ok(())
} else {
Err(Error::general(
"Failed to acquire cache lock for save operation",
))
}
} else {
Err(Error::general("No file path specified for save"))
}
}
pub fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path_str = path.as_ref().to_string_lossy();
let format = Self::detect_format(&path_str);
if let Ok(cache) = self.cache.read() {
let content = self.serialize_to_format(&cache, &format)?;
std::fs::write(path, content)?;
Ok(())
} else {
Err(Error::general(
"Failed to acquire cache lock for save operation",
))
}
}
pub fn keys(&self) -> Vec<String> {
if let Ok(cache) = self.cache.read() {
self.collect_keys(&cache, "")
} else {
Vec::new()
}
}
pub fn make_read_only(&mut self) {
self.read_only = true;
}
pub fn clear(&mut self) -> Result<()> {
if self.read_only {
return Err(Error::general("Configuration is read-only"));
}
let mut cache = self
.cache
.write()
.map_err(|_| Error::concurrency("Cache lock poisoned"))?;
cache.clear();
Ok(())
}
pub fn merge(&mut self, other: &EnterpriseConfig) -> Result<()> {
if self.read_only {
return Err(Error::general("Configuration is read-only"));
}
let other_cache = other
.cache
.read()
.map_err(|_| Error::concurrency("Other cache lock poisoned"))?;
let mut self_cache = self
.cache
.write()
.map_err(|_| Error::concurrency("Self cache lock poisoned"))?;
for (key, value) in other_cache.iter() {
self_cache.insert(key.clone(), value.clone());
}
Ok(())
}
fn detect_format(path: &str) -> String {
if path.ends_with(".json") {
"json".to_string()
} else if path.ends_with(".toml") {
"toml".to_string()
} else if path.ends_with(".noml") {
"noml".to_string()
} else {
"conf".to_string()
}
}
fn parse_content(content: &str, format: &str) -> Result<Value> {
match format {
"conf" => {
crate::parsers::conf::parse(content)
}
#[cfg(feature = "json")]
"json" => {
let parsed: serde_json::Value = serde_json::from_str(content)
.map_err(|e| Error::general(format!("JSON parse error: {e}")))?;
crate::parsers::json_parser::from_json_value(parsed)
}
#[cfg(feature = "toml")]
"toml" => crate::parsers::toml_parser::parse(content),
#[cfg(feature = "noml")]
"noml" => crate::parsers::noml_parser::parse(content),
_ => Err(Error::general(format!("Unsupported format: {format}"))),
}
}
#[inline(always)]
fn get_nested<'a>(&self, table: &'a BTreeMap<String, Value>, key: &str) -> Option<&'a Value> {
if !key.contains('.') {
return table.get(key);
}
let parts: Vec<&str> = key.split('.').collect();
let mut current = table.get(parts[0])?;
for part in &parts[1..] {
match current {
Value::Table(nested_table) => {
current = nested_table.get(*part)?;
}
_ => return None,
}
}
Some(current)
}
fn set_nested(&self, table: &mut BTreeMap<String, Value>, key: &str, value: Value) {
if !key.contains('.') {
table.insert(key.to_string(), value);
return;
}
let parts: Vec<&str> = key.split('.').collect();
fn set_recursive(table: &mut BTreeMap<String, Value>, parts: &[&str], value: Value) {
if parts.len() == 1 {
table.insert(parts[0].to_string(), value);
return;
}
let key = parts[0].to_string();
let remaining = &parts[1..];
if !table.contains_key(&key) {
table.insert(key.clone(), Value::table(BTreeMap::new()));
}
if let Some(entry) = table.get_mut(&key) {
if !entry.is_table() {
*entry = Value::table(BTreeMap::new());
}
if let Value::Table(nested_table) = entry {
set_recursive(nested_table, remaining, value);
}
}
}
set_recursive(table, &parts, value);
}
#[allow(clippy::only_used_in_recursion)]
fn collect_keys(&self, table: &BTreeMap<String, Value>, prefix: &str) -> Vec<String> {
let mut keys = Vec::new();
for (key, value) in table {
let full_key = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
keys.push(full_key.clone());
if let Value::Table(nested_table) = value {
keys.extend(self.collect_keys(nested_table, &full_key));
}
}
keys
}
fn serialize_to_format(&self, table: &BTreeMap<String, Value>, format: &str) -> Result<String> {
match format {
"conf" => {
let mut output = String::new();
for (key, value) in table {
output.push_str(&format!("{} = {}\n", key, self.value_to_string(value)));
}
Ok(output)
}
#[cfg(feature = "json")]
"json" => {
let json_value =
crate::parsers::json_parser::to_json_value(&Value::table(table.clone()))?;
serde_json::to_string_pretty(&json_value)
.map_err(|e| Error::general(format!("JSON serialize error: {e}")))
}
_ => Err(Error::general(format!(
"Serialization not supported for format: {format}"
))),
}
}
#[allow(clippy::only_used_in_recursion)]
fn value_to_string(&self, value: &Value) -> String {
match value {
Value::String(s) => format!("\"{s}\""),
Value::Integer(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(|v| self.value_to_string(v)).collect();
items.join(" ")
}
Value::Table(_) => "[Table]".to_string(), #[cfg(feature = "chrono")]
Value::DateTime(dt) => dt.to_rfc3339(),
}
}
}
impl ConfigManager {
pub fn new() -> Self {
Self::default()
}
pub fn load<P: AsRef<Path>>(&self, name: &str, path: P) -> Result<()> {
let config = EnterpriseConfig::from_file(path)?;
let mut configs = self
.configs
.write()
.map_err(|_| Error::concurrency("Configs lock poisoned"))?;
configs.insert(name.to_string(), config);
Ok(())
}
pub fn get(&self, name: &str) -> Option<Arc<RwLock<EnterpriseConfig>>> {
let configs = self.configs.read().ok()?;
configs.get(name).map(|config| {
Arc::new(RwLock::new(EnterpriseConfig {
fast_cache: config.fast_cache.clone(),
cache: config.cache.clone(),
defaults: config.defaults.clone(),
file_path: config.file_path.clone(),
format: config.format.clone(),
read_only: config.read_only,
}))
})
}
pub fn list(&self) -> Vec<String> {
match self.configs.read() {
Ok(configs) => configs.keys().cloned().collect(),
Err(_) => Vec::new(), }
}
pub fn remove(&self, name: &str) -> bool {
match self.configs.write() {
Ok(mut configs) => configs.remove(name).is_some(),
Err(_) => false, }
}
}
pub mod direct {
use super::*;
#[inline(always)]
pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<Value> {
let content = std::fs::read_to_string(path)?;
parse_string(&content, None)
}
#[inline(always)]
pub fn parse_string(content: &str, format: Option<&str>) -> Result<Value> {
let format = format.unwrap_or("conf");
EnterpriseConfig::parse_content(content, format)
}
#[inline(always)]
pub fn parse_to_vec<T>(content: &str) -> Result<Vec<T>>
where
T: TryFrom<Value>,
T::Error: std::fmt::Display,
{
let value = parse_string(content, None)?;
match value {
Value::Array(arr) => arr
.into_iter()
.map(|v| T::try_from(v).map_err(|e| Error::general(e.to_string())))
.collect(),
_ => Err(Error::general("Expected array value")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_enterprise_config_get_or() {
let mut config = EnterpriseConfig::new();
config.set("port", Value::integer(8080)).unwrap();
if let Some(port_value) = config.get("port") {
let port = port_value.as_integer().unwrap_or(3000);
assert_eq!(port, 8080);
}
if config.get("timeout").is_some() {
panic!("Should not find timeout key");
}
let timeout = config
.get("timeout")
.and_then(|v| v.as_integer().ok())
.unwrap_or(30);
assert_eq!(timeout, 30);
}
#[test]
fn test_exists() {
let mut config = EnterpriseConfig::new();
config.set("debug", Value::bool(true)).unwrap();
assert!(config.exists("debug"));
assert!(!config.exists("production"));
}
#[test]
fn test_nested_keys() {
let mut config = EnterpriseConfig::new();
config
.set("database.host", Value::string("localhost"))
.unwrap();
config.set("database.port", Value::integer(5432)).unwrap();
assert_eq!(
config.get("database.host").unwrap().as_string().unwrap(),
"localhost"
);
assert_eq!(
config.get("database.port").unwrap().as_integer().unwrap(),
5432
);
assert!(config.exists("database.host"));
}
#[test]
fn test_direct_parsing() {
let content = "port = 8080\ndebug = true";
let value = direct::parse_string(content, Some("conf")).unwrap();
if let Value::Table(table) = value {
assert_eq!(table.get("port").unwrap().as_integer().unwrap(), 8080);
assert!(table.get("debug").unwrap().as_bool().unwrap());
} else {
panic!("Expected table value");
}
}
}