use async_trait::async_trait;
use parking_lot::RwLock;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc, OnceLock,
};
use crate::errors::ModuleError;
use crate::module::{Module, ModuleAnnotations, ModuleExample, ValidationResult};
use crate::registry::conflicts::{detect_id_conflicts, ConflictSeverity};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleDescriptor {
pub module_id: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: String,
#[serde(default)]
pub documentation: Option<String>,
pub input_schema: serde_json::Value,
pub output_schema: serde_json::Value,
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub annotations: Option<ModuleAnnotations>,
#[serde(default)]
pub examples: Vec<ModuleExample>,
#[serde(default)]
pub metadata: HashMap<String, serde_json::Value>,
#[serde(default)]
pub display: Option<serde_json::Value>,
#[serde(default)]
pub sunset_date: Option<String>,
#[serde(default)]
pub dependencies: Vec<DependencyInfo>,
#[serde(default = "default_enabled", skip_serializing)]
pub enabled: bool,
}
fn default_version() -> String {
DEFAULT_MODULE_VERSION.to_string()
}
fn default_enabled() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyInfo {
pub module_id: String,
pub version_constraint: String,
#[serde(default)]
pub optional: bool,
}
#[derive(Clone)]
pub struct DiscoveredModule {
pub name: String,
pub source: String,
pub descriptor: ModuleDescriptor,
pub module: Arc<dyn Module>,
}
impl std::fmt::Debug for DiscoveredModule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DiscoveredModule")
.field("name", &self.name)
.field("source", &self.source)
.field("descriptor", &self.descriptor)
.field("module", &"<Module>")
.finish()
}
}
#[async_trait]
pub trait Discoverer: Send + Sync {
async fn discover(&self, roots: &[String]) -> Result<Vec<DiscoveredModule>, ModuleError>;
}
pub trait ModuleValidator: Send + Sync {
fn validate(
&self,
module: &dyn Module,
descriptor: Option<&ModuleDescriptor>,
) -> ValidationResult;
}
pub type ModuleCallbackFn = dyn Fn(&str, &dyn Module) + Send + Sync;
type CallbackMap = HashMap<String, Vec<(u64, Arc<ModuleCallbackFn>)>>;
pub const RESERVED_WORDS: &[&str] = &[
"system", "internal", "core", "apcore", "plugin", "schema", "acl",
];
pub const MAX_MODULE_ID_LENGTH: usize = 192;
pub const DEFAULT_MODULE_VERSION: &str = "1.0.0";
pub mod registry_events {
pub const REGISTER: &str = "register";
pub const UNREGISTER: &str = "unregister";
}
pub struct RegistryEvents;
impl RegistryEvents {
pub const REGISTER: &'static str = registry_events::REGISTER;
pub const UNREGISTER: &'static str = registry_events::UNREGISTER;
}
pub const REGISTRY_EVENTS: RegistryEvents = RegistryEvents;
pub const MODULE_ID_PATTERN: &str = r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$";
pub fn module_id_pattern() -> &'static Regex {
static PATTERN: OnceLock<Regex> = OnceLock::new();
PATTERN.get_or_init(|| Regex::new(MODULE_ID_PATTERN).unwrap())
}
fn validate_module_id(name: &str, allow_reserved: bool) -> Result<(), ModuleError> {
if name.is_empty() {
return Err(ModuleError::new(
crate::errors::ErrorCode::GeneralInvalidInput,
"module_id must be a non-empty string".to_string(),
));
}
if !module_id_pattern().is_match(name) {
return Err(ModuleError::new(
crate::errors::ErrorCode::GeneralInvalidInput,
format!(
"Invalid module ID: '{name}'. Must match pattern: ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ (lowercase, digits, underscores, dots only; no hyphens)"
),
));
}
if name.len() > MAX_MODULE_ID_LENGTH {
return Err(ModuleError::new(
crate::errors::ErrorCode::GeneralInvalidInput,
format!(
"Module ID exceeds maximum length of {}: {}",
MAX_MODULE_ID_LENGTH,
name.len()
),
));
}
if !allow_reserved {
let first_segment = name.split('.').next().unwrap();
if RESERVED_WORDS.contains(&first_segment) {
return Err(ModuleError::new(
crate::errors::ErrorCode::GeneralInvalidInput,
format!("Module ID contains reserved word: '{first_segment}'"),
));
}
}
Ok(())
}
struct RegistryCore {
modules: HashMap<String, Arc<dyn Module>>,
descriptors: HashMap<String, ModuleDescriptor>,
ref_counts: HashMap<String, usize>,
draining: HashSet<String>,
lowercase_map: HashMap<String, String>,
schema_cache: HashMap<String, serde_json::Value>,
}
impl RegistryCore {
fn new() -> Self {
Self {
modules: HashMap::new(),
descriptors: HashMap::new(),
ref_counts: HashMap::new(),
draining: HashSet::new(),
lowercase_map: HashMap::new(),
schema_cache: HashMap::new(),
}
}
}
pub struct Registry {
core: RwLock<RegistryCore>,
callbacks: RwLock<CallbackMap>,
callback_counter: AtomicU64,
drain_events: RwLock<HashMap<String, Arc<tokio::sync::Notify>>>,
discoverer: RwLock<Option<Box<dyn Discoverer>>>,
validator: RwLock<Option<Arc<dyn ModuleValidator>>>,
extension_roots: RwLock<Vec<String>>,
watcher: parking_lot::Mutex<Option<notify::RecommendedWatcher>>,
watch_handle: parking_lot::Mutex<Option<tokio::task::JoinHandle<()>>>,
}
struct DiscovererRestoreGuard<'a> {
slot: &'a RwLock<Option<Box<dyn Discoverer>>>,
discoverer: Option<Box<dyn Discoverer>>,
}
impl Drop for DiscovererRestoreGuard<'_> {
fn drop(&mut self) {
if let Some(d) = self.discoverer.take() {
let mut slot = self.slot.write();
if slot.is_none() {
*slot = Some(d);
}
}
}
}
impl std::fmt::Debug for Registry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let core = self.core.read();
f.debug_struct("Registry")
.field("modules", &core.modules.keys().collect::<Vec<_>>())
.field("descriptors", &core.descriptors)
.field("ref_counts", &core.ref_counts)
.field("draining", &core.draining)
.field(
"drain_events_keys",
&self.drain_events.read().keys().cloned().collect::<Vec<_>>(),
)
.field(
"callbacks_keys",
&self.callbacks.read().keys().cloned().collect::<Vec<_>>(),
)
.field("lowercase_map", &core.lowercase_map)
.field(
"schema_cache_keys",
&core.schema_cache.keys().collect::<Vec<_>>(),
)
.field(
"discoverer",
&self.discoverer.read().as_ref().map(|_| "<Discoverer>"),
)
.field(
"validator",
&self.validator.read().as_ref().map(|_| "<Validator>"),
)
.finish_non_exhaustive()
}
}
impl Registry {
#[must_use]
pub fn new() -> Self {
Self {
core: RwLock::new(RegistryCore::new()),
callbacks: RwLock::new(HashMap::new()),
callback_counter: AtomicU64::new(1),
drain_events: RwLock::new(HashMap::new()),
discoverer: RwLock::new(None),
validator: RwLock::new(None),
extension_roots: RwLock::new(Vec::new()),
watcher: parking_lot::Mutex::new(None),
watch_handle: parking_lot::Mutex::new(None),
}
}
fn snapshot_callbacks(&self, event: &str) -> Vec<Arc<ModuleCallbackFn>> {
self.callbacks
.read()
.get(event)
.map(|v| v.iter().map(|(_, cb)| cb.clone()).collect())
.unwrap_or_default()
}
pub fn register(
&self,
name: &str,
module: Box<dyn Module>,
descriptor: ModuleDescriptor,
) -> Result<(), ModuleError> {
self.register_core(name, module, descriptor, false, true)
}
pub fn register_module(&self, name: &str, module: Box<dyn Module>) -> Result<(), ModuleError> {
let descriptor = ModuleDescriptor {
module_id: name.to_string(),
name: None,
description: module.description().to_string(),
documentation: None,
input_schema: module.input_schema(),
output_schema: module.output_schema(),
version: DEFAULT_MODULE_VERSION.to_string(),
tags: vec![],
annotations: Some(ModuleAnnotations::default()),
examples: vec![],
metadata: HashMap::new(),
display: None,
sunset_date: None,
dependencies: vec![],
enabled: true,
};
self.register(name, module, descriptor)
}
pub fn register_versioned(
&self,
name: &str,
module: Box<dyn Module>,
version: Option<&str>,
metadata: Option<HashMap<String, serde_json::Value>>,
) -> Result<(), ModuleError> {
let descriptor = ModuleDescriptor {
module_id: name.to_string(),
name: None,
description: module.description().to_string(),
documentation: None,
input_schema: module.input_schema(),
output_schema: module.output_schema(),
version: version
.map_or_else(|| DEFAULT_MODULE_VERSION.to_string(), ToString::to_string),
tags: module.tags(),
annotations: Some(ModuleAnnotations::default()),
examples: vec![],
metadata: metadata.unwrap_or_default(),
display: None,
sunset_date: None,
dependencies: vec![],
enabled: true,
};
self.register(name, module, descriptor)
}
pub fn unregister(&self, name: &str) -> Result<bool, ModuleError> {
let removed: Arc<dyn Module> = {
let mut core = self.core.write();
let Some(module) = core.modules.remove(name) else {
return Ok(false);
};
core.descriptors.remove(name);
core.lowercase_map.remove(&name.to_lowercase());
core.schema_cache.remove(name);
core.ref_counts.remove(name);
core.draining.remove(name);
module
};
self.drain_events.write().remove(name);
removed.on_unload();
for cb in self.snapshot_callbacks("unregister") {
cb(name, removed.as_ref());
}
Ok(true)
}
pub fn get(&self, name: &str) -> Result<Option<Arc<dyn Module>>, ModuleError> {
if name.is_empty() {
return Err(ModuleError::new(
crate::errors::ErrorCode::ModuleNotFound,
"Module ID must not be empty",
));
}
Ok(self.core.read().modules.get(name).cloned())
}
pub fn get_definition(&self, name: &str) -> Option<ModuleDescriptor> {
self.core.read().descriptors.get(name).cloned()
}
pub fn list(&self, tags: Option<&[&str]>, prefix: Option<&str>) -> Vec<String> {
let core = self.core.read();
let mut result: Vec<String> = core
.modules
.keys()
.filter(|name| {
if let Some(pfx) = prefix {
if !name.starts_with(pfx) {
return false;
}
}
if let Some(required_tags) = tags {
let mut module_tags: Vec<String> = core
.descriptors
.get(name.as_str())
.map(|desc| desc.tags.clone())
.unwrap_or_default();
if let Some(module) = core.modules.get(name.as_str()) {
for t in module.tags() {
if !module_tags.contains(&t) {
module_tags.push(t);
}
}
}
if !required_tags
.iter()
.all(|t| module_tags.contains(&t.to_string()))
{
return false;
}
}
true
})
.cloned()
.collect();
result.sort();
result
}
pub fn has(&self, name: &str) -> bool {
self.core.read().modules.contains_key(name)
}
#[allow(clippy::similar_names)] pub async fn discover(&self, discoverer: &dyn Discoverer) -> Result<usize, ModuleError> {
let discovered = discoverer.discover(&[]).await?;
Ok(self.register_discovered(discovered))
}
pub fn register_internal(
&self,
name: &str,
module: Box<dyn Module>,
descriptor: ModuleDescriptor,
) -> Result<(), ModuleError> {
self.register_core(name, module, descriptor, true, false)
}
fn register_core(
&self,
name: &str,
module: Box<dyn Module>,
descriptor: ModuleDescriptor,
allow_reserved: bool,
run_validator: bool,
) -> Result<(), ModuleError> {
validate_module_id(name, allow_reserved)?;
if run_validator {
let validator_snapshot = self.validator.read().as_ref().map(Arc::clone);
if let Some(validator) = validator_snapshot {
let result = validator.validate(module.as_ref(), Some(&descriptor));
if !result.valid {
return Err(ModuleError::new(
crate::errors::ErrorCode::ModuleLoadError,
format!(
"Module '{}' failed validation: {}",
name,
result.errors.join(", ")
),
));
}
}
}
let module_arc: Arc<dyn Module> = module.into();
let module_clone = Arc::clone(&module_arc);
{
let mut core = self.core.write();
let reserved: &[&str] = if allow_reserved { &[] } else { RESERVED_WORDS };
let existing_ids: HashSet<String> = core.modules.keys().cloned().collect();
if let Some(conflict) =
detect_id_conflicts(name, &existing_ids, reserved, Some(&core.lowercase_map))
{
match conflict.severity {
ConflictSeverity::Error => {
return Err(ModuleError::new(
crate::errors::ErrorCode::GeneralInvalidInput,
conflict.message,
));
}
ConflictSeverity::Warning => {
tracing::warn!(
module_id = %name,
conflict = %conflict.message,
"Module registration proceeded despite warning-level ID conflict"
);
}
}
}
let schema = serde_json::json!({
"input": descriptor.input_schema,
"output": descriptor.output_schema,
});
core.schema_cache.insert(name.to_string(), schema);
core.lowercase_map
.insert(name.to_lowercase(), name.to_string());
core.modules.insert(name.to_string(), module_arc);
core.descriptors.insert(name.to_string(), descriptor);
}
if let Err(e) = module_clone.on_load() {
let mut core = self.core.write();
core.schema_cache.remove(name);
core.lowercase_map.remove(&name.to_lowercase());
core.modules.remove(name);
core.descriptors.remove(name);
return Err(e);
}
for cb in self.snapshot_callbacks("register") {
cb(name, module_clone.as_ref());
}
Ok(())
}
pub fn for_each_module(&self, mut f: impl FnMut(&str, &dyn Module)) {
let core = self.core.read();
for (name, module) in &core.modules {
f(name.as_str(), module.as_ref());
}
}
pub fn describe(&self, name: &str) -> String {
match self.core.read().modules.get(name) {
Some(module) => module.description().to_string(),
None => "Module not found".to_string(),
}
}
pub async fn safe_unregister(&self, name: &str, timeout_ms: u64) -> Result<bool, ModuleError> {
let need_wait_notify: Option<Arc<tokio::sync::Notify>> = {
let mut core = self.core.write();
if !core.modules.contains_key(name) {
return Ok(false);
}
core.draining.insert(name.to_string());
let current_refs = core.ref_counts.get(name).copied().unwrap_or(0);
if current_refs == 0 {
None
} else {
let notify = Arc::new(tokio::sync::Notify::new());
self.drain_events
.write()
.insert(name.to_string(), Arc::clone(¬ify));
Some(notify)
}
};
match need_wait_notify {
None => {
self.unregister(name)?;
Ok(true)
}
Some(notify) => {
let result = tokio::time::timeout(
std::time::Duration::from_millis(timeout_ms),
notify.notified(),
)
.await;
let clean = result.is_ok();
if !clean {
let in_flight = self.core.read().ref_counts.get(name).copied().unwrap_or(0);
tracing::warn!(
"Force-unloading module '{}' after {}ms timeout ({} in-flight executions)",
name,
timeout_ms,
in_flight,
);
}
{
let mut core = self.core.write();
core.draining.remove(name);
}
self.drain_events.write().remove(name);
self.unregister(name)?;
Ok(clean)
}
}
}
pub fn acquire(&self, name: &str) -> Result<Arc<dyn Module>, ModuleError> {
let mut core = self.core.write();
if core.draining.contains(name) {
return Err(ModuleError::new(
crate::errors::ErrorCode::ModuleNotFound,
format!("Module '{name}' is draining"),
));
}
let module = core.modules.get(name).cloned().ok_or_else(|| {
ModuleError::new(
crate::errors::ErrorCode::ModuleNotFound,
format!("Module '{name}' not found"),
)
})?;
*core.ref_counts.entry(name.to_string()).or_insert(0) += 1;
Ok(module)
}
pub fn release(&self, name: &str) {
let should_notify = {
let mut core = self.core.write();
if let Some(count) = core.ref_counts.get_mut(name) {
if *count > 0 {
*count -= 1;
}
if *count == 0 {
core.ref_counts.remove(name);
true
} else {
false
}
} else {
false
}
};
if should_notify {
if let Some(notify) = self.drain_events.read().get(name) {
notify.notify_one();
}
}
}
#[deprecated(since = "0.20.0", note = "Use `acquire()` — it now ref-counts.")]
pub fn acquire_ref(&self, name: &str) -> Result<Arc<dyn Module>, ModuleError> {
self.acquire(name)
}
#[deprecated(since = "0.20.0", note = "Use `release()`.")]
pub fn release_ref(&self, name: &str) {
self.release(name);
}
pub fn is_draining(&self, name: &str) -> bool {
self.core.read().draining.contains(name)
}
pub fn on(&self, event: &str, callback: Box<ModuleCallbackFn>) -> u64 {
let id = self.callback_counter.fetch_add(1, Ordering::Relaxed);
self.callbacks
.write()
.entry(event.to_string())
.or_default()
.push((id, Arc::from(callback)));
id
}
pub fn off(&self, handle_id: u64) -> bool {
let mut callbacks = self.callbacks.write();
for entries in callbacks.values_mut() {
if let Some(pos) = entries.iter().position(|(id, _)| *id == handle_id) {
entries.remove(pos);
return true;
}
}
false
}
pub async fn reload(&self) -> Result<usize, ModuleError> {
self.discover_internal().await
}
#[allow(clippy::unused_async)] pub async fn watch(self: &Arc<Self>) -> Result<(), ModuleError> {
use notify::{RecursiveMode, Watcher};
{
let watcher_slot = self.watcher.lock();
if watcher_slot.is_some() {
return Ok(()); }
}
let extension_roots: Vec<String> = self.extension_roots.read().clone();
if extension_roots.is_empty() {
tracing::warn!(
"Registry::watch() called with no extension_roots — call set_extension_roots() first"
);
return Ok(());
}
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<notify::Result<notify::Event>>();
let mut watcher = notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
let _ = tx.send(res);
})
.map_err(|e| {
ModuleError::new(
crate::errors::ErrorCode::ReloadFailed,
format!("Failed to create file watcher: {e}"),
)
})?;
for root in &extension_roots {
let path = std::path::Path::new(root);
if let Err(e) = watcher.watch(path, RecursiveMode::Recursive) {
tracing::warn!(
root = %root,
error = %e,
"Registry::watch failed for root, skipping"
);
}
}
let weak = Arc::downgrade(self);
let handle = tokio::spawn(Self::watch_loop(rx, weak));
*self.watcher.lock() = Some(watcher);
*self.watch_handle.lock() = Some(handle);
Ok(())
}
async fn watch_loop(
mut rx: tokio::sync::mpsc::UnboundedReceiver<notify::Result<notify::Event>>,
weak: std::sync::Weak<Self>,
) {
use std::time::{Duration, Instant};
const DEBOUNCE: Duration = Duration::from_millis(300);
let mut last_trigger = Instant::now()
.checked_sub(DEBOUNCE)
.unwrap_or_else(Instant::now);
while let Some(res) = rx.recv().await {
let Ok(event) = res else {
continue;
};
match event.kind {
notify::EventKind::Create(_)
| notify::EventKind::Modify(_)
| notify::EventKind::Remove(_) => {}
_ => continue,
}
if last_trigger.elapsed() < DEBOUNCE {
continue;
}
last_trigger = Instant::now();
let Some(reg) = weak.upgrade() else {
break;
};
if let Err(e) = reg.discover_internal().await {
tracing::warn!(
error = %e.message,
"Registry watch: discover_internal failed during hot-reload"
);
}
}
}
pub fn unwatch(&self) {
self.watcher.lock().take();
if let Some(handle) = self.watch_handle.lock().take() {
handle.abort();
}
}
pub async fn discover_internal(&self) -> Result<usize, ModuleError> {
let discoverer_opt = self.discoverer.write().take();
let guard = DiscovererRestoreGuard {
slot: &self.discoverer,
discoverer: discoverer_opt,
};
let Some(active_discoverer) = guard.discoverer.as_ref() else {
return Err(ModuleError::new(
crate::errors::ErrorCode::NoDiscovererConfigured,
"No discoverer configured".to_string(),
));
};
let roots = self.extension_roots.read().clone();
let discover_result = active_discoverer.discover(&roots).await;
drop(guard);
let discovered = discover_result?;
Ok(self.register_discovered(discovered))
}
fn descriptor_schema_shape_is_valid(descriptor: &ModuleDescriptor) -> bool {
let input_ok = descriptor.input_schema.is_object() || descriptor.input_schema.is_null();
let output_ok = descriptor.output_schema.is_object() || descriptor.output_schema.is_null();
input_ok && output_ok
}
#[allow(clippy::too_many_lines)] fn register_discovered(&self, discovered: Vec<DiscoveredModule>) -> usize {
let mut registered_count = 0usize;
let mut post_insert: Vec<(String, Arc<dyn Module>)> = Vec::new();
for dm in discovered {
if let Err(e) = validate_module_id(&dm.name, false) {
tracing::warn!(
module_id = %dm.name,
error = %e.message,
"Discovered module rejected: invalid module_id"
);
continue;
}
let validator_snapshot = self.validator.read().as_ref().map(Arc::clone);
if let Some(validator) = validator_snapshot {
let result = validator.validate(dm.module.as_ref(), Some(&dm.descriptor));
if !result.valid {
tracing::warn!(
module_id = %dm.name,
errors = ?result.errors,
"Custom validator rejected discovered module"
);
continue;
}
}
if !Self::descriptor_schema_shape_is_valid(&dm.descriptor) {
tracing::warn!(
module_id = %dm.name,
"Discovered module descriptor has non-object schema shape — skipping"
);
continue;
}
let inserted = {
let mut core = self.core.write();
let existing_ids: HashSet<String> = core.modules.keys().cloned().collect();
match detect_id_conflicts(
&dm.name,
&existing_ids,
RESERVED_WORDS,
Some(&core.lowercase_map),
) {
Some(c) if c.severity == ConflictSeverity::Error => {
tracing::warn!(
module_id = %dm.name,
conflict = %c.message,
"Discovered module rejected: id conflict"
);
false
}
Some(c) => {
tracing::warn!(
module_id = %dm.name,
conflict = %c.message,
"Discovered module registered despite warning-level ID conflict"
);
let schema = serde_json::json!({
"input": dm.descriptor.input_schema.clone(),
"output": dm.descriptor.output_schema.clone(),
});
core.schema_cache.insert(dm.name.clone(), schema);
core.lowercase_map
.insert(dm.name.to_lowercase(), dm.name.clone());
core.modules.insert(dm.name.clone(), Arc::clone(&dm.module));
core.descriptors
.insert(dm.name.clone(), dm.descriptor.clone());
true
}
None => {
let schema = serde_json::json!({
"input": dm.descriptor.input_schema.clone(),
"output": dm.descriptor.output_schema.clone(),
});
core.schema_cache.insert(dm.name.clone(), schema);
core.lowercase_map
.insert(dm.name.to_lowercase(), dm.name.clone());
core.modules.insert(dm.name.clone(), Arc::clone(&dm.module));
core.descriptors
.insert(dm.name.clone(), dm.descriptor.clone());
true
}
}
};
if inserted {
post_insert.push((dm.name.clone(), dm.module));
registered_count += 1;
}
}
for (name, module_arc) in post_insert {
if let Err(e) = module_arc.on_load() {
tracing::error!(
module_id = %name,
error = %e.message,
"Discovered module on_load failed; rolling back registration"
);
let mut core = self.core.write();
core.schema_cache.remove(&name);
core.lowercase_map.remove(&name.to_lowercase());
core.modules.remove(&name);
core.descriptors.remove(&name);
registered_count = registered_count.saturating_sub(1);
continue;
}
for cb in self.snapshot_callbacks("register") {
cb(&name, module_arc.as_ref());
}
}
registered_count
}
pub fn set_discoverer(&self, discoverer: Box<dyn Discoverer>) {
*self.discoverer.write() = Some(discoverer);
}
pub fn set_extension_roots(&self, roots: Vec<String>) {
*self.extension_roots.write() = roots;
}
pub fn extension_roots(&self) -> Vec<String> {
self.extension_roots.read().clone()
}
pub fn set_validator(&self, validator: Box<dyn ModuleValidator>) {
*self.validator.write() = Some(validator.into());
}
pub fn count(&self) -> usize {
self.core.read().modules.len()
}
pub fn module_ids(&self) -> Vec<String> {
let mut ids: Vec<String> = self.core.read().modules.keys().cloned().collect();
ids.sort();
ids
}
pub fn entries(&self) -> Vec<(String, Arc<dyn Module>)> {
self.core
.read()
.modules
.iter()
.map(|(k, v)| (k.clone(), Arc::clone(v)))
.collect()
}
pub fn export_schema(&self, name: &str) -> Option<serde_json::Value> {
self.core.read().schema_cache.get(name).cloned()
}
pub fn export_schema_strict(&self, name: &str, strict: bool) -> Option<serde_json::Value> {
let descriptor = self.get_definition(name)?;
let (input_schema, output_schema) = if strict {
(
crate::schema::to_strict_schema(&descriptor.input_schema),
crate::schema::to_strict_schema(&descriptor.output_schema),
)
} else {
(
descriptor.input_schema.clone(),
descriptor.output_schema.clone(),
)
};
Some(serde_json::json!({
"module_id": descriptor.module_id,
"description": descriptor.description,
"input_schema": input_schema,
"output_schema": output_schema,
}))
}
pub fn disable(&self, name: &str) -> Result<(), ModuleError> {
let mut core = self.core.write();
let descriptor = core.descriptors.get_mut(name).ok_or_else(|| {
ModuleError::new(
crate::errors::ErrorCode::ModuleNotFound,
format!("Module '{name}' not found"),
)
})?;
descriptor.enabled = false;
Ok(())
}
pub fn enable(&self, name: &str) -> Result<(), ModuleError> {
let mut core = self.core.write();
let descriptor = core.descriptors.get_mut(name).ok_or_else(|| {
ModuleError::new(
crate::errors::ErrorCode::ModuleNotFound,
format!("Module '{name}' not found"),
)
})?;
descriptor.enabled = true;
Ok(())
}
pub fn is_enabled(&self, name: &str) -> Option<bool> {
self.core.read().descriptors.get(name).map(|d| d.enabled)
}
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}