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}