use std::any::Any;
use std::sync::{Arc, OnceLock};
use dashmap::DashMap;
use tracing::debug;
use super::storage::ConfigDict;
use crate::core::exceptions::OperonError;
pub type ResourceInstance = Arc<dyn Any + Send + Sync>;
pub type Factory =
Arc<dyn Fn(ConfigDict) -> Result<ResourceInstance, OperonError> + Send + Sync + 'static>;
#[derive(Clone)]
pub struct ConfigEntry {
pub factory: Factory,
pub schema_name: Option<&'static str>,
}
impl std::fmt::Debug for ConfigEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfigEntry")
.field("schema_name", &self.schema_name)
.field("factory", &"<fn>")
.finish()
}
}
#[derive(Debug, Default)]
pub struct ConfigRegistry {
entries: DashMap<String, ConfigEntry>,
}
impl ConfigRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(
&self,
category: impl Into<String>,
factory: Factory,
schema_name: Option<&'static str>,
) -> Result<(), OperonError> {
let category = category.into();
if self.entries.contains_key(&category) {
return Err(OperonError::ResourceHub(format!(
"duplicate category '{}' — factory already registered",
category
)));
}
self.entries.insert(
category.clone(),
ConfigEntry {
factory,
schema_name,
},
);
debug!("registered: {} -> {:?}", category, schema_name);
Ok(())
}
pub fn get_entry(&self, category: &str) -> Option<ConfigEntry> {
self.entries.get(category).map(|r| r.clone())
}
pub fn get_factory(&self, category: &str) -> Option<Factory> {
self.entries.get(category).map(|r| r.factory.clone())
}
pub fn create(
&self,
category: &str,
config: ConfigDict,
) -> Result<ResourceInstance, OperonError> {
let factory = self.get_factory(category).ok_or_else(|| {
OperonError::ResourceHub(format!("no factory registered for category '{}'", category))
})?;
factory(config)
}
pub fn clear(&self) {
self.entries.clear();
}
pub fn categories(&self) -> Vec<String> {
self.entries.iter().map(|r| r.key().clone()).collect()
}
}
static GLOBAL: OnceLock<ConfigRegistry> = OnceLock::new();
pub fn registry() -> &'static ConfigRegistry {
GLOBAL.get_or_init(ConfigRegistry::new)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[derive(Debug)]
struct Dummy {
pub url: String,
}
fn dummy_factory() -> Factory {
Arc::new(|cfg: ConfigDict| {
let url = cfg
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| OperonError::Config("missing url".into()))?
.to_string();
Ok(Arc::new(Dummy { url }) as ResourceInstance)
})
}
#[test]
fn register_and_create_roundtrip() {
let reg = ConfigRegistry::new();
reg.register("dummy", dummy_factory(), Some("DummyConfig"))
.unwrap();
let mut cfg = serde_json::Map::new();
cfg.insert("url".into(), json!("http://example.com"));
let inst = reg.create("dummy", cfg).unwrap();
let dummy = inst.downcast::<Dummy>().expect("downcast to Dummy");
assert_eq!(dummy.url, "http://example.com");
}
#[test]
fn duplicate_category_errors() {
let reg = ConfigRegistry::new();
reg.register("dup", dummy_factory(), None).unwrap();
let err = reg.register("dup", dummy_factory(), None).unwrap_err();
assert!(matches!(err, OperonError::ResourceHub(_)));
}
#[test]
fn unknown_category_errors() {
let reg = ConfigRegistry::new();
let err = reg.create("nope", serde_json::Map::new()).unwrap_err();
assert!(matches!(err, OperonError::ResourceHub(_)));
}
}