use std::any::{Any, TypeId};
use std::cell::RefCell;
use std::collections::HashMap;
#[cfg(feature = "reload")]
use std::rc::Rc;
use std::sync::OnceLock;
use crate::core::{AutoBuilder, BuildFn};
use crate::error::TraitKitError;
use crate::i18n::tr;
#[cfg(feature = "encryption")]
use super::EncryptedBlob;
use super::TypeMap;
use super::{DependencyGraph, GraphError, ModuleEntry};
#[cfg(feature = "lifecycle")]
type ShutdownCallback = Box<dyn Fn(&TypeMap)>;
#[cfg(feature = "lifecycle")]
type ReadyCallback = Box<dyn Fn(&Kit<Ready>) -> Result<(), TraitKitError>>;
#[cfg(feature = "health")]
type HealthCheckerFn = Box<dyn Fn(&TypeMap) -> crate::core::health::HealthStatus>;
#[cfg(feature = "observer")]
type ObserverRef = std::sync::Arc<dyn crate::core::observer::BuildObserver>;
#[cfg(feature = "decorator")]
type DecoratorFn = Box<dyn Fn(Box<dyn Any>) -> Box<dyn Any>>;
#[cfg(feature = "encryption")]
const KEY_DERIVATION_VERSION: &str = "v1";
#[cfg(feature = "encryption")]
fn derive_kit_field_key(
master_key: &[u8],
path: &'static str,
context: &'static str,
) -> Result<[u8; 32], TraitKitError> {
super::config::derive_field_key(master_key, path, KEY_DERIVATION_VERSION).map_err(|e| {
TraitKitError::BuildFailed {
context: context.to_string(),
source: Box::new(e),
}
})
}
pub struct Unbuilt;
pub struct Ready;
#[cfg(feature = "reload")]
type SubscriberMap = RefCell<HashMap<TypeId, Vec<Rc<dyn Fn()>>>>;
#[cfg(feature = "encryption")]
type EncryptedConfigMap = RefCell<HashMap<TypeId, EncryptedBlob>>;
pub(crate) struct LazySlot {
pub(crate) builder: Option<BuildFn>,
pub(crate) cell: OnceLock<Box<dyn Any>>,
}
pub struct Kit<S = Unbuilt> {
builders: RefCell<HashMap<TypeId, BuildFn>>,
overrides: RefCell<HashMap<TypeId, Box<dyn Any>>>,
lazy_builders: RefCell<HashMap<TypeId, BuildFn>>,
lazy_slots: RefCell<HashMap<TypeId, LazySlot>>,
multi_builders: RefCell<HashMap<TypeId, Vec<BuildFn>>>,
multi_capabilities: RefCell<HashMap<TypeId, Vec<Box<dyn Any>>>>,
#[cfg(feature = "interface")]
interface_builders: RefCell<HashMap<TypeId, BuildFn>>,
graph: DependencyGraph,
configs: TypeMap,
capabilities: TypeMap,
#[cfg(feature = "reload")]
subscribers: SubscriberMap,
#[cfg(feature = "encryption")]
encrypted_configs: EncryptedConfigMap,
#[cfg(feature = "confers")]
config_snapshots: RefCell<HashMap<TypeId, Box<dyn Any>>>,
#[cfg(feature = "toggle")]
toggles: RefCell<HashMap<String, bool>>,
#[cfg(feature = "lifecycle")]
shutdown_callbacks: RefCell<Vec<(TypeId, ShutdownCallback)>>,
#[cfg(feature = "lifecycle")]
ready_callbacks: RefCell<Vec<(TypeId, ReadyCallback)>>,
#[cfg(feature = "health")]
health_checkers: RefCell<HashMap<TypeId, (/* module_name */ &'static str, HealthCheckerFn)>>,
#[cfg(feature = "observer")]
observers: RefCell<Vec<ObserverRef>>,
#[cfg(feature = "decorator")]
decorators: RefCell<HashMap<TypeId, Vec<DecoratorFn>>>,
#[cfg(feature = "decorator")]
decorator_module_to_cap: RefCell<HashMap<TypeId, TypeId>>,
_state: std::marker::PhantomData<S>,
}
impl Kit {
#[must_use]
pub fn new() -> Self {
Kit {
builders: RefCell::new(HashMap::new()),
overrides: RefCell::new(HashMap::new()),
lazy_builders: RefCell::new(HashMap::new()),
lazy_slots: RefCell::new(HashMap::new()),
multi_builders: RefCell::new(HashMap::new()),
multi_capabilities: RefCell::new(HashMap::new()),
#[cfg(feature = "interface")]
interface_builders: RefCell::new(HashMap::new()),
graph: DependencyGraph::new(),
configs: TypeMap::new(),
capabilities: TypeMap::new(),
#[cfg(feature = "reload")]
subscribers: RefCell::new(HashMap::new()),
#[cfg(feature = "encryption")]
encrypted_configs: RefCell::new(HashMap::new()),
#[cfg(feature = "confers")]
config_snapshots: RefCell::new(HashMap::new()),
#[cfg(feature = "toggle")]
toggles: RefCell::new(HashMap::new()),
#[cfg(feature = "lifecycle")]
shutdown_callbacks: RefCell::new(Vec::new()),
#[cfg(feature = "lifecycle")]
ready_callbacks: RefCell::new(Vec::new()),
#[cfg(feature = "health")]
health_checkers: RefCell::new(HashMap::new()),
#[cfg(feature = "observer")]
observers: RefCell::new(Vec::new()),
#[cfg(feature = "decorator")]
decorators: RefCell::new(HashMap::new()),
#[cfg(feature = "decorator")]
decorator_module_to_cap: RefCell::new(HashMap::new()),
_state: std::marker::PhantomData,
}
}
pub fn register<M: AutoBuilder>(&mut self) -> Result<(), TraitKitError> {
let entry = ModuleEntry {
type_id: TypeId::of::<M>(),
name: M::NAME,
dependencies: M::dependencies().iter().map(|(n, id)| (*n, *id)).collect(),
};
self.graph
.add(entry)
.map_err(|name| TraitKitError::AlreadyRegistered { module: name })?;
let build_fn: BuildFn = Box::new(|kit| {
let capability = M::build(kit)
.map_err(|e| -> Box<dyn std::error::Error + Send + 'static> { Box::new(e) })?;
Ok(Box::new(capability) as Box<dyn Any>)
});
self.builders
.borrow_mut()
.insert(TypeId::of::<M>(), build_fn);
Ok(())
}
pub fn register_lazy<M: AutoBuilder>(&mut self) -> Result<(), TraitKitError>
where
M::Capability: Clone + 'static,
{
let entry = ModuleEntry {
type_id: TypeId::of::<M>(),
name: M::NAME,
dependencies: M::dependencies().iter().map(|(n, id)| (*n, *id)).collect(),
};
self.graph
.add(entry)
.map_err(|name| TraitKitError::AlreadyRegistered { module: name })?;
let build_fn: BuildFn = Box::new(|kit| {
let capability = M::build(kit)
.map_err(|e| -> Box<dyn std::error::Error + Send + 'static> { Box::new(e) })?;
Ok(Box::new(capability) as Box<dyn Any>)
});
self.lazy_builders
.borrow_mut()
.insert(TypeId::of::<M>(), build_fn);
Ok(())
}
pub fn register_multi<M: AutoBuilder>(&mut self) -> Result<(), TraitKitError>
where
M::Capability: Clone + 'static,
{
let entry = ModuleEntry {
type_id: TypeId::of::<M>(),
name: M::NAME,
dependencies: M::dependencies().iter().map(|(n, id)| (*n, *id)).collect(),
};
self.graph
.add(entry)
.map_err(|name| TraitKitError::AlreadyRegistered { module: name })?;
let build_fn: BuildFn = Box::new(|kit| {
let capability = M::build(kit)
.map_err(|e| -> Box<dyn std::error::Error + Send + 'static> { Box::new(e) })?;
Ok(Box::new(capability) as Box<dyn Any>)
});
let cap_id = TypeId::of::<M::Capability>();
self.multi_builders
.borrow_mut()
.entry(cap_id)
.or_default()
.push(build_fn);
Ok(())
}
#[cfg(feature = "interface")]
pub fn register_as<M>(&mut self) -> Result<(), TraitKitError>
where
M: crate::core::InterfaceBuilder,
{
let interface_id = TypeId::of::<M::Interface>();
if self.interface_builders.borrow().contains_key(&interface_id) {
return Err(TraitKitError::AlreadyRegistered { module: M::NAME });
}
let entry = ModuleEntry {
type_id: TypeId::of::<M>(),
name: M::NAME,
dependencies: M::dependencies().iter().map(|(n, id)| (*n, *id)).collect(),
};
self.graph
.add(entry)
.map_err(|name| TraitKitError::AlreadyRegistered { module: name })?;
let build_fn: BuildFn = Box::new(|kit| {
let cap = M::build(kit)
.map_err(|e| -> Box<dyn std::error::Error + Send + 'static> { Box::new(e) })?;
let iface: std::sync::Arc<M::Interface> = M::into_interface(cap);
Ok(Box::new(iface) as Box<dyn Any>)
});
self.interface_builders
.borrow_mut()
.insert(interface_id, build_fn);
Ok(())
}
pub fn override_module<M: AutoBuilder>(&self, capability: M::Capability)
where
M::Capability: 'static,
{
self.overrides
.borrow_mut()
.insert(TypeId::of::<M>(), Box::new(capability));
}
pub fn override_module_strict<M: AutoBuilder>(
&mut self,
capability: M::Capability,
) -> Result<(), TraitKitError>
where
M::Capability: 'static,
{
for (dep_name, dep_id) in M::dependencies() {
if self.graph.name_of(*dep_id).is_none() {
return Err(TraitKitError::DependencyMissing {
module: M::NAME,
missing: dep_name,
});
}
}
self.overrides
.borrow_mut()
.insert(TypeId::of::<M>(), Box::new(capability));
Ok(())
}
pub fn set_config<C: Clone + 'static>(&self, config: C) {
self.configs.insert(config);
}
#[cfg(feature = "confers")]
pub fn load_config<C: super::Configurable>(&self) -> Result<(), TraitKitError> {
let config = C::load().map_err(|e| TraitKitError::BuildFailed {
context: "load_config".into(),
source: e,
})?;
self.set_config(config);
Ok(())
}
#[cfg(feature = "confers")]
pub fn load_and_validate<C>(&self) -> Result<(), TraitKitError>
where
C: super::Configurable + super::Validatable,
{
let config = C::load().map_err(|e| TraitKitError::BuildFailed {
context: "load_and_validate".into(),
source: e,
})?;
match config.validate() {
Ok(()) => {
self.set_config(config);
Ok(())
}
Err(errors) => Err(TraitKitError::BuildFailed {
context: "load_and_validate".into(),
source: Box::new(super::ValidationError { errors }),
}),
}
}
#[cfg(feature = "confers")]
pub fn snapshot_config<C: Clone + 'static>(&self) -> bool {
if let Some(config) = self.configs.get_cloned::<C>() {
self.config_snapshots
.borrow_mut()
.insert(TypeId::of::<C>(), Box::new(config));
true
} else {
false
}
}
#[cfg(feature = "confers")]
pub fn restore_config<C: Clone + 'static>(&self) -> Result<(), TraitKitError> {
let snapshots = self.config_snapshots.borrow();
let boxed =
snapshots
.get(&TypeId::of::<C>())
.ok_or_else(|| TraitKitError::MissingConfig {
key: format!("{} (snapshot)", std::any::type_name::<C>()),
})?;
let config =
boxed
.downcast_ref::<C>()
.cloned()
.ok_or_else(|| TraitKitError::MissingConfig {
key: format!("{} (snapshot downcast)", std::any::type_name::<C>()),
})?;
drop(snapshots);
self.set_config(config);
Ok(())
}
#[cfg(feature = "confers")]
pub fn has_snapshot<C: 'static>(&self) -> bool {
self.config_snapshots
.borrow()
.contains_key(&TypeId::of::<C>())
}
#[cfg(feature = "confers")]
pub fn load_config_with<C, S: std::hash::BuildHasher>(
&self,
vars: &std::collections::HashMap<String, String, S>,
) -> Result<(), TraitKitError>
where
C: super::Configurable + serde::Serialize + serde::de::DeserializeOwned,
{
let config = C::load().map_err(|e| TraitKitError::BuildFailed {
context: "load_config_with".into(),
source: e,
})?;
let mut json_value =
serde_json::to_value(&config).map_err(|e| TraitKitError::BuildFailed {
context: "load_config_with (serialize)".into(),
source: Box::new(e),
})?;
super::config::interpolate_json_value(&mut json_value, vars);
let interpolated: C =
serde_json::from_value(json_value).map_err(|e| TraitKitError::BuildFailed {
context: "load_config_with (deserialize)".into(),
source: Box::new(e),
})?;
self.set_config(interpolated);
Ok(())
}
pub fn build(self) -> Result<Kit<Ready>, TraitKitError> {
let sorted = match self.graph.validate() {
Ok(sorted) => sorted,
Err(GraphError::DependencyMissing { module, missing }) => {
return Err(TraitKitError::DependencyMissing { module, missing });
}
Err(GraphError::CycleDetected { cycle }) => {
return Err(TraitKitError::CycleDetected { cycle });
}
};
self.build_eager_modules(&sorted)?;
self.transfer_lazy_builders();
self.build_multi_bindings()?;
#[cfg(feature = "interface")]
self.build_interface_modules()?;
#[cfg(feature = "lifecycle")]
let ready_callbacks: Vec<(TypeId, ReadyCallback)> =
{ self.ready_callbacks.borrow_mut().drain(..).collect() };
#[cfg(feature = "lifecycle")]
let shutdown_callbacks: Vec<(TypeId, ShutdownCallback)> =
{ self.shutdown_callbacks.borrow_mut().drain(..).collect() };
let kit = Kit {
builders: self.builders,
overrides: self.overrides,
lazy_builders: self.lazy_builders,
lazy_slots: self.lazy_slots,
multi_builders: self.multi_builders,
multi_capabilities: self.multi_capabilities,
#[cfg(feature = "interface")]
interface_builders: self.interface_builders,
graph: self.graph,
configs: self.configs,
capabilities: self.capabilities,
#[cfg(feature = "reload")]
subscribers: self.subscribers,
#[cfg(feature = "encryption")]
encrypted_configs: self.encrypted_configs,
#[cfg(feature = "confers")]
config_snapshots: self.config_snapshots,
#[cfg(feature = "toggle")]
toggles: self.toggles,
#[cfg(feature = "lifecycle")]
shutdown_callbacks: RefCell::new(shutdown_callbacks),
#[cfg(feature = "lifecycle")]
ready_callbacks: RefCell::new(Vec::new()),
#[cfg(feature = "health")]
health_checkers: self.health_checkers,
#[cfg(feature = "observer")]
observers: self.observers,
#[cfg(feature = "decorator")]
decorators: self.decorators,
#[cfg(feature = "decorator")]
decorator_module_to_cap: self.decorator_module_to_cap,
_state: std::marker::PhantomData,
};
#[cfg(feature = "lifecycle")]
{
for (_type_id, callback) in &ready_callbacks {
callback(&kit)?;
}
}
Ok(kit)
}
fn build_eager_modules(&self, sorted: &[TypeId]) -> Result<(), TraitKitError> {
for type_id in sorted {
let module_name = self.module_name(*type_id);
if let Some(boxed) = self.overrides.borrow_mut().remove(type_id) {
self.capabilities.insert_boxed(*type_id, boxed);
continue;
}
if self.lazy_builders.borrow().contains_key(type_id) {
continue;
}
let Some(build_fn) = self.builders.borrow_mut().remove(type_id) else {
continue;
};
#[cfg(feature = "observer")]
{
let start_instant = std::time::Instant::now();
for obs in self.observers.borrow().iter() {
obs.on_module_start(module_name);
}
match (build_fn)(self) {
Ok(boxed) => {
let elapsed = start_instant.elapsed();
#[cfg(feature = "decorator")]
let boxed = {
let cap_type_id = self
.decorator_module_to_cap
.borrow()
.get(type_id)
.copied()
.unwrap_or(*type_id);
self.apply_decorators(cap_type_id, boxed)
};
self.capabilities.insert_boxed(*type_id, boxed);
for obs in self.observers.borrow().iter() {
obs.on_module_built(module_name, elapsed);
}
}
Err(e) => {
let err = TraitKitError::BuildFailed {
context: module_name.to_string(),
source: e,
};
for obs in self.observers.borrow().iter() {
obs.on_build_error(module_name, &err);
}
return Err(err);
}
}
}
#[cfg(not(feature = "observer"))]
{
match (build_fn)(self) {
Ok(boxed) => {
#[cfg(feature = "decorator")]
let boxed = {
let cap_type_id = self
.decorator_module_to_cap
.borrow()
.get(type_id)
.copied()
.unwrap_or(*type_id);
self.apply_decorators(cap_type_id, boxed)
};
self.capabilities.insert_boxed(*type_id, boxed);
}
Err(e) => {
return Err(TraitKitError::BuildFailed {
context: module_name.to_string(),
source: e,
});
}
}
}
}
let remaining: Vec<(TypeId, Box<dyn Any>)> = self.overrides.borrow_mut().drain().collect();
for (type_id, boxed) in remaining {
self.capabilities.insert_boxed(type_id, boxed);
}
Ok(())
}
fn transfer_lazy_builders(&self) {
let lazy: Vec<(TypeId, BuildFn)> = self.lazy_builders.borrow_mut().drain().collect();
self.lazy_slots.borrow_mut().reserve(lazy.len());
for (type_id, builder) in lazy {
self.lazy_slots.borrow_mut().insert(
type_id,
LazySlot {
builder: Some(builder),
cell: OnceLock::new(),
},
);
}
}
fn build_multi_bindings(&self) -> Result<(), TraitKitError> {
let multi: Vec<(TypeId, Vec<BuildFn>)> = self.multi_builders.borrow_mut().drain().collect();
for (cap_id, build_fns) in multi {
let mut vec = Vec::with_capacity(build_fns.len());
for build_fn in build_fns {
let boxed = (build_fn)(self).map_err(|e| TraitKitError::BuildFailed {
context: tr("trait-kit-diag-multi-binding", &[]),
source: e,
})?;
#[cfg(feature = "decorator")]
let boxed = self.apply_decorators(cap_id, boxed);
vec.push(boxed);
}
self.multi_capabilities.borrow_mut().insert(cap_id, vec);
}
Ok(())
}
#[cfg(feature = "interface")]
fn build_interface_modules(&self) -> Result<(), TraitKitError> {
let interfaces: Vec<(TypeId, BuildFn)> =
self.interface_builders.borrow_mut().drain().collect();
for (interface_id, build_fn) in interfaces {
let boxed = (build_fn)(self).map_err(|e| TraitKitError::BuildFailed {
context: tr("trait-kit-diag-interface", &[]),
source: e,
})?;
#[cfg(feature = "decorator")]
let boxed = self.apply_decorators(interface_id, boxed);
self.capabilities.insert_boxed(interface_id, boxed);
}
Ok(())
}
fn module_name(&self, type_id: TypeId) -> &'static str {
self.graph.name_of(type_id).unwrap_or("<unknown>")
}
#[cfg(feature = "lifecycle")]
pub fn register_lifecycle<M>(&mut self)
where
M: crate::core::lifecycle::Lifecycle + 'static,
M::Capability: 'static,
{
let shutdown_cb: ShutdownCallback = Box::new(|caps: &TypeMap| {
let type_id = TypeId::of::<M>();
if let Some((_guard, cap_ref)) = caps.get_ref_by_type_id::<M::Capability>(type_id) {
M::on_shutdown(cap_ref);
}
});
self.shutdown_callbacks
.borrow_mut()
.push((TypeId::of::<M>(), shutdown_cb));
let ready_cb: ReadyCallback = Box::new(|kit: &Kit<Ready>| {
M::on_ready(kit).map_err(|e| TraitKitError::LifecycleFailed {
context: M::NAME.to_string(),
source: Box::new(e),
})
});
self.ready_callbacks
.borrow_mut()
.push((TypeId::of::<M>(), ready_cb));
}
#[cfg(feature = "health")]
pub fn register_health_check<M>(&mut self)
where
M: crate::core::health::HealthCheck + 'static,
M::Capability: 'static,
{
let checker: HealthCheckerFn = Box::new(|caps: &TypeMap| {
let type_id = TypeId::of::<M>();
match caps.get_ref_by_type_id::<M::Capability>(type_id) {
Some((_guard, cap_ref)) => M::check(cap_ref),
None => crate::core::health::HealthStatus::Unhealthy {
detail: "capability not found".to_string(),
},
}
});
self.health_checkers
.borrow_mut()
.insert(TypeId::of::<M>(), (M::NAME, checker));
}
pub fn register_if<M: AutoBuilder>(
&mut self,
predicate: impl FnOnce(&Kit) -> bool,
) -> Result<bool, TraitKitError> {
if predicate(self) {
self.register::<M>()?;
Ok(true)
} else {
Ok(false)
}
}
#[cfg(feature = "toggle")]
pub fn enable_toggle(&self, key: impl Into<String>, enabled: bool) {
self.toggles.borrow_mut().insert(key.into(), enabled);
}
#[cfg(feature = "toggle")]
pub fn is_toggle_enabled(&self, key: &str) -> bool {
self.toggles.borrow().get(key).copied().unwrap_or(false)
}
#[cfg(feature = "toggle")]
pub fn register_if_toggle<M: AutoBuilder>(&mut self, key: &str) -> Result<bool, TraitKitError> {
let enabled = self.is_toggle_enabled(key);
if enabled {
self.register::<M>()?;
}
Ok(enabled)
}
#[cfg(feature = "observer")]
pub fn with_observer(
&mut self,
observer: std::sync::Arc<dyn crate::core::observer::BuildObserver>,
) {
self.observers.borrow_mut().push(observer);
}
#[cfg(feature = "decorator")]
pub fn decorate<M: AutoBuilder>(
&self,
decorator: impl Fn(M::Capability) -> M::Capability + 'static,
) where
M::Capability: 'static,
{
let wrapper: DecoratorFn = Box::new(move |boxed_cap| {
let cap = boxed_cap
.downcast::<M::Capability>()
.expect("decorator type mismatch");
let decorated = decorator(*cap);
Box::new(decorated) as Box<dyn Any>
});
self.decorators
.borrow_mut()
.entry(TypeId::of::<M::Capability>())
.or_default()
.push(wrapper);
self.decorator_module_to_cap
.borrow_mut()
.insert(TypeId::of::<M>(), TypeId::of::<M::Capability>());
}
}
impl<S> Kit<S> {
#[cfg(feature = "decorator")]
fn apply_decorators(&self, cap_type_id: TypeId, boxed: Box<dyn Any>) -> Box<dyn Any> {
let decorators = self.decorators.borrow();
let Some(dec_list) = decorators.get(&cap_type_id) else {
return boxed;
};
let mut current = boxed;
for dec in dec_list {
current = dec(current);
}
current
}
pub fn require<M: AutoBuilder>(&self) -> Result<M::Capability, TraitKitError> {
let type_id = TypeId::of::<M>();
if let Some(cap) = self
.capabilities
.get_cloned_by_type_id::<M::Capability>(type_id)
{
return Ok(cap);
}
if let Some(cached) = Self::get_lazy_cached::<M>(self, type_id) {
return Ok(cached);
}
let builder = self
.lazy_slots
.borrow_mut()
.get_mut(&type_id)
.and_then(|slot| slot.builder.take());
if let Some(builder) = builder {
const _: () = assert!(
std::mem::size_of::<Kit<Ready>>() == std::mem::size_of::<Kit>(),
"Kit layout changed; unsafe cast is no longer sound"
);
#[allow(unsafe_code)]
let kit_ref: &Kit = unsafe { &*std::ptr::from_ref(self).cast::<Kit>() };
let boxed = (builder)(kit_ref).map_err(|e| TraitKitError::BuildFailed {
context: M::NAME.to_string(),
source: e,
})?;
#[cfg(feature = "decorator")]
let boxed = self.apply_decorators(TypeId::of::<M::Capability>(), boxed);
if let Some(slot) = self.lazy_slots.borrow().get(&type_id) {
let _ = slot.cell.set(boxed);
}
return Self::get_lazy_cached::<M>(self, type_id).ok_or(
TraitKitError::MissingCapability {
key: M::NAME.to_string(),
},
);
}
Err(TraitKitError::MissingCapability {
key: M::NAME.to_string(),
})
}
fn get_lazy_cached<M: AutoBuilder>(&self, type_id: TypeId) -> Option<M::Capability> {
self.lazy_slots
.borrow()
.get(&type_id)
.and_then(|slot| slot.cell.get())
.and_then(|b| b.downcast_ref::<M::Capability>().cloned())
}
pub fn require_all<M: AutoBuilder>(&self) -> Result<Vec<M::Capability>, TraitKitError>
where
M::Capability: Clone + 'static,
{
let cap_id = TypeId::of::<M::Capability>();
let multi = self.multi_capabilities.borrow();
let vec = multi.get(&cap_id).ok_or(TraitKitError::MissingCapability {
key: M::NAME.to_string(),
})?;
let mut result = Vec::with_capacity(vec.len());
for boxed in vec {
let cap = boxed.downcast_ref::<M::Capability>().cloned().ok_or(
TraitKitError::MissingCapability {
key: M::NAME.to_string(),
},
)?;
result.push(cap);
}
Ok(result)
}
pub fn config<C: Clone + 'static>(&self) -> Result<C, TraitKitError> {
self.configs
.get_cloned::<C>()
.ok_or(TraitKitError::MissingConfig {
key: std::any::type_name::<C>().to_string(),
})
}
#[cfg(feature = "reload")]
pub fn subscribe<C: 'static>(&self, callback: impl Fn() + 'static) {
let callback: Rc<dyn Fn()> = Rc::new(callback);
self.subscribers
.borrow_mut()
.entry(TypeId::of::<C>())
.or_default()
.push(callback);
}
#[cfg(feature = "reload")]
pub fn reload_config<C: super::Configurable>(&self) -> Result<(), TraitKitError> {
let config = C::load().map_err(|e| TraitKitError::BuildFailed {
context: "reload_config".into(),
source: e,
})?;
self.configs.insert(config);
let callbacks: Vec<Rc<dyn Fn()>> = match self.subscribers.borrow().get(&TypeId::of::<C>()) {
Some(subs) => subs.iter().map(Rc::clone).collect(),
None => Vec::new(),
};
for cb in &callbacks {
cb();
}
Ok(())
}
#[cfg(feature = "interface")]
pub fn resolve<I>(&self) -> Result<std::sync::Arc<I>, TraitKitError>
where
I: ?Sized + 'static,
{
let interface_id = TypeId::of::<I>();
self.capabilities
.get_cloned_by_type_id::<std::sync::Arc<I>>(interface_id)
.ok_or(TraitKitError::MissingCapability {
key: "interface".into(),
})
}
}
impl Kit {
#[cfg(feature = "encryption")]
pub fn set_encrypted<C>(&self, value: &C, master_key: &[u8]) -> Result<(), TraitKitError>
where
C: super::ModuleConfig + serde::Serialize,
{
use super::XChaCha20Crypto;
if master_key.len() < 16 {
return Err(TraitKitError::BuildFailed {
context: "set_encrypted".into(),
source: Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"master_key must be at least 16 bytes, got {}",
master_key.len()
),
)),
});
}
let plaintext = serde_json::to_vec(value).map_err(|e| TraitKitError::BuildFailed {
context: "set_encrypted".into(),
source: Box::new(e),
})?;
let field_key = derive_kit_field_key(master_key, C::PATH, "set_encrypted")?;
let (nonce, ciphertext) = XChaCha20Crypto::new()
.encrypt(&plaintext, &field_key)
.map_err(|e| TraitKitError::BuildFailed {
context: "set_encrypted".into(),
source: Box::new(e),
})?;
self.encrypted_configs
.borrow_mut()
.insert(TypeId::of::<C>(), EncryptedBlob::new(nonce, ciphertext));
Ok(())
}
#[cfg(feature = "encryption")]
pub fn contains_encrypted<C: super::ModuleConfig>(&self) -> bool {
self.encrypted_configs
.borrow()
.contains_key(&TypeId::of::<C>())
}
#[cfg(feature = "confers")]
pub fn load_config_or_default<C>(&self) -> Result<bool, TraitKitError>
where
C: super::Configurable + super::ModuleConfig,
{
match C::load() {
Ok(value) => {
self.set_config(value);
Ok(true)
}
Err(_e) => {
self.set_config(C::default_value());
Ok(false)
}
}
}
}
impl Kit<Ready> {
pub fn optional<M: AutoBuilder>(&self) -> Option<M::Capability> {
let type_id = TypeId::of::<M>();
self.capabilities
.get_cloned_by_type_id::<M::Capability>(type_id)
}
pub fn require_ref<M: AutoBuilder>(
&self,
) -> Result<std::cell::Ref<'_, M::Capability>, TraitKitError>
where
M::Capability: 'static,
{
use std::cell::Ref;
let type_id = TypeId::of::<M>();
if !self.capabilities.contains_by_type_id(type_id) {
return Err(TraitKitError::MissingCapability {
key: M::NAME.to_string(),
});
}
Ref::filter_map(self.capabilities.inner_ref(), |map| {
map.get(&type_id)
.and_then(|b| b.downcast_ref::<M::Capability>())
})
.map_err(|_| TraitKitError::MissingCapability {
key: M::NAME.to_string(),
})
}
pub fn contains<M: AutoBuilder>(&self) -> bool {
self.capabilities.contains_by_type_id(TypeId::of::<M>())
}
pub fn contains_config<C: Clone + 'static>(&self) -> bool {
self.configs.contains::<C>()
}
#[cfg(feature = "toggle")]
pub fn is_toggle_enabled(&self, key: &str) -> bool {
self.toggles.borrow().get(key).copied().unwrap_or(false)
}
#[cfg(feature = "toggle")]
pub fn enable_toggle(&self, key: impl Into<String>, enabled: bool) {
self.toggles.borrow_mut().insert(key.into(), enabled);
}
#[cfg(feature = "lifecycle")]
pub fn shutdown(&self) {
let callbacks: Vec<(TypeId, ShutdownCallback)> =
self.shutdown_callbacks.borrow_mut().drain(..).collect();
for (_type_id, callback) in callbacks.iter().rev() {
callback(&self.capabilities);
}
}
#[cfg(feature = "health")]
pub fn health_check<M: crate::core::health::HealthCheck>(
&self,
) -> Result<crate::core::health::HealthStatus, TraitKitError> {
let type_id = TypeId::of::<M>();
let checkers = self.health_checkers.borrow();
let (_name, checker) = checkers.get(&type_id).ok_or(TraitKitError::MissingConfig {
key: M::NAME.to_string(),
})?;
Ok(checker(&self.capabilities))
}
#[cfg(feature = "health")]
pub fn health_report(&self) -> Vec<(&'static str, crate::core::health::HealthStatus)> {
let checkers = self.health_checkers.borrow();
checkers
.values()
.map(|(name, checker)| (*name, checker(&self.capabilities)))
.collect()
}
pub fn factory<M: AutoBuilder>(
&self,
) -> impl Fn() -> Result<M::Capability, TraitKitError> + '_ {
move || {
#[allow(unsafe_code)]
let kit_ref: &Kit = unsafe { &*std::ptr::from_ref::<Kit<Ready>>(self).cast::<Kit>() };
M::build(kit_ref).map_err(|e| TraitKitError::BuildFailed {
context: M::NAME.to_string(),
source: Box::new(e),
})
}
}
#[cfg(feature = "scope")]
#[must_use]
pub fn create_scope(&self) -> super::scope::Scope {
super::scope::Scope::new()
}
#[must_use]
pub fn graph_dot(&self) -> String {
self.graph.to_dot()
}
#[must_use]
pub fn graph_mermaid(&self) -> String {
self.graph.to_mermaid()
}
#[cfg(feature = "encryption")]
pub fn get_encrypted<C>(&self, master_key: &[u8]) -> Result<C, TraitKitError>
where
C: super::ModuleConfig + serde::de::DeserializeOwned,
{
use super::XChaCha20Crypto;
if master_key.len() < 16 {
return Err(TraitKitError::BuildFailed {
context: "get_encrypted".into(),
source: Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"master_key must be at least 16 bytes, got {}",
master_key.len()
),
)),
});
}
let blob = self
.encrypted_configs
.borrow()
.get(&TypeId::of::<C>())
.cloned()
.ok_or(TraitKitError::MissingConfig {
key: std::any::type_name::<C>().to_string(),
})?;
let field_key = derive_kit_field_key(master_key, C::PATH, "get_encrypted")?;
let plaintext = XChaCha20Crypto::new()
.decrypt(blob.nonce(), blob.ciphertext(), &field_key)
.map_err(|e| TraitKitError::BuildFailed {
context: "get_encrypted".into(),
source: Box::new(e),
})?;
serde_json::from_slice(&plaintext).map_err(|e| TraitKitError::BuildFailed {
context: "get_encrypted".into(),
source: Box::new(e),
})
}
}
impl Default for Kit {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for Kit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Kit<Unbuilt>")
.field("modules", &self.graph.entries().len())
.field("configs", &self.configs.len())
.finish()
}
}
impl std::fmt::Debug for Kit<Ready> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Kit<Ready>")
.field("modules", &self.graph.entries().len())
.field("configs", &self.configs.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{AutoBuilder, ModuleMeta};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
struct MockCapability;
impl ModuleMeta for MockCapability {
const NAME: &'static str = "mock";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl AutoBuilder for MockCapability {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(AtomicUsize::new(0)))
}
}
struct DependentModule;
impl ModuleMeta for DependentModule {
const NAME: &'static str = "dependent";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
static DEPS: &[(&str, std::any::TypeId)] =
&[("mock", std::any::TypeId::of::<MockCapability>())];
DEPS
}
}
impl AutoBuilder for DependentModule {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(AtomicUsize::new(0)))
}
}
#[test]
fn overrides_field_is_empty_on_new() {
let kit = Kit::new();
assert_eq!(kit.overrides.borrow().len(), 0);
}
#[test]
fn overrides_field_is_empty_after_build() {
let kit = Kit::new();
assert_eq!(kit.overrides.borrow().len(), 0);
}
#[test]
fn override_module_inserts_into_overrides_map() {
let kit = Kit::new();
assert_eq!(kit.overrides.borrow().len(), 0);
kit.override_module::<MockCapability>(Arc::new(AtomicUsize::new(42)));
assert_eq!(kit.overrides.borrow().len(), 1);
}
#[test]
fn override_module_strict_succeeds_when_deps_registered() {
let mut kit = Kit::new();
kit.register::<MockCapability>().unwrap();
let result = kit.override_module_strict::<DependentModule>(Arc::new(AtomicUsize::new(99)));
assert!(result.is_ok());
assert_eq!(kit.overrides.borrow().len(), 1);
}
#[test]
fn override_module_strict_fails_when_deps_missing() {
let mut kit = Kit::new();
let result = kit.override_module_strict::<DependentModule>(Arc::new(AtomicUsize::new(99)));
assert!(matches!(
result,
Err(TraitKitError::DependencyMissing {
module: "dependent",
missing: "mock"
})
));
assert_eq!(kit.overrides.borrow().len(), 0);
}
struct CountingModule;
impl ModuleMeta for CountingModule {
const NAME: &'static str = "counting";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl AutoBuilder for CountingModule {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(AtomicUsize::new(0)))
}
}
#[test]
fn build_uses_override_and_skips_build_fn() {
let kit = Kit::new();
let mut kit = kit;
kit.register::<CountingModule>().unwrap();
kit.override_module::<CountingModule>(Arc::new(AtomicUsize::new(42)));
let built = kit.build().unwrap();
let cap = built.require::<CountingModule>().unwrap();
assert_eq!(cap.load(Ordering::SeqCst), 42);
}
#[test]
fn build_uses_build_fn_when_no_override() {
let mut kit = Kit::new();
kit.register::<CountingModule>().unwrap();
let built = kit.build().unwrap();
let cap = built.require::<CountingModule>().unwrap();
assert_eq!(cap.load(Ordering::SeqCst), 0);
}
#[test]
fn build_inserts_unregistered_override_after_topo_loop() {
let kit = Kit::new();
kit.override_module::<MockCapability>(Arc::new(AtomicUsize::new(77)));
let built = kit.build().unwrap();
let cap = built.require::<MockCapability>().unwrap();
assert_eq!(cap.load(Ordering::SeqCst), 77);
}
#[test]
fn require_ref_returns_reference_to_built_capability() {
let mut kit = Kit::new();
kit.register::<CountingModule>().unwrap();
let built = kit.build().unwrap();
let r = built.require_ref::<CountingModule>().unwrap();
assert_eq!((*r).load(Ordering::SeqCst), 0);
}
#[test]
fn require_ref_returns_override_value() {
let mut kit = Kit::new();
kit.register::<CountingModule>().unwrap();
kit.override_module::<CountingModule>(Arc::new(AtomicUsize::new(55)));
let built = kit.build().unwrap();
let r = built.require_ref::<CountingModule>().unwrap();
assert_eq!((*r).load(Ordering::SeqCst), 55);
}
#[test]
fn require_ref_returns_missing_capability_for_unbuilt() {
let kit = Kit::new();
let built = kit.build().unwrap();
let result = built.require_ref::<CountingModule>();
assert!(matches!(
result,
Err(TraitKitError::MissingCapability { ref key }) if key == "counting"
));
}
#[test]
fn register_lazy_does_not_build_during_build() {
let mut kit = Kit::new();
kit.register_lazy::<CountingModule>().unwrap();
let built = kit.build().unwrap();
assert!(!built.contains::<CountingModule>());
}
#[test]
fn register_lazy_adds_to_dependency_graph() {
let mut kit = Kit::new();
kit.register::<MockCapability>().unwrap();
kit.register_lazy::<DependentModule>().unwrap();
let built = kit.build().unwrap();
assert!(built.contains::<MockCapability>());
assert!(!built.contains::<DependentModule>());
}
#[test]
fn register_lazy_returns_already_registered_for_duplicate() {
let mut kit = Kit::new();
kit.register_lazy::<CountingModule>().unwrap();
let result = kit.register_lazy::<CountingModule>();
assert!(matches!(
result,
Err(TraitKitError::AlreadyRegistered { module: "counting" })
));
}
#[test]
fn lazy_slots_empty_on_new_kit() {
let kit = Kit::new();
assert_eq!(kit.lazy_slots.borrow().len(), 0);
}
#[test]
fn build_transfers_lazy_builders_to_lazy_slots() {
let mut kit = Kit::new();
kit.register_lazy::<CountingModule>().unwrap();
assert_eq!(kit.lazy_builders.borrow().len(), 1);
assert_eq!(kit.lazy_slots.borrow().len(), 0);
let built = kit.build().unwrap();
assert_eq!(built.lazy_builders.borrow().len(), 0);
assert_eq!(built.lazy_slots.borrow().len(), 1);
assert!(
built
.lazy_slots
.borrow()
.contains_key(&TypeId::of::<CountingModule>())
);
}
#[test]
fn lazy_slots_cells_empty_after_build() {
let mut kit = Kit::new();
kit.register_lazy::<CountingModule>().unwrap();
let built = kit.build().unwrap();
let slots = built.lazy_slots.borrow();
let slot = slots
.get(&TypeId::of::<CountingModule>())
.expect("slot exists");
assert!(slot.cell.get().is_none());
}
#[test]
fn build_transfers_multiple_lazy_builders_to_lazy_slots() {
let mut kit = Kit::new();
kit.register::<MockCapability>().unwrap();
kit.register_lazy::<DependentModule>().unwrap();
kit.register_lazy::<CountingModule>().unwrap();
assert_eq!(kit.lazy_builders.borrow().len(), 2);
let built = kit.build().unwrap();
assert_eq!(built.lazy_builders.borrow().len(), 0);
assert_eq!(built.lazy_slots.borrow().len(), 2);
assert!(
built
.lazy_slots
.borrow()
.contains_key(&TypeId::of::<DependentModule>())
);
assert!(
built
.lazy_slots
.borrow()
.contains_key(&TypeId::of::<CountingModule>())
);
}
#[test]
fn require_triggers_lazy_construction_on_first_access() {
let mut kit = Kit::new();
kit.register_lazy::<CountingModule>().unwrap();
let built = kit.build().unwrap();
assert!(!built.contains::<CountingModule>());
let cap = built.require::<CountingModule>().unwrap();
assert_eq!(cap.load(Ordering::SeqCst), 0);
}
#[test]
fn require_does_not_rebuild_lazy_on_second_call() {
static COUNT: AtomicUsize = AtomicUsize::new(0);
struct CountedModule;
impl ModuleMeta for CountedModule {
const NAME: &'static str = "test-counted";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl AutoBuilder for CountedModule {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
let n = COUNT.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(AtomicUsize::new(n)))
}
}
COUNT.store(0, Ordering::SeqCst);
let mut kit = Kit::new();
kit.register_lazy::<CountedModule>().unwrap();
let built = kit.build().unwrap();
let cap1 = built.require::<CountedModule>().unwrap();
let cap2 = built.require::<CountedModule>().unwrap();
assert_eq!(
cap1.load(Ordering::SeqCst),
0,
"first require returns count 0"
);
assert_eq!(
cap2.load(Ordering::SeqCst),
0,
"second require returns same count"
);
assert_eq!(
COUNT.load(Ordering::SeqCst),
1,
"builder invoked exactly once"
);
}
#[test]
fn require_lazy_with_registered_dependency_succeeds() {
struct LazyDependentModule;
impl ModuleMeta for LazyDependentModule {
const NAME: &'static str = "lazy-dependent";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
static DEPS: &[(&str, std::any::TypeId)] =
&[("mock", std::any::TypeId::of::<MockCapability>())];
DEPS
}
}
impl AutoBuilder for LazyDependentModule {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(kit: &Kit) -> Result<Self::Capability, Self::Error> {
let mock = kit.require::<MockCapability>()?;
Ok(Arc::new(AtomicUsize::new(
mock.load(Ordering::SeqCst) + 100,
)))
}
}
let mut kit = Kit::new();
kit.register::<MockCapability>().unwrap();
kit.override_module::<MockCapability>(Arc::new(AtomicUsize::new(42)));
kit.register_lazy::<LazyDependentModule>().unwrap();
let built = kit.build().unwrap();
let cap = built.require::<LazyDependentModule>().unwrap();
assert_eq!(
cap.load(Ordering::SeqCst),
142,
"lazy build accessed eager dep (42 + 100)"
);
}
struct MultiModuleA;
impl ModuleMeta for MultiModuleA {
const NAME: &'static str = "multi-a";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl AutoBuilder for MultiModuleA {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(AtomicUsize::new(10)))
}
}
struct MultiModuleB;
impl ModuleMeta for MultiModuleB {
const NAME: &'static str = "multi-b";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl AutoBuilder for MultiModuleB {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(AtomicUsize::new(20)))
}
}
struct MultiModuleC;
impl ModuleMeta for MultiModuleC {
const NAME: &'static str = "multi-c";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl AutoBuilder for MultiModuleC {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(AtomicUsize::new(30)))
}
}
#[test]
fn multi_builders_empty_on_new_kit() {
let kit = Kit::new();
assert_eq!(kit.multi_builders.borrow().len(), 0);
}
#[test]
fn register_multi_adds_to_multi_builders() {
let mut kit = Kit::new();
assert_eq!(kit.multi_builders.borrow().len(), 0);
kit.register_multi::<MultiModuleA>().unwrap();
let cap_id = TypeId::of::<Arc<AtomicUsize>>();
assert_eq!(kit.multi_builders.borrow().len(), 1);
assert!(kit.multi_builders.borrow().contains_key(&cap_id));
assert_eq!(
kit.multi_builders.borrow().get(&cap_id).unwrap().len(),
1,
"first register_multi should produce Vec of length 1"
);
}
#[test]
fn register_multi_three_times_appends_to_vec() {
let mut kit = Kit::new();
kit.register_multi::<MultiModuleA>().unwrap();
kit.register_multi::<MultiModuleB>().unwrap();
kit.register_multi::<MultiModuleC>().unwrap();
let cap_id = TypeId::of::<Arc<AtomicUsize>>();
let builders = kit.multi_builders.borrow();
let vec = builders.get(&cap_id).expect("cap_id exists");
assert_eq!(
vec.len(),
3,
"three register_multi calls should produce Vec of length 3"
);
}
#[test]
fn register_multi_adds_module_to_dependency_graph() {
let mut kit = Kit::new();
kit.register_multi::<MultiModuleA>().unwrap();
assert!(kit.graph.name_of(TypeId::of::<MultiModuleA>()).is_some());
}
#[test]
fn register_multi_returns_already_registered_for_duplicate_module() {
let mut kit = Kit::new();
kit.register_multi::<MultiModuleA>().unwrap();
let result = kit.register_multi::<MultiModuleA>();
assert!(matches!(
result,
Err(TraitKitError::AlreadyRegistered { module: "multi-a" })
));
}
#[test]
fn register_multi_returns_already_registered_if_already_registered_via_register() {
let mut kit = Kit::new();
kit.register::<MockCapability>().unwrap();
let result = kit.register_multi::<MockCapability>();
assert!(matches!(
result,
Err(TraitKitError::AlreadyRegistered { module: "mock" })
));
}
#[test]
fn register_multi_coexists_with_register_for_different_modules() {
let mut kit = Kit::new();
kit.register::<MockCapability>().unwrap();
kit.register_multi::<MultiModuleA>().unwrap();
kit.register_multi::<MultiModuleB>().unwrap();
assert!(
kit.builders
.borrow()
.contains_key(&TypeId::of::<MockCapability>())
);
let cap_id = TypeId::of::<Arc<AtomicUsize>>();
assert_eq!(kit.multi_builders.borrow().get(&cap_id).unwrap().len(), 2);
}
#[test]
fn require_all_returns_empty_for_unregistered_capability() {
let mut kit = Kit::new();
kit.register::<MockCapability>().unwrap();
let built = kit.build().unwrap();
let result = built.require_all::<MultiModuleA>();
assert!(matches!(
result,
Err(TraitKitError::MissingCapability { ref key }) if key == "multi-a"
));
}
#[test]
fn require_all_returns_vec_of_three_after_three_register_multi() {
let mut kit = Kit::new();
kit.register_multi::<MultiModuleA>().unwrap();
kit.register_multi::<MultiModuleB>().unwrap();
kit.register_multi::<MultiModuleC>().unwrap();
let built = kit.build().unwrap();
let caps = built.require_all::<MultiModuleA>().unwrap();
assert_eq!(
caps.len(),
3,
"three register_multi calls should return Vec of length 3"
);
}
#[test]
fn require_all_preserves_registration_order() {
let mut kit = Kit::new();
kit.register_multi::<MultiModuleA>().unwrap(); kit.register_multi::<MultiModuleB>().unwrap(); kit.register_multi::<MultiModuleC>().unwrap(); let built = kit.build().unwrap();
let caps = built.require_all::<MultiModuleA>().unwrap();
assert_eq!(caps.len(), 3);
assert_eq!(
caps[0].load(Ordering::SeqCst),
10,
"first cap should be 10 (MultiModuleA)"
);
assert_eq!(
caps[1].load(Ordering::SeqCst),
20,
"second cap should be 20 (MultiModuleB)"
);
assert_eq!(
caps[2].load(Ordering::SeqCst),
30,
"third cap should be 30 (MultiModuleC)"
);
}
#[test]
fn require_all_returns_missing_capability_before_build() {
let mut kit = Kit::new();
kit.register_multi::<MultiModuleA>().unwrap();
let result = kit.require_all::<MultiModuleA>();
assert!(matches!(
result,
Err(TraitKitError::MissingCapability { ref key }) if key == "multi-a"
));
}
#[test]
fn build_drains_multi_builders_into_multi_capabilities() {
let mut kit = Kit::new();
kit.register_multi::<MultiModuleA>().unwrap();
kit.register_multi::<MultiModuleB>().unwrap();
assert_eq!(kit.multi_builders.borrow().len(), 1); assert_eq!(kit.multi_capabilities.borrow().len(), 0);
let built = kit.build().unwrap();
assert_eq!(built.multi_builders.borrow().len(), 0);
assert_eq!(built.multi_capabilities.borrow().len(), 1);
let cap_id = TypeId::of::<Arc<AtomicUsize>>();
assert_eq!(
built
.multi_capabilities
.borrow()
.get(&cap_id)
.unwrap()
.len(),
2
);
}
#[test]
fn require_all_coexists_with_require_for_single_binding() {
let mut kit = Kit::new();
kit.register::<MockCapability>().unwrap();
kit.register_multi::<MultiModuleA>().unwrap();
kit.register_multi::<MultiModuleB>().unwrap();
let built = kit.build().unwrap();
let single = built.require::<MockCapability>().unwrap();
assert_eq!(single.load(Ordering::SeqCst), 0);
let multi = built.require_all::<MultiModuleA>().unwrap();
assert_eq!(multi.len(), 2);
assert_eq!(multi[0].load(Ordering::SeqCst), 10);
assert_eq!(multi[1].load(Ordering::SeqCst), 20);
}
#[test]
fn multi_binding_build_error_returns_build_failed() {
struct FailMultiModule;
impl ModuleMeta for FailMultiModule {
const NAME: &'static str = "fail-multi";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for FailMultiModule {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<AtomicUsize>, TraitKitError> {
Err(TraitKitError::BuildFailed {
context: "fail-multi".into(),
source: Box::new(std::io::Error::other("multi fail")),
})
}
}
let mut kit = Kit::new();
kit.register_multi::<FailMultiModule>().unwrap();
let result = kit.build();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TraitKitError::BuildFailed { .. }
));
}
}
#[cfg(all(test, feature = "interface"))]
mod interface_tests {
use super::*;
use crate::core::{InterfaceBuilder, ModuleMeta};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
trait Logger: 'static {
fn log(&self, msg: &str) -> String;
}
struct ConsoleLogger;
impl Logger for ConsoleLogger {
fn log(&self, msg: &str) -> String {
format!("[console] {msg}")
}
}
struct FileLogger;
impl Logger for FileLogger {
fn log(&self, msg: &str) -> String {
format!("[file] {msg}")
}
}
#[derive(Debug)]
struct InterfaceTestError;
impl std::fmt::Display for InterfaceTestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "interface test error")
}
}
impl std::error::Error for InterfaceTestError {}
struct ConsoleLoggerModule;
impl ModuleMeta for ConsoleLoggerModule {
const NAME: &'static str = "console-logger-iface";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl InterfaceBuilder for ConsoleLoggerModule {
type Interface = dyn Logger;
type Capability = Arc<ConsoleLogger>;
type Error = InterfaceTestError;
fn build(_kit: &Kit) -> Result<Arc<ConsoleLogger>, InterfaceTestError> {
Ok(Arc::new(ConsoleLogger))
}
fn into_interface(cap: Arc<ConsoleLogger>) -> Arc<dyn Logger> {
cap
}
}
struct FileLoggerModule;
impl ModuleMeta for FileLoggerModule {
const NAME: &'static str = "file-logger";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl InterfaceBuilder for FileLoggerModule {
type Interface = dyn Logger;
type Capability = Arc<FileLogger>;
type Error = InterfaceTestError;
fn build(_kit: &Kit) -> Result<Arc<FileLogger>, InterfaceTestError> {
Ok(Arc::new(FileLogger))
}
fn into_interface(cap: Arc<FileLogger>) -> Arc<dyn Logger> {
cap
}
}
#[test]
fn register_as_then_resolve_returns_arc_dyn_trait() {
let mut kit = Kit::new();
kit.register_as::<ConsoleLoggerModule>()
.expect("register_as succeeds");
let built = kit.build().expect("build succeeds");
let logger: Arc<dyn Logger> = built.resolve::<dyn Logger>().expect("resolve succeeds");
assert_eq!(logger.log("hello"), "[console] hello");
}
#[test]
fn register_as_twice_same_interface_returns_already_registered() {
let mut kit = Kit::new();
kit.register_as::<ConsoleLoggerModule>()
.expect("first register_as succeeds");
let err = kit.register_as::<FileLoggerModule>().unwrap_err();
assert!(
matches!(err, TraitKitError::AlreadyRegistered { .. }),
"expected AlreadyRegistered, got {err:?}"
);
}
#[test]
fn resolve_before_build_returns_missing_capability() {
let mut kit = Kit::new();
kit.register_as::<ConsoleLoggerModule>()
.expect("register_as succeeds");
assert!(kit.resolve::<dyn Logger>().is_err());
}
#[test]
fn resolve_unregistered_interface_returns_missing_capability() {
let kit = Kit::new();
let built = kit.build().expect("build succeeds");
assert!(built.resolve::<dyn Logger>().is_err());
}
#[test]
fn register_as_builds_during_build() {
let mut kit = Kit::new();
kit.register_as::<ConsoleLoggerModule>()
.expect("register_as succeeds");
let built = kit.build().expect("build succeeds");
let logger = built.resolve::<dyn Logger>().expect("resolve succeeds");
assert_eq!(logger.log("test"), "[console] test");
}
#[test]
fn resolve_returns_callable_trait_object() {
let mut kit = Kit::new();
kit.register_as::<ConsoleLoggerModule>()
.expect("register_as succeeds");
let built = kit.build().expect("build succeeds");
let logger: Arc<dyn Logger> = built.resolve().expect("resolve succeeds");
let result = logger.log("world");
assert_eq!(result, "[console] world");
}
#[test]
fn register_as_coexists_with_register() {
struct RegularModule;
impl ModuleMeta for RegularModule {
const NAME: &'static str = "regular";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl AutoBuilder for RegularModule {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<AtomicUsize>, TraitKitError> {
Ok(Arc::new(AtomicUsize::new(42)))
}
}
let mut kit = Kit::new();
kit.register::<RegularModule>().expect("register succeeds");
kit.register_as::<ConsoleLoggerModule>()
.expect("register_as succeeds");
let built = kit.build().expect("build succeeds");
let cap = built.require::<RegularModule>().expect("require succeeds");
assert_eq!(cap.load(Ordering::SeqCst), 42);
let logger = built.resolve::<dyn Logger>().expect("resolve succeeds");
assert_eq!(logger.log("coexist"), "[console] coexist");
}
#[test]
fn register_as_same_module_twice_returns_already_registered() {
let mut kit = Kit::new();
kit.register_as::<ConsoleLoggerModule>()
.expect("first register_as succeeds");
let err = kit.register_as::<ConsoleLoggerModule>().unwrap_err();
assert!(
matches!(err, TraitKitError::AlreadyRegistered { .. }),
"expected AlreadyRegistered, got {err:?}"
);
}
#[test]
fn file_logger_interface_build_and_resolve() {
let mut kit = Kit::new();
kit.register_as::<FileLoggerModule>()
.expect("register_as succeeds");
let built = kit.build().expect("build succeeds");
let logger: Arc<dyn Logger> = built.resolve::<dyn Logger>().expect("resolve succeeds");
assert_eq!(logger.log("hello"), "[file] hello");
}
#[test]
fn interface_test_error_display() {
let e = InterfaceTestError;
assert_eq!(format!("{e}"), "interface test error");
}
#[test]
fn interface_build_error_returns_build_failed() {
struct FailIfaceModule;
impl ModuleMeta for FailIfaceModule {
const NAME: &'static str = "fail-iface";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl InterfaceBuilder for FailIfaceModule {
type Interface = dyn Logger;
type Capability = Arc<()>;
type Error = InterfaceTestError;
fn build(_kit: &Kit) -> Result<Arc<()>, InterfaceTestError> {
Err(InterfaceTestError)
}
fn into_interface(_cap: Arc<()>) -> Arc<dyn Logger> {
unreachable!()
}
}
let mut kit = Kit::new();
kit.register_as::<FailIfaceModule>().unwrap();
let result = kit.build();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TraitKitError::BuildFailed { .. }
));
}
}
#[cfg(all(test, feature = "lifecycle"))]
mod lifecycle_tests {
use super::*;
use crate::core::ModuleMeta;
use crate::core::lifecycle::Lifecycle;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
static LC_SHUTDOWN: AtomicUsize = AtomicUsize::new(0);
static LC_READY: AtomicUsize = AtomicUsize::new(0);
struct LcModule;
impl ModuleMeta for LcModule {
const NAME: &'static str = "lc-module";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for LcModule {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<AtomicUsize>, TraitKitError> {
Ok(Arc::new(AtomicUsize::new(0)))
}
}
impl Lifecycle for LcModule {
fn on_ready(_kit: &Kit<Ready>) -> Result<(), Self::Error> {
LC_READY.fetch_add(1, Ordering::SeqCst);
Ok(())
}
fn on_shutdown(_cap: &Arc<AtomicUsize>) {
LC_SHUTDOWN.fetch_add(1, Ordering::SeqCst);
}
}
#[test]
fn lifecycle_on_ready_called_during_build() {
LC_READY.store(0, Ordering::SeqCst);
let mut kit = Kit::new();
kit.register::<LcModule>().unwrap();
kit.register_lifecycle::<LcModule>();
let _built = kit.build().unwrap();
assert_eq!(
LC_READY.load(Ordering::SeqCst),
1,
"on_ready should be called once"
);
}
#[test]
fn lifecycle_shutdown_called_in_reverse_order() {
LC_SHUTDOWN.store(0, Ordering::SeqCst);
let mut kit = Kit::new();
kit.register::<LcModule>().unwrap();
kit.register_lifecycle::<LcModule>();
let built = kit.build().unwrap();
built.shutdown();
assert_eq!(
LC_SHUTDOWN.load(Ordering::SeqCst),
1,
"on_shutdown should be called once"
);
}
#[test]
fn lifecycle_on_ready_failure_propagates() {
struct FailReadyModule;
impl ModuleMeta for FailReadyModule {
const NAME: &'static str = "fail-ready";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for FailReadyModule {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Ok(Arc::new(()))
}
}
impl Lifecycle for FailReadyModule {
fn on_ready(_kit: &Kit<Ready>) -> Result<(), TraitKitError> {
Err(TraitKitError::BuildFailed {
context: "on_ready".into(),
source: Box::new(std::io::Error::other("intentional failure")),
})
}
}
let mut kit = Kit::new();
kit.register::<FailReadyModule>().unwrap();
kit.register_lifecycle::<FailReadyModule>();
let result = kit.build();
assert!(result.is_err(), "build should fail when on_ready fails");
let err = result.unwrap_err();
assert!(matches!(err, TraitKitError::LifecycleFailed { .. }));
}
}
#[cfg(all(test, feature = "health"))]
mod health_tests {
use super::*;
use crate::core::ModuleMeta;
use crate::core::health::{HealthCheck, HealthStatus};
use std::sync::Arc;
#[derive(Debug, Clone)]
struct HcCap {
val: i32,
}
struct HcModule;
impl ModuleMeta for HcModule {
const NAME: &'static str = "hc-module";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for HcModule {
type Capability = Arc<HcCap>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<HcCap>, TraitKitError> {
Ok(Arc::new(HcCap { val: 42 }))
}
}
impl HealthCheck for HcModule {
fn check(cap: &Arc<HcCap>) -> HealthStatus {
if cap.val > 0 {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy {
detail: "zero".into(),
}
}
}
}
#[test]
fn health_check_registered_and_queryable() {
let mut kit = Kit::new();
kit.register::<HcModule>().unwrap();
kit.register_health_check::<HcModule>();
let built = kit.build().unwrap();
let status = built.health_check::<HcModule>().unwrap();
assert_eq!(status, HealthStatus::Healthy);
}
#[test]
fn health_report_returns_all_checkers() {
let mut kit = Kit::new();
kit.register::<HcModule>().unwrap();
kit.register_health_check::<HcModule>();
let built = kit.build().unwrap();
let report = built.health_report();
assert_eq!(report.len(), 1);
assert_eq!(report[0].0, "hc-module");
assert_eq!(report[0].1, HealthStatus::Healthy);
}
#[test]
fn health_check_unregistered_returns_error() {
let mut kit = Kit::new();
kit.register::<HcModule>().unwrap();
let built = kit.build().unwrap();
let err = built.health_check::<HcModule>().unwrap_err();
assert!(matches!(err, TraitKitError::MissingConfig { .. }));
}
#[test]
fn health_check_unhealthy_for_zero_value() {
struct ZeroHcModule;
impl ModuleMeta for ZeroHcModule {
const NAME: &'static str = "zero-hc";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for ZeroHcModule {
type Capability = Arc<HcCap>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<HcCap>, TraitKitError> {
Ok(Arc::new(HcCap { val: 0 }))
}
}
impl HealthCheck for ZeroHcModule {
fn check(cap: &Arc<HcCap>) -> HealthStatus {
if cap.val > 0 {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy {
detail: "zero".into(),
}
}
}
}
let mut kit = Kit::new();
kit.register::<ZeroHcModule>().unwrap();
kit.register_health_check::<ZeroHcModule>();
let built = kit.build().unwrap();
let status = built.health_check::<ZeroHcModule>().unwrap();
assert!(matches!(status, HealthStatus::Unhealthy { .. }));
}
}
#[cfg(all(test, feature = "observer"))]
mod observability_tests {
use super::*;
use crate::core::ModuleMeta;
use crate::core::observer::BuildObserver;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
struct CountingObs {
start: Arc<AtomicUsize>,
built: Arc<AtomicUsize>,
}
impl BuildObserver for CountingObs {
fn on_module_start(&self, _: &'static str) {
self.start.fetch_add(1, Ordering::SeqCst);
}
fn on_module_built(&self, _: &'static str, _: Duration) {
self.built.fetch_add(1, Ordering::SeqCst);
}
}
struct ObsModule;
impl ModuleMeta for ObsModule {
const NAME: &'static str = "obs-module";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for ObsModule {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Ok(Arc::new(()))
}
}
#[test]
fn observer_callbacks_fired_during_build() {
let start = Arc::new(AtomicUsize::new(0));
let built = Arc::new(AtomicUsize::new(0));
let obs = Arc::new(CountingObs {
start: Arc::clone(&start),
built: Arc::clone(&built),
});
let mut kit = Kit::new();
kit.with_observer(obs);
kit.register::<ObsModule>().unwrap();
kit.build().unwrap();
assert_eq!(
start.load(Ordering::SeqCst),
1,
"on_module_start should fire"
);
assert_eq!(
built.load(Ordering::SeqCst),
1,
"on_module_built should fire"
);
}
#[test]
fn observer_on_build_error_called_on_failure() {
struct FailObs {
errors: Arc<AtomicUsize>,
}
impl BuildObserver for FailObs {
fn on_build_error(&self, _: &'static str, _: &TraitKitError) {
self.errors.fetch_add(1, Ordering::SeqCst);
}
}
struct FailBuildModule;
impl ModuleMeta for FailBuildModule {
const NAME: &'static str = "fail-build";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for FailBuildModule {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Err(TraitKitError::BuildFailed {
context: "intentional".into(),
source: Box::new(std::io::Error::other("test failure")),
})
}
}
let errors = Arc::new(AtomicUsize::new(0));
let obs = Arc::new(FailObs {
errors: Arc::clone(&errors),
});
let mut kit = Kit::new();
kit.with_observer(obs);
kit.register::<FailBuildModule>().unwrap();
let result = kit.build();
assert!(result.is_err(), "build should fail");
assert_eq!(
errors.load(Ordering::SeqCst),
1,
"on_build_error should fire once"
);
}
}
#[cfg(test)]
mod factory_tests {
use super::*;
use crate::core::ModuleMeta;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
static FACTORY_COUNT: AtomicUsize = AtomicUsize::new(0);
struct FactoryModule;
impl ModuleMeta for FactoryModule {
const NAME: &'static str = "factory-module";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for FactoryModule {
type Capability = Arc<AtomicUsize>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<AtomicUsize>, TraitKitError> {
let n = FACTORY_COUNT.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(AtomicUsize::new(n)))
}
}
#[test]
fn factory_creates_new_instance_each_call() {
FACTORY_COUNT.store(0, Ordering::SeqCst);
let mut kit = Kit::new();
kit.register::<FactoryModule>().unwrap();
let built = kit.build().unwrap();
let factory = built.factory::<FactoryModule>();
let cap1 = factory().unwrap();
let cap2 = factory().unwrap();
assert_ne!(
cap1.load(Ordering::SeqCst),
cap2.load(Ordering::SeqCst),
"factory should produce different instances"
);
}
}
#[cfg(all(test, feature = "scope"))]
mod scope_tests {
use super::*;
use crate::core::ModuleMeta;
use std::sync::Arc;
struct ScopeMockModule;
impl ModuleMeta for ScopeMockModule {
const NAME: &'static str = "scope-mock";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for ScopeMockModule {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Ok(Arc::new(()))
}
}
#[test]
fn create_scope_returns_empty_scope() {
let mut kit = Kit::new();
kit.register::<ScopeMockModule>().unwrap();
let built = kit.build().unwrap();
let scope = built.create_scope();
assert!(!scope.contains::<ScopeMockModule>());
}
}
#[cfg(test)]
mod conditional_tests {
use super::*;
use crate::core::ModuleMeta;
use std::sync::Arc;
struct CondMockModule;
impl ModuleMeta for CondMockModule {
const NAME: &'static str = "cond-mock";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for CondMockModule {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Ok(Arc::new(()))
}
}
#[test]
fn register_if_true_registers_module() {
let mut kit = Kit::new();
let registered = kit.register_if::<CondMockModule>(|_| true).unwrap();
assert!(registered);
let built = kit.build().unwrap();
assert!(built.contains::<CondMockModule>());
}
#[test]
fn register_if_false_skips_module() {
let mut kit = Kit::new();
let registered = kit.register_if::<CondMockModule>(|_| false).unwrap();
assert!(!registered);
let built = kit.build().unwrap();
assert!(!built.contains::<CondMockModule>());
}
}
#[cfg(all(test, feature = "decorator"))]
mod decorator_tests {
use super::*;
use crate::core::ModuleMeta;
use std::sync::Arc;
#[derive(Debug, Clone)]
struct DecCap {
val: String,
}
struct DecModule;
impl ModuleMeta for DecModule {
const NAME: &'static str = "dec-module";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for DecModule {
type Capability = Arc<DecCap>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<DecCap>, TraitKitError> {
Ok(Arc::new(DecCap {
val: "original".into(),
}))
}
}
#[test]
fn decorate_registers_decorator() {
let mut kit = Kit::new();
kit.register_lazy::<DecModule>().unwrap();
kit.decorate::<DecModule>(|cap| {
Arc::new(DecCap {
val: format!("{}+decorated", cap.val),
})
});
let built = kit.build().unwrap();
let cap = built.require::<DecModule>().unwrap();
assert_eq!(cap.val, "original+decorated");
}
}
#[cfg(all(test, feature = "encryption"))]
mod encryption_tests {
use super::*;
use crate::kit::ModuleConfig;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct SecretConfig {
api_key: String,
}
impl ModuleConfig for SecretConfig {
const PATH: &'static str = "test.secret";
fn default_value() -> Self {
Self {
api_key: "default".into(),
}
}
}
#[test]
fn set_and_get_encrypted_roundtrip() {
let kit = Kit::new();
let master_key = [0x42u8; 32];
let config = SecretConfig {
api_key: "super-secret".into(),
};
kit.set_encrypted(&config, &master_key).unwrap();
assert!(kit.contains_encrypted::<SecretConfig>());
let built = kit.build().unwrap();
let decrypted: SecretConfig = built.get_encrypted(&master_key).unwrap();
assert_eq!(decrypted, config);
}
#[test]
fn contains_encrypted_false_for_missing() {
let kit = Kit::new();
assert!(!kit.contains_encrypted::<SecretConfig>());
}
#[test]
fn get_encrypted_missing_returns_error() {
let kit = Kit::new();
let built = kit.build().unwrap();
let master_key = [0x42u8; 32];
let err = built
.get_encrypted::<SecretConfig>(&master_key)
.unwrap_err();
assert!(matches!(err, TraitKitError::MissingConfig { .. }));
}
#[test]
fn secret_config_default_value() {
let default = SecretConfig::default_value();
assert_eq!(default.api_key, "default");
}
#[test]
fn get_encrypted_wrong_key_returns_error() {
let kit = Kit::new();
let master_key = [0x42u8; 32];
let config = SecretConfig {
api_key: "secret".into(),
};
kit.set_encrypted(&config, &master_key).unwrap();
let built = kit.build().unwrap();
let wrong_key = [0xFFu8; 32];
let err = built.get_encrypted::<SecretConfig>(&wrong_key).unwrap_err();
assert!(matches!(err, TraitKitError::BuildFailed { .. }));
}
}
#[cfg(test)]
mod ready_tests {
use super::*;
use crate::core::ModuleMeta;
use std::sync::Arc;
struct ReadyMockModule;
impl ModuleMeta for ReadyMockModule {
const NAME: &'static str = "ready-mock";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for ReadyMockModule {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Ok(Arc::new(()))
}
}
#[test]
fn ready_optional_returns_none_for_unbuilt() {
let kit = Kit::new();
let built = kit.build().unwrap();
assert!(built.optional::<ReadyMockModule>().is_none());
}
#[test]
fn ready_optional_returns_some_for_built() {
let mut kit = Kit::new();
kit.register::<ReadyMockModule>().unwrap();
let built = kit.build().unwrap();
assert!(built.optional::<ReadyMockModule>().is_some());
}
#[test]
fn ready_contains_returns_true_for_built() {
let mut kit = Kit::new();
kit.register::<ReadyMockModule>().unwrap();
let built = kit.build().unwrap();
assert!(built.contains::<ReadyMockModule>());
}
#[test]
fn ready_contains_returns_false_for_unbuilt() {
let kit = Kit::new();
let built = kit.build().unwrap();
assert!(!built.contains::<ReadyMockModule>());
}
#[test]
fn ready_contains_config_returns_true() {
let kit = Kit::new();
kit.set_config(42i32);
let built = kit.build().unwrap();
assert!(built.contains_config::<i32>());
}
#[test]
fn ready_contains_config_returns_false() {
let kit = Kit::new();
let built = kit.build().unwrap();
assert!(!built.contains_config::<u64>());
}
#[test]
fn debug_unbuilt_format() {
let kit = Kit::new();
let debug = format!("{kit:?}");
assert!(debug.contains("Kit<Unbuilt>"));
assert!(debug.contains("modules"));
}
#[test]
fn debug_ready_format() {
let mut kit = Kit::new();
kit.register::<ReadyMockModule>().unwrap();
let built = kit.build().unwrap();
let debug = format!("{built:?}");
assert!(debug.contains("Kit<Ready>"));
assert!(debug.contains("modules"));
}
#[test]
fn default_creates_empty_kit() {
let kit = Kit::default();
let built = kit.build().unwrap();
assert_eq!(built.graph.entries().len(), 0);
}
#[test]
fn graph_dot_returns_valid_string() {
let mut kit = Kit::new();
kit.register::<ReadyMockModule>().unwrap();
let built = kit.build().unwrap();
let dot = built.graph_dot();
assert!(dot.contains("digraph"));
}
#[test]
fn graph_mermaid_returns_valid_string() {
let mut kit = Kit::new();
kit.register::<ReadyMockModule>().unwrap();
let built = kit.build().unwrap();
let mermaid = built.graph_mermaid();
assert!(mermaid.contains("graph TD"));
}
#[test]
fn config_missing_returns_error() {
let kit = Kit::new();
let built = kit.build().unwrap();
let err = built.config::<i32>().unwrap_err();
assert!(matches!(err, TraitKitError::MissingConfig { .. }));
}
#[test]
fn require_ref_returns_missing_for_unbuilt() {
let kit = Kit::new();
let built = kit.build().unwrap();
let err = built.require_ref::<ReadyMockModule>().unwrap_err();
assert!(matches!(err, TraitKitError::MissingCapability { .. }));
}
#[test]
fn build_missing_dependency_returns_error() {
struct DepModule;
impl ModuleMeta for DepModule {
const NAME: &'static str = "dep";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for DepModule {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Ok(Arc::new(()))
}
}
struct NeedsDepModule;
impl ModuleMeta for NeedsDepModule {
const NAME: &'static str = "needs-dep";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("dep", TypeId::of::<DepModule>())];
DEPS
}
}
impl AutoBuilder for NeedsDepModule {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Ok(Arc::new(()))
}
}
let mut kit = Kit::new();
kit.register::<NeedsDepModule>().unwrap();
let result = kit.build();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TraitKitError::DependencyMissing { .. }
));
}
#[test]
fn build_cycle_detected_returns_error() {
struct CycleA;
impl ModuleMeta for CycleA {
const NAME: &'static str = "cycle-a";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("cycle-b", TypeId::of::<CycleB>())];
DEPS
}
}
impl AutoBuilder for CycleA {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Ok(Arc::new(()))
}
}
struct CycleB;
impl ModuleMeta for CycleB {
const NAME: &'static str = "cycle-b";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("cycle-a", TypeId::of::<CycleA>())];
DEPS
}
}
impl AutoBuilder for CycleB {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Ok(Arc::new(()))
}
}
let mut kit = Kit::new();
kit.register::<CycleA>().unwrap();
kit.register::<CycleB>().unwrap();
let result = kit.build();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TraitKitError::CycleDetected { .. }
));
}
#[test]
fn lazy_require_build_error() {
struct LazyFailModule;
impl ModuleMeta for LazyFailModule {
const NAME: &'static str = "lazy-fail";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for LazyFailModule {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<()>, TraitKitError> {
Err(TraitKitError::BuildFailed {
context: "lazy-fail".into(),
source: Box::new(std::io::Error::other("lazy fail")),
})
}
}
let mut kit = Kit::new();
kit.register_lazy::<LazyFailModule>().unwrap();
let built = kit.build().unwrap();
let err = built.require::<LazyFailModule>().unwrap_err();
assert!(matches!(err, TraitKitError::BuildFailed { .. }));
}
}
#[cfg(all(test, feature = "confers"))]
mod validation_tests {
use super::*;
use crate::kit::config::{Configurable, Validatable};
use std::error::Error;
#[derive(Clone, Debug, PartialEq)]
struct ValidConfig {
port: u16,
}
impl Configurable for ValidConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Ok(Self { port: 8080 })
}
}
impl Validatable for ValidConfig {
fn validate(&self) -> Result<(), Vec<String>> {
if self.port > 0 && self.port < 65535 {
Ok(())
} else {
Err(vec!["port out of range".to_string()])
}
}
}
#[derive(Clone, Debug, PartialEq)]
struct InvalidConfig {
port: u16,
}
impl Configurable for InvalidConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Ok(Self { port: 0 })
}
}
impl Validatable for InvalidConfig {
fn validate(&self) -> Result<(), Vec<String>> {
Err(vec![
"port must be > 0".to_string(),
"port must be < 65535".to_string(),
])
}
}
#[test]
fn load_and_validate_succeeds_with_valid_config() {
let kit = Kit::new();
kit.load_and_validate::<ValidConfig>()
.expect("valid config should pass");
let config: ValidConfig = kit.config().expect("config should be stored");
assert_eq!(config.port, 8080);
}
#[test]
fn load_and_validate_fails_with_invalid_config() {
let kit = Kit::new();
let err = kit
.load_and_validate::<InvalidConfig>()
.expect_err("invalid config should fail");
let msg = format!("{err}");
assert!(
msg.contains("port must be > 0"),
"error should contain first validation error: {msg}"
);
assert!(
msg.contains("port must be < 65535"),
"error should contain second validation error: {msg}"
);
}
#[test]
fn load_and_validate_does_not_store_on_failure() {
let kit = Kit::new();
let _ = kit.load_and_validate::<InvalidConfig>();
let result: Result<InvalidConfig, _> = kit.config();
assert!(result.is_err(), "invalid config should not be stored");
}
#[test]
fn load_and_validate_retry_after_failure() {
let kit = Kit::new();
let _ = kit.load_and_validate::<InvalidConfig>();
kit.load_and_validate::<ValidConfig>()
.expect("valid config should succeed after previous failure");
let config: ValidConfig = kit.config().expect("valid config should be stored");
assert_eq!(config.port, 8080);
}
}
#[cfg(all(test, feature = "confers"))]
mod snapshot_tests {
use super::*;
use crate::kit::config::Configurable;
use std::error::Error;
#[derive(Clone, Debug, PartialEq)]
struct SnapConfig {
value: String,
}
impl Configurable for SnapConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Ok(Self {
value: "loaded".to_string(),
})
}
}
#[test]
fn snapshot_returns_true_when_config_exists() {
let kit = Kit::new();
kit.set_config(SnapConfig {
value: "original".to_string(),
});
assert!(kit.snapshot_config::<SnapConfig>());
}
#[test]
fn snapshot_returns_false_when_config_missing() {
let kit = Kit::new();
assert!(!kit.snapshot_config::<SnapConfig>());
}
#[test]
fn restore_overwrites_current_config() {
let kit = Kit::new();
kit.set_config(SnapConfig {
value: "original".to_string(),
});
kit.snapshot_config::<SnapConfig>();
kit.set_config(SnapConfig {
value: "modified".to_string(),
});
let current: SnapConfig = kit.config().unwrap();
assert_eq!(current.value, "modified");
kit.restore_config::<SnapConfig>()
.expect("restore should succeed");
let restored: SnapConfig = kit.config().unwrap();
assert_eq!(restored.value, "original");
}
#[test]
fn restore_returns_error_when_no_snapshot() {
let kit = Kit::new();
let err = kit
.restore_config::<SnapConfig>()
.expect_err("restore without snapshot should fail");
assert!(matches!(err, TraitKitError::MissingConfig { .. }));
}
#[test]
fn has_snapshot_reflects_state() {
let kit = Kit::new();
assert!(!kit.has_snapshot::<SnapConfig>());
kit.set_config(SnapConfig {
value: "test".to_string(),
});
kit.snapshot_config::<SnapConfig>();
assert!(kit.has_snapshot::<SnapConfig>());
}
#[test]
fn snapshot_overwrite_replaces_previous() {
let kit = Kit::new();
kit.set_config(SnapConfig {
value: "v1".to_string(),
});
kit.snapshot_config::<SnapConfig>();
kit.set_config(SnapConfig {
value: "v2".to_string(),
});
kit.snapshot_config::<SnapConfig>();
kit.set_config(SnapConfig {
value: "current".to_string(),
});
kit.restore_config::<SnapConfig>().unwrap();
let restored: SnapConfig = kit.config().unwrap();
assert_eq!(restored.value, "v2");
}
}
#[cfg(all(test, feature = "toggle"))]
mod toggle_tests {
use super::*;
use crate::core::ModuleMeta;
use std::sync::Arc;
struct ToggleModule;
impl ModuleMeta for ToggleModule {
const NAME: &'static str = "toggle-mod";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AutoBuilder for ToggleModule {
type Capability = Arc<String>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Arc<String>, TraitKitError> {
Ok(Arc::new("toggle-cap".to_string()))
}
}
#[test]
fn enable_toggle_sets_value() {
let kit = Kit::new();
kit.enable_toggle("feature-a", true);
assert!(kit.is_toggle_enabled("feature-a"));
kit.enable_toggle("feature-a", false);
assert!(!kit.is_toggle_enabled("feature-a"));
}
#[test]
fn is_toggle_enabled_returns_false_for_unknown() {
let kit = Kit::new();
assert!(!kit.is_toggle_enabled("nonexistent"));
}
#[test]
fn register_if_toggle_registers_when_enabled() {
let mut kit = Kit::new();
kit.enable_toggle("mod-x", true);
let registered = kit
.register_if_toggle::<ToggleModule>("mod-x")
.expect("registration should succeed");
assert!(registered);
}
#[test]
fn register_if_toggle_skips_when_disabled() {
let mut kit = Kit::new();
kit.enable_toggle("mod-x", false);
let registered = kit
.register_if_toggle::<ToggleModule>("mod-x")
.expect("should return Ok(false)");
assert!(!registered);
}
#[test]
fn register_if_toggle_returns_error_on_duplicate() {
let mut kit = Kit::new();
kit.enable_toggle("mod-x", true);
kit.register_if_toggle::<ToggleModule>("mod-x")
.expect("first registration");
let err = kit
.register_if_toggle::<ToggleModule>("mod-x")
.expect_err("duplicate should fail");
assert!(matches!(err, TraitKitError::AlreadyRegistered { .. }));
}
#[test]
fn toggle_state_survives_build() {
let mut kit = Kit::new();
kit.enable_toggle("persist", true);
kit.register_if_toggle::<ToggleModule>("persist").unwrap();
let ready = kit.build().unwrap();
assert!(ready.is_toggle_enabled("persist"));
}
#[test]
fn toggle_enable_on_ready_state() {
let mut kit = Kit::new();
kit.register::<ToggleModule>().unwrap();
let ready = kit.build().unwrap();
ready.enable_toggle("runtime", true);
assert!(ready.is_toggle_enabled("runtime"));
}
}
#[cfg(all(test, feature = "confers"))]
mod interpolation_tests {
use crate::kit::config::interpolate_json_value;
use std::collections::HashMap;
#[test]
fn basic_var_replacement() {
let mut value = serde_json::json!("${HOST}");
let mut vars = HashMap::new();
vars.insert("HOST".to_string(), "localhost".to_string());
interpolate_json_value(&mut value, &vars);
assert_eq!(value, serde_json::json!("localhost"));
}
#[test]
fn default_value_when_var_missing() {
let mut value = serde_json::json!("${HOST:-127.0.0.1}");
let vars = HashMap::new();
interpolate_json_value(&mut value, &vars);
assert_eq!(value, serde_json::json!("127.0.0.1"));
}
#[test]
fn default_value_ignored_when_var_present() {
let mut value = serde_json::json!("${HOST:-127.0.0.1}");
let mut vars = HashMap::new();
vars.insert("HOST".to_string(), "10.0.0.1".to_string());
interpolate_json_value(&mut value, &vars);
assert_eq!(value, serde_json::json!("10.0.0.1"));
}
#[test]
fn no_match_preserved() {
let mut value = serde_json::json!("${UNKNOWN}");
let vars = HashMap::new();
interpolate_json_value(&mut value, &vars);
assert_eq!(value, serde_json::json!("${UNKNOWN}"));
}
#[test]
fn nested_object_replacement() {
let mut value = serde_json::json!({
"db": {
"host": "${DB_HOST}",
"port": 5432
}
});
let mut vars = HashMap::new();
vars.insert("DB_HOST".to_string(), "db.example.com".to_string());
interpolate_json_value(&mut value, &vars);
assert_eq!(value["db"]["host"], serde_json::json!("db.example.com"));
assert_eq!(value["db"]["port"], serde_json::json!(5432));
}
#[test]
fn array_string_elements_replaced() {
let mut value = serde_json::json!(["${A}", "${B}", 42]);
let mut vars = HashMap::new();
vars.insert("A".to_string(), "alpha".to_string());
vars.insert("B".to_string(), "beta".to_string());
interpolate_json_value(&mut value, &vars);
assert_eq!(value[0], serde_json::json!("alpha"));
assert_eq!(value[1], serde_json::json!("beta"));
assert_eq!(value[2], serde_json::json!(42));
}
#[test]
fn non_string_values_untouched() {
let mut value = serde_json::json!({
"num": 42,
"bool": true,
"null": null
});
let vars = HashMap::new();
interpolate_json_value(&mut value, &vars);
assert_eq!(value["num"], serde_json::json!(42));
assert_eq!(value["bool"], serde_json::json!(true));
assert_eq!(value["null"], serde_json::json!(null));
}
#[test]
fn object_keys_not_replaced() {
let mut value = serde_json::json!({"${KEY}": "value"});
let vars = HashMap::new();
interpolate_json_value(&mut value, &vars);
assert!(value.as_object().unwrap().contains_key("${KEY}"));
}
#[test]
fn multiple_vars_in_one_string() {
let mut value = serde_json::json!("${HOST}:${PORT}");
let mut vars = HashMap::new();
vars.insert("HOST".to_string(), "localhost".to_string());
vars.insert("PORT".to_string(), "8080".to_string());
interpolate_json_value(&mut value, &vars);
assert_eq!(value, serde_json::json!("localhost:8080"));
}
}