use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use crate::config::value::ConfigDict;
use crate::ObjectType;
#[derive(Debug, Clone)]
pub struct ConfigNode {
pub name: String,
pub node: ConfigDict,
pub group: Option<String>,
pub package: Option<String>,
pub provider: Option<String>,
}
impl ConfigNode {
pub fn new(
name: String,
node: ConfigDict,
group: Option<String>,
package: Option<String>,
provider: Option<String>,
) -> Self {
Self {
name,
node,
group,
package,
provider,
}
}
}
#[derive(Debug, Clone)]
pub enum RepoEntry {
Group(HashMap<String, RepoEntry>),
Config(ConfigNode),
}
#[derive(Debug)]
pub struct ConfigStore {
repo: RwLock<HashMap<String, RepoEntry>>,
}
impl ConfigStore {
pub fn new() -> Self {
Self {
repo: RwLock::new(HashMap::new()),
}
}
pub fn store(
&self,
name: &str,
node: ConfigDict,
group: Option<&str>,
package: Option<&str>,
provider: Option<&str>,
) {
let mut repo = self.repo.write().unwrap();
let mut cur: &mut HashMap<String, RepoEntry> = &mut *repo;
if let Some(group_path) = group {
for part in group_path.split('/') {
if part.is_empty() {
continue;
}
if !cur.contains_key(part) {
cur.insert(part.to_string(), RepoEntry::Group(HashMap::new()));
}
if let RepoEntry::Group(ref mut inner) = cur.get_mut(part).unwrap() {
cur = inner;
} else {
return;
}
}
}
let full_name = if name.ends_with(".yaml") {
name.to_string()
} else {
format!("{}.yaml", name)
};
let config_node = ConfigNode::new(
full_name.clone(),
node,
group.map(|s| s.to_string()),
package.map(|s| s.to_string()),
provider.map(|s| s.to_string()),
);
cur.insert(full_name, RepoEntry::Config(config_node));
}
pub fn load(&self, config_path: &str) -> Option<ConfigNode> {
let repo = self.repo.read().unwrap();
self.load_from_repo(&repo, config_path)
}
fn load_from_repo(
&self,
repo: &HashMap<String, RepoEntry>,
config_path: &str,
) -> Option<ConfigNode> {
let path = if config_path.ends_with(".yaml") {
config_path.to_string()
} else {
format!("{}.yaml", config_path)
};
if let Some(idx) = path.rfind('/') {
let group_path = &path[..idx];
let name = &path[idx + 1..];
let mut cur = repo;
for part in group_path.split('/') {
if part.is_empty() {
continue;
}
match cur.get(part) {
Some(RepoEntry::Group(inner)) => cur = inner,
_ => return None,
}
}
match cur.get(name) {
Some(RepoEntry::Config(node)) => Some(node.clone()),
_ => None,
}
} else {
match repo.get(&path) {
Some(RepoEntry::Config(node)) => Some(node.clone()),
_ => None,
}
}
}
pub fn get_type(&self, path: &str) -> ObjectType {
let repo = self.repo.read().unwrap();
if path.is_empty() {
if repo.is_empty() {
return ObjectType::NotFound;
}
return ObjectType::Group;
}
let mut cur: &HashMap<String, RepoEntry> = &repo;
let parts: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
for (i, part) in parts.iter().enumerate() {
let key = if i == parts.len() - 1 {
if cur.contains_key(*part) {
part.to_string()
} else if cur.contains_key(&format!("{}.yaml", part)) {
format!("{}.yaml", part)
} else {
return ObjectType::NotFound;
}
} else {
part.to_string()
};
match cur.get(&key) {
Some(RepoEntry::Group(inner)) => {
if i == parts.len() - 1 {
return ObjectType::Group;
}
cur = inner;
}
Some(RepoEntry::Config(_)) => {
if i == parts.len() - 1 {
return ObjectType::Config;
}
return ObjectType::NotFound; }
None => return ObjectType::NotFound,
}
}
ObjectType::Group
}
pub fn list(&self, path: &str) -> Option<Vec<String>> {
let repo = self.repo.read().unwrap();
if path.is_empty() {
let mut items: Vec<String> = repo.keys().cloned().collect();
items.sort();
return Some(items);
}
let mut cur: &HashMap<String, RepoEntry> = &repo;
for part in path.split('/') {
if part.is_empty() {
continue;
}
match cur.get(part) {
Some(RepoEntry::Group(inner)) => cur = inner,
_ => return None,
}
}
let mut items: Vec<String> = cur.keys().cloned().collect();
items.sort();
Some(items)
}
pub fn config_exists(&self, config_path: &str) -> bool {
self.get_type(config_path) == ObjectType::Config
}
pub fn group_exists(&self, group_path: &str) -> bool {
self.get_type(group_path) == ObjectType::Group
}
pub fn clear(&self) {
let mut repo = self.repo.write().unwrap();
repo.clear();
}
}
impl Default for ConfigStore {
fn default() -> Self {
Self::new()
}
}
use std::sync::OnceLock;
static INSTANCE: OnceLock<Arc<ConfigStore>> = OnceLock::new();
pub fn instance() -> Arc<ConfigStore> {
Arc::clone(INSTANCE.get_or_init(|| Arc::new(ConfigStore::new())))
}
pub fn reset_instance() {
if let Some(store) = INSTANCE.get() {
store.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::value::ConfigValue;
fn make_test_dict() -> ConfigDict {
let mut dict = ConfigDict::new();
dict.insert(
"driver".to_string(),
ConfigValue::String("mysql".to_string()),
);
dict.insert("port".to_string(), ConfigValue::Int(3306));
dict
}
#[test]
fn test_store_and_load_simple() {
let store = ConfigStore::new();
let node = make_test_dict();
store.store("config", node.clone(), None, None, None);
let loaded = store.load("config").unwrap();
assert_eq!(loaded.name, "config.yaml");
}
#[test]
fn test_store_with_group() {
let store = ConfigStore::new();
let node = make_test_dict();
store.store("mysql", node.clone(), Some("db"), Some("db"), Some("test"));
let loaded = store.load("db/mysql").unwrap();
assert_eq!(loaded.name, "mysql.yaml");
assert_eq!(loaded.group, Some("db".to_string()));
assert_eq!(loaded.package, Some("db".to_string()));
}
#[test]
fn test_get_type() {
let store = ConfigStore::new();
let node = make_test_dict();
store.store("mysql", node.clone(), Some("db"), None, None);
assert_eq!(store.get_type("db"), ObjectType::Group);
assert_eq!(store.get_type("db/mysql"), ObjectType::Config);
assert_eq!(store.get_type("nonexistent"), ObjectType::NotFound);
}
#[test]
fn test_list() {
let store = ConfigStore::new();
let node = make_test_dict();
store.store("mysql", node.clone(), Some("db"), None, None);
store.store("postgres", node.clone(), Some("db"), None, None);
let items = store.list("db").unwrap();
assert_eq!(items, vec!["mysql.yaml", "postgres.yaml"]);
}
}