Skip to main content

rivet_foundation/
lib.rs

1use std::any::Any;
2use std::collections::BTreeMap;
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::sync::Mutex;
6
7#[derive(Debug, Clone, PartialEq)]
8pub enum ConfigValue {
9    Null,
10    String(String),
11    Integer(i64),
12    Float(f64),
13    Boolean(bool),
14    Array(Vec<ConfigValue>),
15    Object(BTreeMap<String, ConfigValue>),
16}
17
18impl From<String> for ConfigValue {
19    fn from(value: String) -> Self {
20        Self::String(value)
21    }
22}
23
24impl From<&str> for ConfigValue {
25    fn from(value: &str) -> Self {
26        Self::String(value.to_string())
27    }
28}
29
30impl From<bool> for ConfigValue {
31    fn from(value: bool) -> Self {
32        Self::Boolean(value)
33    }
34}
35
36impl From<i64> for ConfigValue {
37    fn from(value: i64) -> Self {
38        Self::Integer(value)
39    }
40}
41
42impl From<i32> for ConfigValue {
43    fn from(value: i32) -> Self {
44        Self::Integer(value as i64)
45    }
46}
47
48impl From<usize> for ConfigValue {
49    fn from(value: usize) -> Self {
50        Self::Integer(value as i64)
51    }
52}
53
54impl From<f64> for ConfigValue {
55    fn from(value: f64) -> Self {
56        Self::Float(value)
57    }
58}
59
60impl From<f32> for ConfigValue {
61    fn from(value: f32) -> Self {
62        Self::Float(value as f64)
63    }
64}
65
66impl<T> From<Vec<T>> for ConfigValue
67where
68    T: Into<ConfigValue>,
69{
70    fn from(values: Vec<T>) -> Self {
71        Self::Array(values.into_iter().map(Into::into).collect())
72    }
73}
74
75impl<T, const N: usize> From<[T; N]> for ConfigValue
76where
77    T: Into<ConfigValue>,
78{
79    fn from(values: [T; N]) -> Self {
80        Self::Array(values.into_iter().map(Into::into).collect())
81    }
82}
83
84pub trait FromConfigValue: Sized {
85    fn from_config_value(value: ConfigValue) -> Option<Self>;
86}
87
88impl FromConfigValue for ConfigValue {
89    fn from_config_value(value: ConfigValue) -> Option<Self> {
90        Some(value)
91    }
92}
93
94impl FromConfigValue for String {
95    fn from_config_value(value: ConfigValue) -> Option<Self> {
96        match value {
97            ConfigValue::String(value) => Some(value),
98            ConfigValue::Integer(value) => Some(value.to_string()),
99            ConfigValue::Float(value) => Some(value.to_string()),
100            ConfigValue::Boolean(value) => Some(value.to_string()),
101            _ => None,
102        }
103    }
104}
105
106impl FromConfigValue for i64 {
107    fn from_config_value(value: ConfigValue) -> Option<Self> {
108        match value {
109            ConfigValue::Integer(value) => Some(value),
110            ConfigValue::String(value) => value.parse().ok(),
111            _ => None,
112        }
113    }
114}
115
116impl FromConfigValue for f64 {
117    fn from_config_value(value: ConfigValue) -> Option<Self> {
118        match value {
119            ConfigValue::Float(value) => Some(value),
120            ConfigValue::Integer(value) => Some(value as f64),
121            ConfigValue::String(value) => value.parse().ok(),
122            _ => None,
123        }
124    }
125}
126
127impl FromConfigValue for bool {
128    fn from_config_value(value: ConfigValue) -> Option<Self> {
129        match value {
130            ConfigValue::Boolean(value) => Some(value),
131            ConfigValue::String(value) => match value.to_ascii_lowercase().as_str() {
132                "true" => Some(true),
133                "false" => Some(false),
134                _ => None,
135            },
136            _ => None,
137        }
138    }
139}
140
141fn parse_env_value(value: String) -> ConfigValue {
142    let lower = value.to_ascii_lowercase();
143    if lower == "true" {
144        return ConfigValue::Boolean(true);
145    }
146    if lower == "false" {
147        return ConfigValue::Boolean(false);
148    }
149    if let Ok(int) = value.parse::<i64>() {
150        return ConfigValue::Integer(int);
151    }
152    if let Ok(float) = value.parse::<f64>() {
153        return ConfigValue::Float(float);
154    }
155    ConfigValue::String(value)
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
159pub enum FoundationError {
160    #[error("service registration failed: {0}")]
161    ServiceRegistration(String),
162    #[error("service boot failed: {0}")]
163    ServiceBoot(String),
164    #[error("service resolution failed: {0}")]
165    ServiceResolve(String),
166    #[error("config access failed: {0}")]
167    Config(String),
168}
169
170type DynInstance = Arc<dyn Any + Send + Sync>;
171type SingletonFactory = Box<dyn FnOnce() -> DynInstance + Send>;
172type TransientFactory = Arc<dyn Fn() -> DynInstance + Send + Sync>;
173
174pub trait Container: Any + Send + Sync {
175    fn register_provider(&self, provider: Box<dyn ServiceProvider>) -> Result<(), FoundationError>;
176    fn boot_providers(&self) -> Result<(), FoundationError>;
177
178    fn singleton_any(
179        &self,
180        type_name: &'static str,
181        value: DynInstance,
182    ) -> Result<(), FoundationError>;
183    fn singleton_factory_any(
184        &self,
185        type_name: &'static str,
186        factory: SingletonFactory,
187    ) -> Result<(), FoundationError>;
188    fn bind(
189        &self,
190        abstract_name: &'static str,
191        concrete_name: &'static str,
192    ) -> Result<(), FoundationError>;
193    fn factory_any(
194        &self,
195        type_name: &'static str,
196        factory: TransientFactory,
197    ) -> Result<(), FoundationError>;
198
199    fn resolve_any(
200        &self,
201        type_name: &'static str,
202    ) -> Result<Arc<dyn Any + Send + Sync>, FoundationError>;
203}
204
205pub trait ContainerRegistrationExt {
206    fn singleton<T: Any + Send + Sync + 'static>(&self, value: T) -> Result<(), FoundationError>;
207    fn singleton_factory<T, F>(&self, factory: F) -> Result<(), FoundationError>
208    where
209        T: Any + Send + Sync + 'static,
210        F: FnOnce() -> T + Send + 'static;
211    fn bind_types<TAbstract: ?Sized + 'static, TConcrete: Any + Send + Sync + 'static>(
212        &self,
213    ) -> Result<(), FoundationError>;
214    fn bind_names(
215        &self,
216        abstract_name: &'static str,
217        concrete_name: &'static str,
218    ) -> Result<(), FoundationError>;
219    fn factory<T, F>(&self, factory: F) -> Result<(), FoundationError>
220    where
221        T: Any + Send + Sync + 'static,
222        F: Fn() -> T + Send + Sync + 'static;
223}
224
225impl<TContainer> ContainerRegistrationExt for TContainer
226where
227    TContainer: Container + ?Sized,
228{
229    fn singleton<T: Any + Send + Sync + 'static>(&self, value: T) -> Result<(), FoundationError> {
230        self.singleton_any(core::any::type_name::<T>(), Arc::new(value))
231    }
232
233    fn singleton_factory<T, F>(&self, factory: F) -> Result<(), FoundationError>
234    where
235        T: Any + Send + Sync + 'static,
236        F: FnOnce() -> T + Send + 'static,
237    {
238        self.singleton_factory_any(
239            core::any::type_name::<T>(),
240            Box::new(move || Arc::new(factory()) as DynInstance),
241        )
242    }
243
244    fn bind_types<TAbstract: ?Sized + 'static, TConcrete: Any + Send + Sync + 'static>(
245        &self,
246    ) -> Result<(), FoundationError> {
247        self.bind(
248            core::any::type_name::<TAbstract>(),
249            core::any::type_name::<TConcrete>(),
250        )
251    }
252
253    fn bind_names(
254        &self,
255        abstract_name: &'static str,
256        concrete_name: &'static str,
257    ) -> Result<(), FoundationError> {
258        self.bind(abstract_name, concrete_name)
259    }
260
261    fn factory<T, F>(&self, factory: F) -> Result<(), FoundationError>
262    where
263        T: Any + Send + Sync + 'static,
264        F: Fn() -> T + Send + Sync + 'static,
265    {
266        self.factory_any(
267            core::any::type_name::<T>(),
268            Arc::new(move || Arc::new(factory()) as DynInstance),
269        )
270    }
271}
272
273pub trait ContainerResolveExt {
274    fn resolve<T: Any + Send + Sync + 'static>(&self) -> Result<Arc<T>, FoundationError>;
275}
276
277impl<TContainer> ContainerResolveExt for TContainer
278where
279    TContainer: Container + ?Sized,
280{
281    fn resolve<T: Any + Send + Sync + 'static>(&self) -> Result<Arc<T>, FoundationError> {
282        self.resolve_any(core::any::type_name::<T>())?
283            .downcast::<T>()
284            .map_err(|_| {
285                FoundationError::ServiceResolve(format!(
286                    "failed downcast for {}",
287                    core::any::type_name::<T>()
288                ))
289            })
290    }
291}
292
293pub trait ServiceProvider: Send + Sync {
294    fn register(&self, container: &dyn Container) -> Result<(), FoundationError>;
295    fn boot(&self, container: &dyn Container) -> Result<(), FoundationError>;
296
297    fn name(&self) -> &'static str {
298        let short = core::any::type_name::<Self>()
299            .rsplit("::")
300            .next()
301            .unwrap_or(core::any::type_name::<Self>());
302
303        if let Some(trimmed) = short.strip_suffix("Provider") {
304            if !trimmed.is_empty() {
305                return trimmed;
306            }
307        }
308
309        short
310    }
311
312    fn factory() -> Box<dyn ServiceProvider>
313    where
314        Self: Default + Sized + 'static,
315    {
316        Box::new(Self::default())
317    }
318}
319
320pub trait Config: Send + Sync {
321    fn get(&self, key: &str) -> Option<ConfigValue>;
322    fn set(&mut self, key: &str, value: ConfigValue) -> Result<(), FoundationError>;
323}
324
325#[derive(Debug, Default, Clone, Copy)]
326pub struct EnvReader;
327
328impl EnvReader {
329    pub fn get(&self, key: &str) -> Option<String> {
330        std::env::var(key).ok()
331    }
332
333    pub fn env(&self, key: &str, default: impl Into<ConfigValue>) -> ConfigValue {
334        match self.get(key) {
335            Some(value) => parse_env_value(value),
336            None => default.into(),
337        }
338    }
339}
340
341#[derive(Debug, Default)]
342pub struct ConfigRepository {
343    values: BTreeMap<String, ConfigValue>,
344}
345
346impl ConfigRepository {
347    pub fn new() -> Self {
348        Self::default()
349    }
350
351    pub fn with_values(values: BTreeMap<String, ConfigValue>) -> Self {
352        Self { values }
353    }
354
355    pub fn env(&self, key: &str, default: impl Into<ConfigValue>) -> ConfigValue {
356        EnvReader.env(key, default)
357    }
358
359    fn parse_segments<'a>(&self, key: &'a str) -> Result<Vec<&'a str>, FoundationError> {
360        let segments: Vec<&str> = key.split('.').collect();
361
362        if segments.is_empty() || segments.iter().any(|segment| segment.is_empty()) {
363            return Err(FoundationError::Config(format!(
364                "invalid config key '{key}'"
365            )));
366        }
367
368        Ok(segments)
369    }
370}
371
372impl Config for ConfigRepository {
373    fn get(&self, key: &str) -> Option<ConfigValue> {
374        let segments = self.parse_segments(key).ok()?;
375        let mut current = self.values.get(*segments.first()?)?;
376
377        for segment in segments.iter().skip(1) {
378            match current {
379                ConfigValue::Object(map) => {
380                    current = map.get(*segment)?;
381                }
382                _ => return None,
383            }
384        }
385
386        Some(current.clone())
387    }
388
389    fn set(&mut self, key: &str, value: ConfigValue) -> Result<(), FoundationError> {
390        let segments = self.parse_segments(key)?;
391
392        let mut current = &mut self.values;
393        for segment in segments.iter().take(segments.len() - 1) {
394            let key = (*segment).to_string();
395            let next = current
396                .entry(key)
397                .or_insert_with(|| ConfigValue::Object(BTreeMap::new()));
398
399            match next {
400                ConfigValue::Object(map) => current = map,
401                _ => {
402                    return Err(FoundationError::Config(
403                        "cannot traverse through non-object config value".to_string(),
404                    ))
405                }
406            }
407        }
408
409        current.insert(
410            segments
411                .last()
412                .expect("parse_segments must return at least one segment")
413                .to_string(),
414            value,
415        );
416        Ok(())
417    }
418}
419
420#[derive(Clone, Default)]
421pub struct SharedConfigRepository {
422    inner: Arc<Mutex<ConfigRepository>>,
423}
424
425impl SharedConfigRepository {
426    pub fn new(repository: ConfigRepository) -> Self {
427        Self {
428            inner: Arc::new(Mutex::new(repository)),
429        }
430    }
431
432    pub fn env(&self, key: &str, default: impl Into<ConfigValue>) -> ConfigValue {
433        self.inner
434            .lock()
435            .expect("config repository lock poisoned")
436            .env(key, default)
437    }
438}
439
440impl Config for SharedConfigRepository {
441    fn get(&self, key: &str) -> Option<ConfigValue> {
442        self.inner
443            .lock()
444            .expect("config repository lock poisoned")
445            .get(key)
446    }
447
448    fn set(&mut self, key: &str, value: ConfigValue) -> Result<(), FoundationError> {
449        self.inner
450            .lock()
451            .expect("config repository lock poisoned")
452            .set(key, value)
453    }
454}
455
456#[derive(Default)]
457pub struct InMemoryConfig {
458    repository: ConfigRepository,
459}
460
461impl Config for InMemoryConfig {
462    fn get(&self, key: &str) -> Option<ConfigValue> {
463        self.repository.get(key)
464    }
465
466    fn set(&mut self, key: &str, value: ConfigValue) -> Result<(), FoundationError> {
467        self.repository.set(key, value)
468    }
469}
470
471#[derive(Default)]
472pub struct InMemoryContainer {
473    inner: Mutex<InMemoryContainerInner>,
474}
475
476#[derive(Default)]
477struct InMemoryContainerInner {
478    singletons: HashMap<&'static str, DynInstance>,
479    singleton_factories: HashMap<&'static str, SingletonFactory>,
480    factories: HashMap<&'static str, TransientFactory>,
481    bindings: HashMap<&'static str, &'static str>,
482}
483
484impl InMemoryContainer {
485    pub fn new() -> Self {
486        Self::default()
487    }
488}
489
490impl Container for InMemoryContainer {
491    fn register_provider(
492        &self,
493        _provider: Box<dyn ServiceProvider>,
494    ) -> Result<(), FoundationError> {
495        Ok(())
496    }
497
498    fn boot_providers(&self) -> Result<(), FoundationError> {
499        Ok(())
500    }
501
502    fn singleton_any(
503        &self,
504        type_name: &'static str,
505        value: DynInstance,
506    ) -> Result<(), FoundationError> {
507        self.inner
508            .lock()
509            .map_err(|_| {
510                FoundationError::ServiceRegistration("services lock poisoned".to_string())
511            })?
512            .singletons
513            .insert(type_name, value);
514        Ok(())
515    }
516
517    fn singleton_factory_any(
518        &self,
519        type_name: &'static str,
520        factory: SingletonFactory,
521    ) -> Result<(), FoundationError> {
522        self.inner
523            .lock()
524            .map_err(|_| {
525                FoundationError::ServiceRegistration("services lock poisoned".to_string())
526            })?
527            .singleton_factories
528            .insert(type_name, factory);
529        Ok(())
530    }
531
532    fn bind(
533        &self,
534        abstract_name: &'static str,
535        concrete_name: &'static str,
536    ) -> Result<(), FoundationError> {
537        self.inner
538            .lock()
539            .map_err(|_| {
540                FoundationError::ServiceRegistration("services lock poisoned".to_string())
541            })?
542            .bindings
543            .insert(abstract_name, concrete_name);
544        Ok(())
545    }
546
547    fn factory_any(
548        &self,
549        type_name: &'static str,
550        factory: TransientFactory,
551    ) -> Result<(), FoundationError> {
552        self.inner
553            .lock()
554            .map_err(|_| {
555                FoundationError::ServiceRegistration("services lock poisoned".to_string())
556            })?
557            .factories
558            .insert(type_name, factory);
559        Ok(())
560    }
561
562    fn resolve_any(
563        &self,
564        type_name: &'static str,
565    ) -> Result<Arc<dyn Any + Send + Sync>, FoundationError> {
566        let resolved_name = {
567            let inner = self.inner.lock().map_err(|_| {
568                FoundationError::ServiceResolve("services lock poisoned".to_string())
569            })?;
570            *inner.bindings.get(type_name).unwrap_or(&type_name)
571        };
572
573        if let Some(instance) = self
574            .inner
575            .lock()
576            .map_err(|_| FoundationError::ServiceResolve("services lock poisoned".to_string()))?
577            .singletons
578            .get(resolved_name)
579            .cloned()
580        {
581            return Ok(instance);
582        }
583
584        let singleton_factory = self
585            .inner
586            .lock()
587            .map_err(|_| FoundationError::ServiceResolve("services lock poisoned".to_string()))?
588            .singleton_factories
589            .remove(resolved_name);
590
591        if let Some(factory) = singleton_factory {
592            let instance = factory();
593            self.inner
594                .lock()
595                .map_err(|_| FoundationError::ServiceResolve("services lock poisoned".to_string()))?
596                .singletons
597                .insert(resolved_name, Arc::clone(&instance));
598            return Ok(instance);
599        }
600
601        if let Some(factory) = self
602            .inner
603            .lock()
604            .map_err(|_| FoundationError::ServiceResolve("services lock poisoned".to_string()))?
605            .factories
606            .get(resolved_name)
607            .cloned()
608        {
609            return Ok(factory());
610        }
611
612        Err(FoundationError::ServiceResolve(type_name.to_string()))
613    }
614}
615
616pub fn defaults() -> (Arc<dyn Container>, Box<dyn Config>) {
617    (
618        Arc::new(InMemoryContainer::new()) as Arc<dyn Container>,
619        Box::new(InMemoryConfig::default()) as Box<dyn Config>,
620    )
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use std::sync::atomic::{AtomicUsize, Ordering};
627
628    static ENV_TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
629
630    #[derive(Default)]
631    struct FakeContainer {
632        services: Mutex<HashMap<&'static str, Arc<dyn Any + Send + Sync>>>,
633    }
634
635    impl FakeContainer {
636        fn insert<T: Any + Send + Sync + 'static>(&self, value: T) {
637            self.services
638                .lock()
639                .expect("services lock poisoned")
640                .insert(core::any::type_name::<T>(), Arc::new(value));
641        }
642    }
643
644    impl Container for FakeContainer {
645        fn register_provider(
646            &self,
647            _provider: Box<dyn ServiceProvider>,
648        ) -> Result<(), FoundationError> {
649            Ok(())
650        }
651
652        fn boot_providers(&self) -> Result<(), FoundationError> {
653            Ok(())
654        }
655
656        fn singleton_any(
657            &self,
658            type_name: &'static str,
659            value: DynInstance,
660        ) -> Result<(), FoundationError> {
661            self.services
662                .lock()
663                .expect("services lock poisoned")
664                .insert(type_name, value);
665            Ok(())
666        }
667
668        fn singleton_factory_any(
669            &self,
670            _type_name: &'static str,
671            _factory: SingletonFactory,
672        ) -> Result<(), FoundationError> {
673            Err(FoundationError::ServiceRegistration(
674                "fake container does not support singleton_factory".to_string(),
675            ))
676        }
677
678        fn bind(
679            &self,
680            _abstract_name: &'static str,
681            _concrete_name: &'static str,
682        ) -> Result<(), FoundationError> {
683            Err(FoundationError::ServiceRegistration(
684                "fake container does not support bindings".to_string(),
685            ))
686        }
687
688        fn factory_any(
689            &self,
690            _type_name: &'static str,
691            _factory: TransientFactory,
692        ) -> Result<(), FoundationError> {
693            Err(FoundationError::ServiceRegistration(
694                "fake container does not support factories".to_string(),
695            ))
696        }
697
698        fn resolve_any(
699            &self,
700            type_name: &'static str,
701        ) -> Result<Arc<dyn Any + Send + Sync>, FoundationError> {
702            self.services
703                .lock()
704                .expect("services lock poisoned")
705                .get(type_name)
706                .cloned()
707                .ok_or_else(|| FoundationError::ServiceResolve(type_name.to_string()))
708        }
709    }
710
711    #[test]
712    fn typed_resolve_returns_registered_service() {
713        let container = FakeContainer::default();
714        container.insert::<String>("hello".to_string());
715
716        let resolved = container
717            .resolve::<String>()
718            .expect("service should resolve");
719        assert_eq!(resolved.as_str(), "hello");
720    }
721
722    #[test]
723    fn typed_resolve_returns_not_found_error() {
724        let container = FakeContainer::default();
725        let err = container
726            .resolve::<String>()
727            .expect_err("missing service should fail");
728
729        assert_eq!(
730            err,
731            FoundationError::ServiceResolve(core::any::type_name::<String>().to_string())
732        );
733    }
734
735    #[test]
736    fn in_memory_container_singleton_round_trip() {
737        let container = InMemoryContainer::new();
738        container
739            .singleton::<String>("hello".to_string())
740            .expect("singleton insert should succeed");
741
742        let resolved = container
743            .resolve::<String>()
744            .expect("singleton resolve should succeed");
745        assert_eq!(resolved.as_str(), "hello");
746    }
747
748    #[test]
749    fn defaults_return_usable_backends() {
750        let (_container, mut config) = defaults();
751        config
752            .set("app.name", ConfigValue::String("demo".to_string()))
753            .expect("config set should succeed");
754
755        assert_eq!(
756            config.get("app.name"),
757            Some(ConfigValue::String("demo".to_string()))
758        );
759    }
760
761    #[test]
762    fn config_repository_supports_dot_set_and_get_with_intermediate_nodes() {
763        let mut repository = ConfigRepository::new();
764        repository
765            .set("services.mailgun.timeout", ConfigValue::Integer(30))
766            .expect("nested set should succeed");
767
768        assert_eq!(
769            repository.get("services.mailgun.timeout"),
770            Some(ConfigValue::Integer(30))
771        );
772    }
773
774    #[test]
775    fn config_repository_returns_none_for_non_object_traversal_reads() {
776        let mut repository = ConfigRepository::new();
777        repository
778            .set("app", ConfigValue::String("demo".to_string()))
779            .expect("top-level set should succeed");
780
781        assert_eq!(repository.get("app.name"), None);
782    }
783
784    #[test]
785    fn config_repository_rejects_non_object_traversal_writes() {
786        let mut repository = ConfigRepository::new();
787        repository
788            .set("app", ConfigValue::String("demo".to_string()))
789            .expect("top-level set should succeed");
790
791        let err = repository
792            .set("app.name", ConfigValue::String("demo".to_string()))
793            .expect_err("set should fail when traversing through scalar");
794
795        assert_eq!(
796            err,
797            FoundationError::Config("cannot traverse through non-object config value".to_string())
798        );
799    }
800
801    #[test]
802    fn config_repository_overwrites_existing_values_at_target_key() {
803        let mut repository = ConfigRepository::new();
804        repository
805            .set("app.name", ConfigValue::String("demo".to_string()))
806            .expect("initial set should succeed");
807        repository
808            .set("app.name", ConfigValue::String("new-name".to_string()))
809            .expect("value overwrite should succeed");
810        repository
811            .set(
812                "app",
813                ConfigValue::Object(BTreeMap::from([(
814                    "env".to_string(),
815                    ConfigValue::String("local".to_string()),
816                )])),
817            )
818            .expect("object overwrite should succeed");
819        repository
820            .set("app", ConfigValue::String("scalar".to_string()))
821            .expect("scalar overwrite should succeed");
822
823        assert_eq!(
824            repository.get("app"),
825            Some(ConfigValue::String("scalar".to_string()))
826        );
827    }
828
829    #[test]
830    fn shared_config_repository_shares_state_across_clones() {
831        let mut shared = SharedConfigRepository::new(ConfigRepository::new());
832        shared
833            .set("app.name", ConfigValue::String("demo".to_string()))
834            .expect("set through first handle should succeed");
835
836        let reader = shared.clone();
837        assert_eq!(
838            reader.get("app.name"),
839            Some(ConfigValue::String("demo".to_string()))
840        );
841    }
842
843    #[test]
844    fn env_reader_returns_default_when_variable_is_missing() {
845        let key = format!(
846            "RIVET_TEST_MISSING_{}",
847            ENV_TEST_COUNTER.fetch_add(1, Ordering::SeqCst)
848        );
849        std::env::remove_var(&key);
850
851        let value = EnvReader.env(&key, "fallback");
852        assert_eq!(value, ConfigValue::String("fallback".to_string()));
853    }
854
855    #[test]
856    fn env_reader_best_effort_casts_values() {
857        let suffix = ENV_TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
858        let bool_key = format!("RIVET_TEST_BOOL_{suffix}");
859        let int_key = format!("RIVET_TEST_INT_{suffix}");
860        let float_key = format!("RIVET_TEST_FLOAT_{suffix}");
861        let string_key = format!("RIVET_TEST_STRING_{suffix}");
862        let empty_key = format!("RIVET_TEST_EMPTY_{suffix}");
863
864        std::env::set_var(&bool_key, "TRUE");
865        std::env::set_var(&int_key, "42");
866        std::env::set_var(&float_key, "2.5");
867        std::env::set_var(&string_key, "alpha");
868        std::env::set_var(&empty_key, "");
869
870        let reader = EnvReader;
871        assert_eq!(reader.env(&bool_key, false), ConfigValue::Boolean(true));
872        assert_eq!(reader.env(&int_key, 0i64), ConfigValue::Integer(42));
873        assert_eq!(reader.env(&float_key, 0.0f64), ConfigValue::Float(2.5));
874        assert_eq!(
875            reader.env(&string_key, "fallback"),
876            ConfigValue::String("alpha".to_string())
877        );
878        assert_eq!(
879            reader.env(&empty_key, "fallback"),
880            ConfigValue::String(String::new())
881        );
882
883        std::env::remove_var(&bool_key);
884        std::env::remove_var(&int_key);
885        std::env::remove_var(&float_key);
886        std::env::remove_var(&string_key);
887        std::env::remove_var(&empty_key);
888    }
889
890    #[test]
891    fn singleton_factory_runs_once_and_caches_instance() {
892        let container = InMemoryContainer::new();
893        let calls = Arc::new(AtomicUsize::new(0));
894        let calls_for_factory = Arc::clone(&calls);
895
896        container
897            .singleton_factory::<String, _>(move || {
898                calls_for_factory.fetch_add(1, Ordering::SeqCst);
899                "cached".to_string()
900            })
901            .expect("singleton factory should register");
902
903        let first = container
904            .resolve::<String>()
905            .expect("first resolve should succeed");
906        let second = container
907            .resolve::<String>()
908            .expect("second resolve should succeed");
909
910        assert_eq!(first.as_str(), "cached");
911        assert!(Arc::ptr_eq(&first, &second));
912        assert_eq!(calls.load(Ordering::SeqCst), 1);
913    }
914
915    #[test]
916    fn factory_runs_on_each_resolve_with_new_instance() {
917        let container = InMemoryContainer::new();
918        let calls = Arc::new(AtomicUsize::new(0));
919        let calls_for_factory = Arc::clone(&calls);
920
921        container
922            .factory::<usize, _>(move || calls_for_factory.fetch_add(1, Ordering::SeqCst) + 1)
923            .expect("factory should register");
924
925        let first = container
926            .resolve::<usize>()
927            .expect("first resolve should succeed");
928        let second = container
929            .resolve::<usize>()
930            .expect("second resolve should succeed");
931
932        assert_eq!(*first, 1);
933        assert_eq!(*second, 2);
934        assert!(!Arc::ptr_eq(&first, &second));
935        assert_eq!(calls.load(Ordering::SeqCst), 2);
936    }
937
938    #[test]
939    fn bind_aliases_abstract_to_concrete_name() {
940        let container = InMemoryContainer::new();
941        container
942            .singleton::<String>("hello".to_string())
943            .expect("singleton registration should succeed");
944        container
945            .bind("contracts::Greeter", core::any::type_name::<String>())
946            .expect("binding should register");
947
948        let resolved_any = container
949            .resolve_any("contracts::Greeter")
950            .expect("abstract resolution should succeed");
951        let resolved = resolved_any
952            .downcast::<String>()
953            .expect("bound resolution should downcast to concrete");
954
955        assert_eq!(resolved.as_str(), "hello");
956    }
957
958    #[test]
959    fn bind_helpers_cover_type_and_name_paths() {
960        let container = InMemoryContainer::new();
961        container
962            .singleton::<String>("world".to_string())
963            .expect("singleton registration should succeed");
964
965        container
966            .bind_types::<str, String>()
967            .expect("type binding should succeed");
968        container
969            .bind_names("contracts::Alias", core::any::type_name::<String>())
970            .expect("name binding should succeed");
971
972        let typed = container
973            .resolve_any(core::any::type_name::<str>())
974            .expect("typed binding should resolve")
975            .downcast::<String>()
976            .expect("typed binding should downcast");
977        assert_eq!(typed.as_str(), "world");
978
979        let named = container
980            .resolve_any("contracts::Alias")
981            .expect("name binding should resolve")
982            .downcast::<String>()
983            .expect("name binding should downcast");
984        assert_eq!(named.as_str(), "world");
985    }
986
987    #[test]
988    fn resolve_reports_downcast_failure_with_type_context() {
989        let container = FakeContainer::default();
990        container
991            .singleton_any(core::any::type_name::<String>(), Arc::new(42usize))
992            .expect("fake singleton insert should succeed");
993
994        let err = container
995            .resolve::<String>()
996            .expect_err("downcast mismatch should fail");
997        assert!(
998            err.to_string()
999                .contains("failed downcast for alloc::string::String"),
1000            "unexpected error: {err}"
1001        );
1002    }
1003
1004    #[test]
1005    fn service_provider_default_name_keeps_non_provider_suffix() {
1006        #[derive(Default)]
1007        struct PlainService;
1008
1009        impl ServiceProvider for PlainService {
1010            fn register(&self, _container: &dyn Container) -> Result<(), FoundationError> {
1011                Ok(())
1012            }
1013
1014            fn boot(&self, _container: &dyn Container) -> Result<(), FoundationError> {
1015                Ok(())
1016            }
1017        }
1018
1019        let provider = PlainService;
1020        assert_eq!(provider.name(), "PlainService");
1021    }
1022
1023    #[test]
1024    fn in_memory_container_provider_noops_and_not_found_resolve_any() {
1025        #[derive(Default)]
1026        struct NoopProvider;
1027
1028        impl ServiceProvider for NoopProvider {
1029            fn register(&self, _container: &dyn Container) -> Result<(), FoundationError> {
1030                Ok(())
1031            }
1032
1033            fn boot(&self, _container: &dyn Container) -> Result<(), FoundationError> {
1034                Ok(())
1035            }
1036        }
1037
1038        let container = InMemoryContainer::new();
1039        container
1040            .register_provider(Box::new(NoopProvider))
1041            .expect("register_provider noop should succeed");
1042        container
1043            .boot_providers()
1044            .expect("boot_providers noop should succeed");
1045
1046        let err = container
1047            .resolve_any("missing::Service")
1048            .expect_err("missing service should fail");
1049        assert_eq!(
1050            err,
1051            FoundationError::ServiceResolve("missing::Service".to_string())
1052        );
1053    }
1054
1055    #[test]
1056    fn config_value_from_vec_of_string_slices() {
1057        let value = ConfigValue::from(vec!["daily", "stdout"]);
1058        assert_eq!(
1059            value,
1060            ConfigValue::Array(vec![
1061                ConfigValue::String("daily".to_string()),
1062                ConfigValue::String("stdout".to_string()),
1063            ])
1064        );
1065    }
1066
1067    #[test]
1068    fn config_value_from_array_of_string_slices() {
1069        let value = ConfigValue::from(["daily", "stdout"]);
1070        assert_eq!(
1071            value,
1072            ConfigValue::Array(vec![
1073                ConfigValue::String("daily".to_string()),
1074                ConfigValue::String("stdout".to_string()),
1075            ])
1076        );
1077    }
1078}