use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use dashmap::DashMap;
use parking_lot::Mutex;
use tracing::{debug, warn};
use super::config_registry::{registry, ResourceInstance};
use super::shortcuts::HealthCheckResult;
use super::storage::{ConfigDict, ConfigStorage, JsonConfigStorage, YamlConfigStorage};
use crate::core::exceptions::OperonError;
#[derive(Clone, Debug)]
pub struct CacheEntry {
pub config: ConfigDict,
pub instance: Option<ResourceInstance>,
}
impl CacheEntry {
fn new(config: ConfigDict) -> Self {
Self {
config,
instance: None,
}
}
}
pub struct ResourceHub {
storage: Arc<dyn ConfigStorage>,
cache: DashMap<String, CacheEntry>,
load_lock: Mutex<()>,
source_path: Option<PathBuf>,
}
impl std::fmt::Debug for ResourceHub {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResourceHub")
.field("cached_keys", &self.cache.len())
.finish()
}
}
impl ResourceHub {
pub fn new(storage: Arc<dyn ConfigStorage>) -> Self {
Self::new_with_source(storage, None)
}
fn new_with_source(storage: Arc<dyn ConfigStorage>, source_path: Option<PathBuf>) -> Self {
Self {
storage,
cache: DashMap::new(),
load_lock: Mutex::new(()),
source_path,
}
}
pub fn from_yaml(path: impl AsRef<Path>) -> Result<Self, OperonError> {
let abs = path
.as_ref()
.canonicalize()
.unwrap_or_else(|_| path.as_ref().to_path_buf());
let storage = YamlConfigStorage::new(abs.clone())?;
Ok(Self::new_with_source(Arc::new(storage), Some(abs)))
}
pub fn from_json(path: impl AsRef<Path>) -> Result<Self, OperonError> {
let abs = path
.as_ref()
.canonicalize()
.unwrap_or_else(|_| path.as_ref().to_path_buf());
let storage = JsonConfigStorage::new(abs.clone())?;
Ok(Self::new_with_source(Arc::new(storage), Some(abs)))
}
pub fn empty() -> Self {
Self::new(Arc::new(EmptyStorage))
}
pub fn source_path(&self) -> Option<&Path> {
self.source_path.as_deref()
}
pub fn auto() -> Option<Arc<ResourceHub>> {
if let Ok(g) = global().read() {
if let Some(existing) = g.clone() {
return Some(existing);
}
}
let candidate = std::env::current_dir()
.map(|p| p.join("resources.yaml"))
.unwrap_or_else(|_| PathBuf::from("resources.yaml"));
if !candidate.exists() {
warn!(
"No resources.yaml found at {}. ResourceHub not installed; \
provider ops will fail at resolution. Call \
ResourceHub::from_yaml(<path>) with an explicit path if \
your file lives elsewhere.",
candidate.display()
);
return None;
}
match ResourceHub::from_yaml(&candidate) {
Ok(hub) => {
let arc = Arc::new(hub);
ResourceHub::set_instance(arc.clone());
Some(arc)
}
Err(e) => {
warn!(
"Failed to load resources.yaml at {}: {}",
candidate.display(),
e
);
None
}
}
}
pub fn instance() -> Result<Arc<ResourceHub>, OperonError> {
let guard = global().read().expect("ResourceHub global lock poisoned");
guard.clone().ok_or_else(|| {
OperonError::ResourceHub(
"ResourceHub not initialized. Install one before resolving resources:\n\
\u{20} operonx::bootstrap(); // auto-discover ./resources.yaml + .env\n\
Or with an explicit path:\n\
\u{20} operonx::ResourceHub::set_instance(\n\
\u{20} std::sync::Arc::new(operonx::ResourceHub::from_yaml(\"path/to/resources.yaml\")?));"
.to_string(),
)
})
}
pub fn set_instance(hub: Arc<ResourceHub>) {
let mut guard = global().write().expect("ResourceHub global lock poisoned");
*guard = Some(hub);
}
pub fn reset_instance() {
let mut guard = global().write().expect("ResourceHub global lock poisoned");
*guard = None;
}
fn load_config(&self, key: &str) -> Result<Option<ConfigDict>, OperonError> {
if let Some(entry) = self.cache.get(key) {
return Ok(Some(entry.config.clone()));
}
let Some(config) = self.storage.load_one(key)? else {
return Ok(None);
};
if !key.contains(':') {
warn!("invalid key format, missing category: {}", key);
return Ok(None);
}
self.cache
.entry(key.to_string())
.or_insert_with(|| CacheEntry::new(config.clone()));
Ok(Some(config))
}
pub fn keys(&self) -> Result<Vec<String>, OperonError> {
for (key, cfg) in self.storage.load_all()? {
self.cache
.entry(key)
.or_insert_with(|| CacheEntry::new(cfg));
}
Ok(self.cache.iter().map(|r| r.key().clone()).collect())
}
pub fn has(&self, key: &str) -> Result<bool, OperonError> {
if self.cache.contains_key(key) {
return Ok(true);
}
Ok(self.load_config(key)?.is_some())
}
pub fn get(&self, key: &str) -> Result<ResourceInstance, OperonError> {
if let Some(entry) = self.cache.get(key) {
if let Some(inst) = &entry.instance {
let inst = inst.clone();
drop(entry);
refresh_keycloak(&inst);
return Ok(inst);
}
}
let _guard = self.load_lock.lock();
if let Some(entry) = self.cache.get(key) {
if let Some(inst) = &entry.instance {
return Ok(inst.clone());
}
}
let config = self
.load_config(key)?
.ok_or_else(|| OperonError::ResourceHub(self.not_found_message(key)))?;
let category = key
.split(':')
.next()
.filter(|c| !c.is_empty())
.ok_or_else(|| {
OperonError::ResourceHub(format!("invalid key '{}' — missing category", key))
})?;
let resolved_config = resolve_keycloak(self, &config)?;
let create_config = resolved_config.clone().unwrap_or_else(|| config.clone());
let instance = registry().create(category, create_config).map_err(|e| {
warn!("failed to create resource '{}': {}", key, e);
OperonError::ResourceHub(format!("resource '{}' failed to initialize: {}", key, e))
})?;
self.cache
.entry(key.to_string())
.and_modify(|e| e.instance = Some(instance.clone()))
.or_insert_with(|| CacheEntry {
config: config.clone(),
instance: Some(instance.clone()),
});
debug!("lazy loaded resource: {}", key);
Ok(instance)
}
pub fn get_config(&self, key: &str) -> Result<ConfigDict, OperonError> {
self.load_config(key)?
.ok_or_else(|| OperonError::ResourceHub(self.not_found_message(key)))
}
fn not_found_message(&self, key: &str) -> String {
let available: Vec<String> = match self.storage.load_all() {
Ok(m) => {
let mut v: Vec<String> = m.keys().cloned().collect();
v.sort();
v
}
Err(_) => {
let mut v: Vec<String> = self.cache.iter().map(|r| r.key().clone()).collect();
v.sort();
v
}
};
let source = self
.source_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<in-memory storage>".to_string());
if available.is_empty() {
format!(
"Resource '{}' not found in {}.\n (No resources loaded — file may be empty.)",
key, source
)
} else {
let avail = available
.iter()
.map(|k| format!("'{}'", k))
.collect::<Vec<_>>()
.join(", ");
format!(
"Resource '{}' not found in {}.\n Available: [{}]",
key, source, avail
)
}
}
pub fn register(
&self,
category: &str,
config: ConfigDict,
registry_key: Option<String>,
) -> Result<String, OperonError> {
let key = registry_key.unwrap_or_else(|| key_of(category, &config));
let instance = registry().create(category, config.clone())?;
self.cache.insert(
key.clone(),
CacheEntry {
config: config.clone(),
instance: Some(instance),
},
);
self.storage.save(&key, config)?;
debug!("registered: {}", key);
Ok(key)
}
pub fn remove(&self, key: &str) -> Result<bool, OperonError> {
let existed_in_cache = self.cache.remove(key).is_some();
let existed_in_storage = self.storage.remove(key)?;
let existed = existed_in_cache || existed_in_storage;
if existed {
debug!("removed: {}", key);
}
Ok(existed)
}
pub fn clear(&self) -> Result<(), OperonError> {
let keys: Vec<String> = self.cache.iter().map(|r| r.key().clone()).collect();
self.cache.clear();
for key in keys {
let _ = self.storage.remove(&key);
}
debug!("cleared all resources");
Ok(())
}
pub fn close(&self) -> Result<(), OperonError> {
self.storage.close()
}
pub fn health_check(&self, keys: Option<&[String]>) -> HealthCheckResult {
let check_keys: Vec<String> = match keys {
Some(k) => k.to_vec(),
None => self.keys().unwrap_or_default(),
};
let mut results = std::collections::HashMap::new();
let mut errors = std::collections::HashMap::new();
for key in check_keys {
match self.get(&key) {
Ok(_) => {
results.insert(key, true);
}
Err(e) => {
warn!("health check failed for '{}': {}", key, e);
results.insert(key.clone(), false);
errors.insert(key, e.to_string());
}
}
}
HealthCheckResult { results, errors }
}
}
fn global() -> &'static RwLock<Option<Arc<ResourceHub>>> {
use std::sync::OnceLock;
static G: OnceLock<RwLock<Option<Arc<ResourceHub>>>> = OnceLock::new();
G.get_or_init(|| RwLock::new(None))
}
#[derive(Debug)]
struct EmptyStorage;
impl ConfigStorage for EmptyStorage {
fn load_one(&self, _key: &str) -> Result<Option<ConfigDict>, OperonError> {
Ok(None)
}
fn load_all(&self) -> Result<std::collections::HashMap<String, ConfigDict>, OperonError> {
Ok(std::collections::HashMap::new())
}
fn save(&self, _key: &str, _config: ConfigDict) -> Result<bool, OperonError> {
Err(OperonError::ResourceHub(
"empty ResourceHub: writes disabled".into(),
))
}
fn remove(&self, _key: &str) -> Result<bool, OperonError> {
Ok(false)
}
}
fn refresh_keycloak(_instance: &ResourceInstance) {
}
fn resolve_keycloak(
_hub: &ResourceHub,
config: &ConfigDict,
) -> Result<Option<ConfigDict>, OperonError> {
if let Some(serde_json::Value::String(api_key)) = config.get("api_key") {
if let Some(name) = api_key.strip_prefix("keycloak:") {
return Err(OperonError::ResourceHub(format!(
"keycloak reference 'keycloak:{}' in api_key — keycloak provider \
not yet implemented in Rust backend (Phase 5)",
name
)));
}
}
Ok(None)
}
fn key_of(category: &str, config: &ConfigDict) -> String {
if let Some(model) = config.get("model").and_then(|v| v.as_str()) {
return format!("{}:{}", category, model);
}
if let Some(name) = config.get("name").and_then(|v| v.as_str()) {
return format!("{}:{}", category, name);
}
format!("{}:{}", category, hash_of(config))
}
fn hash_of(config: &ConfigDict) -> String {
let raw = serde_json::to_string(config).unwrap_or_default();
let hash = fnv1a(&raw);
format!("{:08x}", hash)
}
fn fnv1a(input: &str) -> u32 {
let mut hash: u32 = 0x811c_9dc5;
for b in input.bytes() {
hash ^= u32::from(b);
hash = hash.wrapping_mul(0x0100_0193);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::registry::config_registry::{registry, Factory};
use std::sync::Arc;
#[derive(Debug)]
struct FakeResource {
value: String,
}
fn register_fake_once() {
let factory: Factory = Arc::new(|cfg: ConfigDict| {
let v = cfg
.get("value")
.and_then(|v| v.as_str())
.unwrap_or("default")
.to_string();
Ok(Arc::new(FakeResource { value: v }) as ResourceInstance)
});
let _ = registry().register("testfake", factory, Some("FakeConfig"));
}
#[test]
fn instance_errors_before_set() {
ResourceHub::reset_instance();
let err = ResourceHub::instance().unwrap_err();
assert!(matches!(err, OperonError::ResourceHub(_)));
}
#[test]
fn get_from_cache_roundtrip() {
register_fake_once();
struct MemStorage {
data: std::sync::Mutex<std::collections::HashMap<String, ConfigDict>>,
}
impl ConfigStorage for MemStorage {
fn load_one(&self, key: &str) -> Result<Option<ConfigDict>, OperonError> {
Ok(self.data.lock().unwrap().get(key).cloned())
}
fn load_all(
&self,
) -> Result<std::collections::HashMap<String, ConfigDict>, OperonError> {
Ok(self.data.lock().unwrap().clone())
}
fn save(&self, k: &str, c: ConfigDict) -> Result<bool, OperonError> {
self.data.lock().unwrap().insert(k.to_string(), c);
Ok(true)
}
fn remove(&self, k: &str) -> Result<bool, OperonError> {
Ok(self.data.lock().unwrap().remove(k).is_some())
}
}
let mut seed = ConfigDict::new();
seed.insert("value".into(), serde_json::json!("hello"));
let mut data = std::collections::HashMap::new();
data.insert("testfake:a".to_string(), seed);
let storage = Arc::new(MemStorage {
data: std::sync::Mutex::new(data),
});
let hub = ResourceHub::new(storage);
assert!(hub.has("testfake:a").unwrap());
let inst = hub.get("testfake:a").unwrap();
let r = inst
.downcast::<FakeResource>()
.expect("downcast to FakeResource");
assert_eq!(r.value, "hello");
}
}