use std::any::Any;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigValue {
Null,
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<ConfigValue>),
Object(BTreeMap<String, ConfigValue>),
}
impl From<String> for ConfigValue {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<&str> for ConfigValue {
fn from(value: &str) -> Self {
Self::String(value.to_string())
}
}
impl From<bool> for ConfigValue {
fn from(value: bool) -> Self {
Self::Boolean(value)
}
}
impl From<i64> for ConfigValue {
fn from(value: i64) -> Self {
Self::Integer(value)
}
}
impl From<i32> for ConfigValue {
fn from(value: i32) -> Self {
Self::Integer(value as i64)
}
}
impl From<usize> for ConfigValue {
fn from(value: usize) -> Self {
Self::Integer(value as i64)
}
}
impl From<f64> for ConfigValue {
fn from(value: f64) -> Self {
Self::Float(value)
}
}
impl From<f32> for ConfigValue {
fn from(value: f32) -> Self {
Self::Float(value as f64)
}
}
impl<T> From<Vec<T>> for ConfigValue
where
T: Into<ConfigValue>,
{
fn from(values: Vec<T>) -> Self {
Self::Array(values.into_iter().map(Into::into).collect())
}
}
impl<T, const N: usize> From<[T; N]> for ConfigValue
where
T: Into<ConfigValue>,
{
fn from(values: [T; N]) -> Self {
Self::Array(values.into_iter().map(Into::into).collect())
}
}
pub trait FromConfigValue: Sized {
fn from_config_value(value: ConfigValue) -> Option<Self>;
}
impl FromConfigValue for ConfigValue {
fn from_config_value(value: ConfigValue) -> Option<Self> {
Some(value)
}
}
impl FromConfigValue for String {
fn from_config_value(value: ConfigValue) -> Option<Self> {
match value {
ConfigValue::String(value) => Some(value),
ConfigValue::Integer(value) => Some(value.to_string()),
ConfigValue::Float(value) => Some(value.to_string()),
ConfigValue::Boolean(value) => Some(value.to_string()),
_ => None,
}
}
}
impl FromConfigValue for i64 {
fn from_config_value(value: ConfigValue) -> Option<Self> {
match value {
ConfigValue::Integer(value) => Some(value),
ConfigValue::String(value) => value.parse().ok(),
_ => None,
}
}
}
impl FromConfigValue for f64 {
fn from_config_value(value: ConfigValue) -> Option<Self> {
match value {
ConfigValue::Float(value) => Some(value),
ConfigValue::Integer(value) => Some(value as f64),
ConfigValue::String(value) => value.parse().ok(),
_ => None,
}
}
}
impl FromConfigValue for bool {
fn from_config_value(value: ConfigValue) -> Option<Self> {
match value {
ConfigValue::Boolean(value) => Some(value),
ConfigValue::String(value) => match value.to_ascii_lowercase().as_str() {
"true" => Some(true),
"false" => Some(false),
_ => None,
},
_ => None,
}
}
}
fn parse_env_value(value: String) -> ConfigValue {
let lower = value.to_ascii_lowercase();
if lower == "true" {
return ConfigValue::Boolean(true);
}
if lower == "false" {
return ConfigValue::Boolean(false);
}
if let Ok(int) = value.parse::<i64>() {
return ConfigValue::Integer(int);
}
if let Ok(float) = value.parse::<f64>() {
return ConfigValue::Float(float);
}
ConfigValue::String(value)
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FoundationError {
#[error("service registration failed: {0}")]
ServiceRegistration(String),
#[error("service boot failed: {0}")]
ServiceBoot(String),
#[error("service resolution failed: {0}")]
ServiceResolve(String),
#[error("config access failed: {0}")]
Config(String),
}
type DynInstance = Arc<dyn Any + Send + Sync>;
type SingletonFactory = Box<dyn FnOnce() -> DynInstance + Send>;
type TransientFactory = Arc<dyn Fn() -> DynInstance + Send + Sync>;
pub trait Container: Any + Send + Sync {
fn register_provider(&self, provider: Box<dyn ServiceProvider>) -> Result<(), FoundationError>;
fn boot_providers(&self) -> Result<(), FoundationError>;
fn singleton_any(
&self,
type_name: &'static str,
value: DynInstance,
) -> Result<(), FoundationError>;
fn singleton_factory_any(
&self,
type_name: &'static str,
factory: SingletonFactory,
) -> Result<(), FoundationError>;
fn bind(
&self,
abstract_name: &'static str,
concrete_name: &'static str,
) -> Result<(), FoundationError>;
fn factory_any(
&self,
type_name: &'static str,
factory: TransientFactory,
) -> Result<(), FoundationError>;
fn resolve_any(
&self,
type_name: &'static str,
) -> Result<Arc<dyn Any + Send + Sync>, FoundationError>;
}
pub trait ContainerRegistrationExt {
fn singleton<T: Any + Send + Sync + 'static>(&self, value: T) -> Result<(), FoundationError>;
fn singleton_factory<T, F>(&self, factory: F) -> Result<(), FoundationError>
where
T: Any + Send + Sync + 'static,
F: FnOnce() -> T + Send + 'static;
fn bind_types<TAbstract: ?Sized + 'static, TConcrete: Any + Send + Sync + 'static>(
&self,
) -> Result<(), FoundationError>;
fn bind_names(
&self,
abstract_name: &'static str,
concrete_name: &'static str,
) -> Result<(), FoundationError>;
fn factory<T, F>(&self, factory: F) -> Result<(), FoundationError>
where
T: Any + Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static;
}
impl<TContainer> ContainerRegistrationExt for TContainer
where
TContainer: Container + ?Sized,
{
fn singleton<T: Any + Send + Sync + 'static>(&self, value: T) -> Result<(), FoundationError> {
self.singleton_any(core::any::type_name::<T>(), Arc::new(value))
}
fn singleton_factory<T, F>(&self, factory: F) -> Result<(), FoundationError>
where
T: Any + Send + Sync + 'static,
F: FnOnce() -> T + Send + 'static,
{
self.singleton_factory_any(
core::any::type_name::<T>(),
Box::new(move || Arc::new(factory()) as DynInstance),
)
}
fn bind_types<TAbstract: ?Sized + 'static, TConcrete: Any + Send + Sync + 'static>(
&self,
) -> Result<(), FoundationError> {
self.bind(
core::any::type_name::<TAbstract>(),
core::any::type_name::<TConcrete>(),
)
}
fn bind_names(
&self,
abstract_name: &'static str,
concrete_name: &'static str,
) -> Result<(), FoundationError> {
self.bind(abstract_name, concrete_name)
}
fn factory<T, F>(&self, factory: F) -> Result<(), FoundationError>
where
T: Any + Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
self.factory_any(
core::any::type_name::<T>(),
Arc::new(move || Arc::new(factory()) as DynInstance),
)
}
}
pub trait ContainerResolveExt {
fn resolve<T: Any + Send + Sync + 'static>(&self) -> Result<Arc<T>, FoundationError>;
}
impl<TContainer> ContainerResolveExt for TContainer
where
TContainer: Container + ?Sized,
{
fn resolve<T: Any + Send + Sync + 'static>(&self) -> Result<Arc<T>, FoundationError> {
self.resolve_any(core::any::type_name::<T>())?
.downcast::<T>()
.map_err(|_| {
FoundationError::ServiceResolve(format!(
"failed downcast for {}",
core::any::type_name::<T>()
))
})
}
}
pub trait ServiceProvider: Send + Sync {
fn register(&self, container: &dyn Container) -> Result<(), FoundationError>;
fn boot(&self, container: &dyn Container) -> Result<(), FoundationError>;
fn name(&self) -> &'static str {
let short = core::any::type_name::<Self>()
.rsplit("::")
.next()
.unwrap_or(core::any::type_name::<Self>());
if let Some(trimmed) = short.strip_suffix("Provider") {
if !trimmed.is_empty() {
return trimmed;
}
}
short
}
fn factory() -> Box<dyn ServiceProvider>
where
Self: Default + Sized + 'static,
{
Box::new(Self::default())
}
}
pub trait Config: Send + Sync {
fn get(&self, key: &str) -> Option<ConfigValue>;
fn set(&mut self, key: &str, value: ConfigValue) -> Result<(), FoundationError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct EnvReader;
impl EnvReader {
pub fn get(&self, key: &str) -> Option<String> {
std::env::var(key).ok()
}
pub fn env(&self, key: &str, default: impl Into<ConfigValue>) -> ConfigValue {
match self.get(key) {
Some(value) => parse_env_value(value),
None => default.into(),
}
}
}
#[derive(Debug, Default)]
pub struct ConfigRepository {
values: BTreeMap<String, ConfigValue>,
}
impl ConfigRepository {
pub fn new() -> Self {
Self::default()
}
pub fn with_values(values: BTreeMap<String, ConfigValue>) -> Self {
Self { values }
}
pub fn env(&self, key: &str, default: impl Into<ConfigValue>) -> ConfigValue {
EnvReader.env(key, default)
}
fn parse_segments<'a>(&self, key: &'a str) -> Result<Vec<&'a str>, FoundationError> {
let segments: Vec<&str> = key.split('.').collect();
if segments.is_empty() || segments.iter().any(|segment| segment.is_empty()) {
return Err(FoundationError::Config(format!(
"invalid config key '{key}'"
)));
}
Ok(segments)
}
}
impl Config for ConfigRepository {
fn get(&self, key: &str) -> Option<ConfigValue> {
let segments = self.parse_segments(key).ok()?;
let mut current = self.values.get(*segments.first()?)?;
for segment in segments.iter().skip(1) {
match current {
ConfigValue::Object(map) => {
current = map.get(*segment)?;
}
_ => return None,
}
}
Some(current.clone())
}
fn set(&mut self, key: &str, value: ConfigValue) -> Result<(), FoundationError> {
let segments = self.parse_segments(key)?;
let mut current = &mut self.values;
for segment in segments.iter().take(segments.len() - 1) {
let key = (*segment).to_string();
let next = current
.entry(key)
.or_insert_with(|| ConfigValue::Object(BTreeMap::new()));
match next {
ConfigValue::Object(map) => current = map,
_ => {
return Err(FoundationError::Config(
"cannot traverse through non-object config value".to_string(),
))
}
}
}
current.insert(
segments
.last()
.expect("parse_segments must return at least one segment")
.to_string(),
value,
);
Ok(())
}
}
#[derive(Clone, Default)]
pub struct SharedConfigRepository {
inner: Arc<Mutex<ConfigRepository>>,
}
impl SharedConfigRepository {
pub fn new(repository: ConfigRepository) -> Self {
Self {
inner: Arc::new(Mutex::new(repository)),
}
}
pub fn env(&self, key: &str, default: impl Into<ConfigValue>) -> ConfigValue {
self.inner
.lock()
.expect("config repository lock poisoned")
.env(key, default)
}
}
impl Config for SharedConfigRepository {
fn get(&self, key: &str) -> Option<ConfigValue> {
self.inner
.lock()
.expect("config repository lock poisoned")
.get(key)
}
fn set(&mut self, key: &str, value: ConfigValue) -> Result<(), FoundationError> {
self.inner
.lock()
.expect("config repository lock poisoned")
.set(key, value)
}
}
#[derive(Default)]
pub struct InMemoryConfig {
repository: ConfigRepository,
}
impl Config for InMemoryConfig {
fn get(&self, key: &str) -> Option<ConfigValue> {
self.repository.get(key)
}
fn set(&mut self, key: &str, value: ConfigValue) -> Result<(), FoundationError> {
self.repository.set(key, value)
}
}
#[derive(Default)]
pub struct InMemoryContainer {
inner: Mutex<InMemoryContainerInner>,
}
#[derive(Default)]
struct InMemoryContainerInner {
singletons: HashMap<&'static str, DynInstance>,
singleton_factories: HashMap<&'static str, SingletonFactory>,
factories: HashMap<&'static str, TransientFactory>,
bindings: HashMap<&'static str, &'static str>,
}
impl InMemoryContainer {
pub fn new() -> Self {
Self::default()
}
}
impl Container for InMemoryContainer {
fn register_provider(
&self,
_provider: Box<dyn ServiceProvider>,
) -> Result<(), FoundationError> {
Ok(())
}
fn boot_providers(&self) -> Result<(), FoundationError> {
Ok(())
}
fn singleton_any(
&self,
type_name: &'static str,
value: DynInstance,
) -> Result<(), FoundationError> {
self.inner
.lock()
.map_err(|_| {
FoundationError::ServiceRegistration("services lock poisoned".to_string())
})?
.singletons
.insert(type_name, value);
Ok(())
}
fn singleton_factory_any(
&self,
type_name: &'static str,
factory: SingletonFactory,
) -> Result<(), FoundationError> {
self.inner
.lock()
.map_err(|_| {
FoundationError::ServiceRegistration("services lock poisoned".to_string())
})?
.singleton_factories
.insert(type_name, factory);
Ok(())
}
fn bind(
&self,
abstract_name: &'static str,
concrete_name: &'static str,
) -> Result<(), FoundationError> {
self.inner
.lock()
.map_err(|_| {
FoundationError::ServiceRegistration("services lock poisoned".to_string())
})?
.bindings
.insert(abstract_name, concrete_name);
Ok(())
}
fn factory_any(
&self,
type_name: &'static str,
factory: TransientFactory,
) -> Result<(), FoundationError> {
self.inner
.lock()
.map_err(|_| {
FoundationError::ServiceRegistration("services lock poisoned".to_string())
})?
.factories
.insert(type_name, factory);
Ok(())
}
fn resolve_any(
&self,
type_name: &'static str,
) -> Result<Arc<dyn Any + Send + Sync>, FoundationError> {
let resolved_name = {
let inner = self.inner.lock().map_err(|_| {
FoundationError::ServiceResolve("services lock poisoned".to_string())
})?;
*inner.bindings.get(type_name).unwrap_or(&type_name)
};
if let Some(instance) = self
.inner
.lock()
.map_err(|_| FoundationError::ServiceResolve("services lock poisoned".to_string()))?
.singletons
.get(resolved_name)
.cloned()
{
return Ok(instance);
}
let singleton_factory = self
.inner
.lock()
.map_err(|_| FoundationError::ServiceResolve("services lock poisoned".to_string()))?
.singleton_factories
.remove(resolved_name);
if let Some(factory) = singleton_factory {
let instance = factory();
self.inner
.lock()
.map_err(|_| FoundationError::ServiceResolve("services lock poisoned".to_string()))?
.singletons
.insert(resolved_name, Arc::clone(&instance));
return Ok(instance);
}
if let Some(factory) = self
.inner
.lock()
.map_err(|_| FoundationError::ServiceResolve("services lock poisoned".to_string()))?
.factories
.get(resolved_name)
.cloned()
{
return Ok(factory());
}
Err(FoundationError::ServiceResolve(type_name.to_string()))
}
}
pub fn defaults() -> (Arc<dyn Container>, Box<dyn Config>) {
(
Arc::new(InMemoryContainer::new()) as Arc<dyn Container>,
Box::new(InMemoryConfig::default()) as Box<dyn Config>,
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
static ENV_TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
#[derive(Default)]
struct FakeContainer {
services: Mutex<HashMap<&'static str, Arc<dyn Any + Send + Sync>>>,
}
impl FakeContainer {
fn insert<T: Any + Send + Sync + 'static>(&self, value: T) {
self.services
.lock()
.expect("services lock poisoned")
.insert(core::any::type_name::<T>(), Arc::new(value));
}
}
impl Container for FakeContainer {
fn register_provider(
&self,
_provider: Box<dyn ServiceProvider>,
) -> Result<(), FoundationError> {
Ok(())
}
fn boot_providers(&self) -> Result<(), FoundationError> {
Ok(())
}
fn singleton_any(
&self,
type_name: &'static str,
value: DynInstance,
) -> Result<(), FoundationError> {
self.services
.lock()
.expect("services lock poisoned")
.insert(type_name, value);
Ok(())
}
fn singleton_factory_any(
&self,
_type_name: &'static str,
_factory: SingletonFactory,
) -> Result<(), FoundationError> {
Err(FoundationError::ServiceRegistration(
"fake container does not support singleton_factory".to_string(),
))
}
fn bind(
&self,
_abstract_name: &'static str,
_concrete_name: &'static str,
) -> Result<(), FoundationError> {
Err(FoundationError::ServiceRegistration(
"fake container does not support bindings".to_string(),
))
}
fn factory_any(
&self,
_type_name: &'static str,
_factory: TransientFactory,
) -> Result<(), FoundationError> {
Err(FoundationError::ServiceRegistration(
"fake container does not support factories".to_string(),
))
}
fn resolve_any(
&self,
type_name: &'static str,
) -> Result<Arc<dyn Any + Send + Sync>, FoundationError> {
self.services
.lock()
.expect("services lock poisoned")
.get(type_name)
.cloned()
.ok_or_else(|| FoundationError::ServiceResolve(type_name.to_string()))
}
}
#[test]
fn typed_resolve_returns_registered_service() {
let container = FakeContainer::default();
container.insert::<String>("hello".to_string());
let resolved = container
.resolve::<String>()
.expect("service should resolve");
assert_eq!(resolved.as_str(), "hello");
}
#[test]
fn typed_resolve_returns_not_found_error() {
let container = FakeContainer::default();
let err = container
.resolve::<String>()
.expect_err("missing service should fail");
assert_eq!(
err,
FoundationError::ServiceResolve(core::any::type_name::<String>().to_string())
);
}
#[test]
fn in_memory_container_singleton_round_trip() {
let container = InMemoryContainer::new();
container
.singleton::<String>("hello".to_string())
.expect("singleton insert should succeed");
let resolved = container
.resolve::<String>()
.expect("singleton resolve should succeed");
assert_eq!(resolved.as_str(), "hello");
}
#[test]
fn defaults_return_usable_backends() {
let (_container, mut config) = defaults();
config
.set("app.name", ConfigValue::String("demo".to_string()))
.expect("config set should succeed");
assert_eq!(
config.get("app.name"),
Some(ConfigValue::String("demo".to_string()))
);
}
#[test]
fn config_repository_supports_dot_set_and_get_with_intermediate_nodes() {
let mut repository = ConfigRepository::new();
repository
.set("services.mailgun.timeout", ConfigValue::Integer(30))
.expect("nested set should succeed");
assert_eq!(
repository.get("services.mailgun.timeout"),
Some(ConfigValue::Integer(30))
);
}
#[test]
fn config_repository_returns_none_for_non_object_traversal_reads() {
let mut repository = ConfigRepository::new();
repository
.set("app", ConfigValue::String("demo".to_string()))
.expect("top-level set should succeed");
assert_eq!(repository.get("app.name"), None);
}
#[test]
fn config_repository_rejects_non_object_traversal_writes() {
let mut repository = ConfigRepository::new();
repository
.set("app", ConfigValue::String("demo".to_string()))
.expect("top-level set should succeed");
let err = repository
.set("app.name", ConfigValue::String("demo".to_string()))
.expect_err("set should fail when traversing through scalar");
assert_eq!(
err,
FoundationError::Config("cannot traverse through non-object config value".to_string())
);
}
#[test]
fn config_repository_overwrites_existing_values_at_target_key() {
let mut repository = ConfigRepository::new();
repository
.set("app.name", ConfigValue::String("demo".to_string()))
.expect("initial set should succeed");
repository
.set("app.name", ConfigValue::String("new-name".to_string()))
.expect("value overwrite should succeed");
repository
.set(
"app",
ConfigValue::Object(BTreeMap::from([(
"env".to_string(),
ConfigValue::String("local".to_string()),
)])),
)
.expect("object overwrite should succeed");
repository
.set("app", ConfigValue::String("scalar".to_string()))
.expect("scalar overwrite should succeed");
assert_eq!(
repository.get("app"),
Some(ConfigValue::String("scalar".to_string()))
);
}
#[test]
fn shared_config_repository_shares_state_across_clones() {
let mut shared = SharedConfigRepository::new(ConfigRepository::new());
shared
.set("app.name", ConfigValue::String("demo".to_string()))
.expect("set through first handle should succeed");
let reader = shared.clone();
assert_eq!(
reader.get("app.name"),
Some(ConfigValue::String("demo".to_string()))
);
}
#[test]
fn env_reader_returns_default_when_variable_is_missing() {
let key = format!(
"RIVET_TEST_MISSING_{}",
ENV_TEST_COUNTER.fetch_add(1, Ordering::SeqCst)
);
std::env::remove_var(&key);
let value = EnvReader.env(&key, "fallback");
assert_eq!(value, ConfigValue::String("fallback".to_string()));
}
#[test]
fn env_reader_best_effort_casts_values() {
let suffix = ENV_TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let bool_key = format!("RIVET_TEST_BOOL_{suffix}");
let int_key = format!("RIVET_TEST_INT_{suffix}");
let float_key = format!("RIVET_TEST_FLOAT_{suffix}");
let string_key = format!("RIVET_TEST_STRING_{suffix}");
let empty_key = format!("RIVET_TEST_EMPTY_{suffix}");
std::env::set_var(&bool_key, "TRUE");
std::env::set_var(&int_key, "42");
std::env::set_var(&float_key, "2.5");
std::env::set_var(&string_key, "alpha");
std::env::set_var(&empty_key, "");
let reader = EnvReader;
assert_eq!(reader.env(&bool_key, false), ConfigValue::Boolean(true));
assert_eq!(reader.env(&int_key, 0i64), ConfigValue::Integer(42));
assert_eq!(reader.env(&float_key, 0.0f64), ConfigValue::Float(2.5));
assert_eq!(
reader.env(&string_key, "fallback"),
ConfigValue::String("alpha".to_string())
);
assert_eq!(
reader.env(&empty_key, "fallback"),
ConfigValue::String(String::new())
);
std::env::remove_var(&bool_key);
std::env::remove_var(&int_key);
std::env::remove_var(&float_key);
std::env::remove_var(&string_key);
std::env::remove_var(&empty_key);
}
#[test]
fn singleton_factory_runs_once_and_caches_instance() {
let container = InMemoryContainer::new();
let calls = Arc::new(AtomicUsize::new(0));
let calls_for_factory = Arc::clone(&calls);
container
.singleton_factory::<String, _>(move || {
calls_for_factory.fetch_add(1, Ordering::SeqCst);
"cached".to_string()
})
.expect("singleton factory should register");
let first = container
.resolve::<String>()
.expect("first resolve should succeed");
let second = container
.resolve::<String>()
.expect("second resolve should succeed");
assert_eq!(first.as_str(), "cached");
assert!(Arc::ptr_eq(&first, &second));
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn factory_runs_on_each_resolve_with_new_instance() {
let container = InMemoryContainer::new();
let calls = Arc::new(AtomicUsize::new(0));
let calls_for_factory = Arc::clone(&calls);
container
.factory::<usize, _>(move || calls_for_factory.fetch_add(1, Ordering::SeqCst) + 1)
.expect("factory should register");
let first = container
.resolve::<usize>()
.expect("first resolve should succeed");
let second = container
.resolve::<usize>()
.expect("second resolve should succeed");
assert_eq!(*first, 1);
assert_eq!(*second, 2);
assert!(!Arc::ptr_eq(&first, &second));
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[test]
fn bind_aliases_abstract_to_concrete_name() {
let container = InMemoryContainer::new();
container
.singleton::<String>("hello".to_string())
.expect("singleton registration should succeed");
container
.bind("contracts::Greeter", core::any::type_name::<String>())
.expect("binding should register");
let resolved_any = container
.resolve_any("contracts::Greeter")
.expect("abstract resolution should succeed");
let resolved = resolved_any
.downcast::<String>()
.expect("bound resolution should downcast to concrete");
assert_eq!(resolved.as_str(), "hello");
}
#[test]
fn bind_helpers_cover_type_and_name_paths() {
let container = InMemoryContainer::new();
container
.singleton::<String>("world".to_string())
.expect("singleton registration should succeed");
container
.bind_types::<str, String>()
.expect("type binding should succeed");
container
.bind_names("contracts::Alias", core::any::type_name::<String>())
.expect("name binding should succeed");
let typed = container
.resolve_any(core::any::type_name::<str>())
.expect("typed binding should resolve")
.downcast::<String>()
.expect("typed binding should downcast");
assert_eq!(typed.as_str(), "world");
let named = container
.resolve_any("contracts::Alias")
.expect("name binding should resolve")
.downcast::<String>()
.expect("name binding should downcast");
assert_eq!(named.as_str(), "world");
}
#[test]
fn resolve_reports_downcast_failure_with_type_context() {
let container = FakeContainer::default();
container
.singleton_any(core::any::type_name::<String>(), Arc::new(42usize))
.expect("fake singleton insert should succeed");
let err = container
.resolve::<String>()
.expect_err("downcast mismatch should fail");
assert!(
err.to_string()
.contains("failed downcast for alloc::string::String"),
"unexpected error: {err}"
);
}
#[test]
fn service_provider_default_name_keeps_non_provider_suffix() {
#[derive(Default)]
struct PlainService;
impl ServiceProvider for PlainService {
fn register(&self, _container: &dyn Container) -> Result<(), FoundationError> {
Ok(())
}
fn boot(&self, _container: &dyn Container) -> Result<(), FoundationError> {
Ok(())
}
}
let provider = PlainService;
assert_eq!(provider.name(), "PlainService");
}
#[test]
fn in_memory_container_provider_noops_and_not_found_resolve_any() {
#[derive(Default)]
struct NoopProvider;
impl ServiceProvider for NoopProvider {
fn register(&self, _container: &dyn Container) -> Result<(), FoundationError> {
Ok(())
}
fn boot(&self, _container: &dyn Container) -> Result<(), FoundationError> {
Ok(())
}
}
let container = InMemoryContainer::new();
container
.register_provider(Box::new(NoopProvider))
.expect("register_provider noop should succeed");
container
.boot_providers()
.expect("boot_providers noop should succeed");
let err = container
.resolve_any("missing::Service")
.expect_err("missing service should fail");
assert_eq!(
err,
FoundationError::ServiceResolve("missing::Service".to_string())
);
}
#[test]
fn config_value_from_vec_of_string_slices() {
let value = ConfigValue::from(vec!["daily", "stdout"]);
assert_eq!(
value,
ConfigValue::Array(vec![
ConfigValue::String("daily".to_string()),
ConfigValue::String("stdout".to_string()),
])
);
}
#[test]
fn config_value_from_array_of_string_slices() {
let value = ConfigValue::from(["daily", "stdout"]);
assert_eq!(
value,
ConfigValue::Array(vec![
ConfigValue::String("daily".to_string()),
ConfigValue::String("stdout".to_string()),
])
);
}
}