1#![allow(clippy::pedantic)]
42use std::any::{Any, TypeId};
43use std::collections::{HashMap, VecDeque};
44use std::sync::atomic::{AtomicBool, Ordering};
45use std::sync::{Arc, RwLock};
46
47use async_trait::async_trait;
48use futures_util::future::BoxFuture;
49use thiserror::Error;
50
51use super::component::{AnyComponent, AnyComponentError, Component, ComponentContext};
52use super::lifecycle::ShutdownToken;
53use behest_core::health::HealthStatus;
54
55#[derive(Debug, Error)]
57#[non_exhaustive]
58pub enum RegistryError {
59 #[error("component `{name}` is already registered")]
61 AlreadyRegistered {
62 name: String,
64 },
65 #[error("component `{name}` is not registered")]
67 NotFound {
68 name: String,
70 },
71 #[error("component `{name}` depends on missing component `{dep}`")]
73 MissingDependency {
74 name: String,
76 dep: String,
78 },
79 #[error("cycle detected in component dependencies: {cycle:?}")]
82 Cycle {
83 cycle: Vec<String>,
85 },
86 #[error("init failed for component `{name}`: {message}")]
88 Init {
89 name: String,
91 message: String,
93 },
94 #[error("start failed for component `{name}`: {message}")]
96 Start {
97 name: String,
99 message: String,
101 },
102 #[error("stop failed for component `{name}`: {message}")]
105 Stop {
106 name: String,
108 message: String,
110 },
111 #[error("reload failed for component `{name}`: {message}")]
113 Reload {
114 name: String,
116 message: String,
118 },
119 #[error("component registry lock poisoned")]
121 LockPoisoned,
122 #[error("component `{name}` exists but is not of type `{type_id:?}`")]
124 TypeMismatch {
125 name: String,
127 type_id: TypeId,
129 },
130 #[error("component `{name}` has already been initialized")]
133 AlreadyInitialized {
134 name: String,
136 },
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141#[non_exhaustive]
142pub enum ComponentState {
143 Pending,
145 Initializing,
147 Initialized,
149 Starting,
151 Running,
153 Stopping,
155 Stopped,
158 Failed,
160}
161
162#[derive(Debug, Clone)]
165pub struct ComponentDescriptor {
166 pub name: String,
168 pub depends_on: Vec<String>,
170 pub config: serde_json::Value,
172}
173
174#[async_trait]
178pub trait ComponentFactory: Send + Sync {
179 fn name(&self) -> &str;
181 fn kind(&self) -> &'static str;
183 fn depends_on(&self) -> Vec<String>;
185 async fn build(
187 self: Box<Self>,
188 config: serde_json::Value,
189 ctx: &ComponentContext,
190 ) -> Result<Box<dyn AnyComponent>, RegistryError>;
191}
192
193pub struct TypedFactory<C: Component> {
196 name: String,
197 extra_deps: Vec<String>,
198 _marker: std::marker::PhantomData<fn() -> C>,
199}
200
201impl<C: Component> TypedFactory<C> {
202 #[must_use]
204 pub fn new(name: impl Into<String>, extra_deps: Vec<String>) -> Self {
205 Self {
206 name: name.into(),
207 extra_deps,
208 _marker: std::marker::PhantomData,
209 }
210 }
211}
212
213#[async_trait]
214impl<C: Component> ComponentFactory for TypedFactory<C> {
215 fn name(&self) -> &str {
216 &self.name
217 }
218
219 fn kind(&self) -> &'static str {
220 C::NAME
221 }
222
223 fn depends_on(&self) -> Vec<String> {
224 let mut deps: Vec<String> = C::depends_on().iter().map(|s| (*s).to_string()).collect();
225 for d in &self.extra_deps {
226 if !deps.contains(d) {
227 deps.push(d.clone());
228 }
229 }
230 deps
231 }
232
233 async fn build(
234 self: Box<Self>,
235 config: serde_json::Value,
236 ctx: &ComponentContext,
237 ) -> Result<Box<dyn AnyComponent>, RegistryError> {
238 let name = self.name.clone();
239 let cfg: C::Config = serde_json::from_value(config).map_err(|e| RegistryError::Init {
240 name: name.clone(),
241 message: format!("config deserialize: {e}"),
242 })?;
243 let instance = C::init(&cfg, ctx).await.map_err(|e| RegistryError::Init {
244 name: name.clone(),
245 message: e.to_string(),
246 })?;
247 Ok(Box::new(TypedAnyComponent {
248 name,
249 kind: C::NAME,
250 instance: Arc::new(instance),
251 }))
252 }
253}
254
255pub struct TypedAnyComponent<C: Component> {
258 name: String,
259 kind: &'static str,
260 instance: Arc<C>,
261}
262
263impl<C: Component> TypedAnyComponent<C> {
264 #[must_use]
266 pub fn new(instance: C) -> Self {
267 Self {
268 name: C::NAME.to_owned(),
269 kind: C::NAME,
270 instance: Arc::new(instance),
271 }
272 }
273}
274
275#[async_trait]
276impl<C: Component> AnyComponent for TypedAnyComponent<C> {
277 fn name(&self) -> &'static str {
278 self.kind
279 }
280
281 fn as_any_arc(&self) -> Arc<dyn Any + Send + Sync> {
282 self.instance.clone()
283 }
284
285 fn start(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
286 let name = self.name.clone();
287 let instance = self.instance.clone();
288 Box::pin(async move {
289 instance
290 .start()
291 .await
292 .map_err(|e| AnyComponentError::Component {
293 name,
294 message: e.to_string(),
295 })
296 })
297 }
298
299 fn stop(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
300 let name = self.name.clone();
301 let instance = self.instance.clone();
302 Box::pin(async move {
303 instance
304 .stop()
305 .await
306 .map_err(|e| AnyComponentError::Component {
307 name,
308 message: e.to_string(),
309 })
310 })
311 }
312
313 fn health(&self) -> BoxFuture<'_, HealthStatus> {
314 let instance = self.instance.clone();
315 Box::pin(async move { instance.health().await })
316 }
317
318 fn pre_replace(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
319 let name = self.name.clone();
320 let instance = self.instance.clone();
321 Box::pin(async move {
322 instance
323 .pre_replace_hook()
324 .await
325 .map_err(|e| AnyComponentError::Component {
326 name,
327 message: e.to_string(),
328 })
329 })
330 }
331
332 fn post_replace(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
333 let name = self.name.clone();
334 let instance = self.instance.clone();
335 Box::pin(async move {
336 instance
337 .post_replace_hook()
338 .await
339 .map_err(|e| AnyComponentError::Component {
340 name,
341 message: e.to_string(),
342 })
343 })
344 }
345}
346
347pub struct ComponentRegistry {
352 inner: Arc<RegistryInner>,
353}
354
355struct RegistryInner {
356 factories: RwLock<HashMap<String, Box<dyn ComponentFactory>>>,
358 descriptors: RwLock<HashMap<String, ComponentDescriptor>>,
361 instances: RwLock<HashMap<String, Arc<dyn AnyComponent>>>,
363 states: RwLock<HashMap<String, ComponentState>>,
365 topo: RwLock<Option<Vec<String>>>,
368 shutdown: ShutdownToken,
370 init_started: AtomicBool,
373}
374
375impl Default for ComponentRegistry {
376 fn default() -> Self {
377 Self::new()
378 }
379}
380
381impl ComponentRegistry {
382 #[must_use]
384 pub fn new() -> Self {
385 Self::with_shutdown(ShutdownToken::new())
386 }
387
388 #[must_use]
392 pub fn with_shutdown(shutdown: ShutdownToken) -> Self {
393 Self {
394 inner: Arc::new(RegistryInner {
395 factories: RwLock::new(HashMap::new()),
396 descriptors: RwLock::new(HashMap::new()),
397 instances: RwLock::new(HashMap::new()),
398 states: RwLock::new(HashMap::new()),
399 topo: RwLock::new(None),
400 shutdown,
401 init_started: AtomicBool::new(false),
402 }),
403 }
404 }
405
406 #[must_use]
408 pub fn shutdown(&self) -> ShutdownToken {
409 self.inner.shutdown.clone()
410 }
411
412 pub fn register_factory(
417 &self,
418 descriptor: ComponentDescriptor,
419 factory: Box<dyn ComponentFactory>,
420 ) -> Result<(), RegistryError> {
421 let name = descriptor.name.clone();
422 {
423 let mut factories = self
424 .inner
425 .factories
426 .write()
427 .map_err(|_| RegistryError::LockPoisoned)?;
428 if factories.contains_key(&name) {
429 return Err(RegistryError::AlreadyRegistered { name });
430 }
431 factories.insert(name.clone(), factory);
432 }
433 {
434 let mut descriptors = self
435 .inner
436 .descriptors
437 .write()
438 .map_err(|_| RegistryError::LockPoisoned)?;
439 descriptors.insert(name.clone(), descriptor);
440 }
441 {
442 let mut states = self
443 .inner
444 .states
445 .write()
446 .map_err(|_| RegistryError::LockPoisoned)?;
447 states.insert(name, ComponentState::Pending);
448 }
449 self.invalidate_topo();
450 Ok(())
451 }
452
453 pub fn register_typed<C: Component>(
456 &self,
457 instance_name: impl Into<String>,
458 config: serde_json::Value,
459 ) -> Result<(), RegistryError> {
460 let name = instance_name.into();
461 let factory = TypedFactory::<C>::new(name.clone(), Vec::new());
462 let descriptor = ComponentDescriptor {
463 name: name.clone(),
464 depends_on: factory.depends_on(),
465 config,
466 };
467 self.register_factory(descriptor, Box::new(factory))
468 }
469
470 pub fn unregister(&self, name: &str) -> Result<ComponentDescriptor, RegistryError> {
473 {
474 let mut factories = self
475 .inner
476 .factories
477 .write()
478 .map_err(|_| RegistryError::LockPoisoned)?;
479 factories.remove(name);
480 }
481 let descriptor = {
482 let mut descriptors = self
483 .inner
484 .descriptors
485 .write()
486 .map_err(|_| RegistryError::LockPoisoned)?;
487 descriptors
488 .remove(name)
489 .ok_or_else(|| RegistryError::NotFound {
490 name: name.to_string(),
491 })?
492 };
493 {
494 let mut states = self
495 .inner
496 .states
497 .write()
498 .map_err(|_| RegistryError::LockPoisoned)?;
499 states.remove(name);
500 }
501 self.invalidate_topo();
502 Ok(descriptor)
503 }
504
505 #[must_use]
507 pub fn len(&self) -> usize {
508 self.inner
509 .descriptors
510 .read()
511 .map(|m| m.len())
512 .unwrap_or_default()
513 }
514
515 #[must_use]
517 pub fn is_empty(&self) -> bool {
518 self.len() == 0
519 }
520
521 #[must_use]
524 pub fn names(&self) -> Vec<String> {
525 self.recompute_topo().unwrap_or_default()
526 }
527
528 pub fn get<C: Component>(&self, name: &str) -> Result<Arc<C>, RegistryError> {
539 let instances = self
540 .inner
541 .instances
542 .read()
543 .map_err(|_| RegistryError::LockPoisoned)?;
544 let instance = instances.get(name).ok_or_else(|| RegistryError::NotFound {
545 name: name.to_string(),
546 })?;
547 let any = instance.as_any_arc();
548 let type_id = any.type_id();
549 any.downcast::<C>()
550 .map_err(|_| RegistryError::TypeMismatch {
551 name: name.to_string(),
552 type_id,
553 })
554 }
555
556 #[must_use]
559 pub fn is_initialized(&self, name: &str) -> bool {
560 self.inner
561 .states
562 .read()
563 .ok()
564 .and_then(|m| m.get(name).copied())
565 .is_some_and(|s| {
566 matches!(
567 s,
568 ComponentState::Initialized
569 | ComponentState::Running
570 | ComponentState::Starting
571 | ComponentState::Stopping
572 | ComponentState::Stopped
573 )
574 })
575 }
576
577 #[must_use]
580 pub fn state_of(&self, name: &str) -> Option<ComponentState> {
581 self.inner.states.read().ok()?.get(name).copied()
582 }
583
584 pub async fn init_all(&self) -> Result<(), RegistryError> {
595 self.inner.init_started.store(true, Ordering::SeqCst);
596 let order = self.recompute_topo()?;
597 let ctx = ComponentContext::new(self.inner.shutdown.child());
598 let mut first_error: Option<RegistryError> = None;
599 for name in order {
600 let state = self.state_of(&name);
601 if matches!(
602 state,
603 Some(
604 ComponentState::Initialized
605 | ComponentState::Running
606 | ComponentState::Starting
607 | ComponentState::Stopping
608 | ComponentState::Stopped,
609 )
610 ) {
611 continue;
612 }
613 let factory_box = {
614 let mut factories = self
615 .inner
616 .factories
617 .write()
618 .map_err(|_| RegistryError::LockPoisoned)?;
619 factories.remove(&name)
620 };
621 let Some(factory_box) = factory_box else {
622 continue;
623 };
624 let config = {
625 let descriptors = self
626 .inner
627 .descriptors
628 .read()
629 .map_err(|_| RegistryError::LockPoisoned)?;
630 descriptors
631 .get(&name)
632 .map(|d| d.config.clone())
633 .unwrap_or_default()
634 };
635 self.set_state(&name, ComponentState::Initializing);
636 match factory_box.build(config, &ctx).await {
637 Ok(any) => {
638 let arc: Arc<dyn AnyComponent> = any.into();
639 self.inner
640 .instances
641 .write()
642 .map_err(|_| RegistryError::LockPoisoned)?
643 .insert(name.clone(), arc);
644 self.set_state(&name, ComponentState::Initialized);
645 }
646 Err(e) => {
647 self.set_state(&name, ComponentState::Failed);
648 if first_error.is_none() {
649 first_error = Some(e);
650 }
651 }
652 }
653 }
654 first_error.map_or(Ok(()), Err)
655 }
656
657 pub async fn start_all(&self) -> Result<(), RegistryError> {
661 let order = self.recompute_topo()?;
662 let mut first_error: Option<RegistryError> = None;
663 for name in order {
664 let state = self.state_of(&name);
665 if !matches!(state, Some(ComponentState::Initialized)) {
666 continue;
667 }
668 self.set_state(&name, ComponentState::Starting);
669 let instance = {
670 let instances = self
671 .inner
672 .instances
673 .read()
674 .map_err(|_| RegistryError::LockPoisoned)?;
675 instances.get(&name).cloned()
676 };
677 let Some(instance) = instance else {
678 continue;
679 };
680 match instance.start().await {
681 Ok(()) => {
682 self.set_state(&name, ComponentState::Running);
683 }
684 Err(e) => {
685 self.set_state(&name, ComponentState::Failed);
686 if first_error.is_none() {
687 first_error = Some(RegistryError::Start {
688 name: name.clone(),
689 message: e.to_string(),
690 });
691 }
692 }
693 }
694 }
695 first_error.map_or(Ok(()), Err)
696 }
697
698 pub async fn stop_all(&self) -> Result<(), RegistryError> {
702 let order = self.recompute_topo()?;
703 let mut first_error: Option<RegistryError> = None;
704 for name in order.into_iter().rev() {
705 let state = self.state_of(&name);
706 if !matches!(
707 state,
708 Some(ComponentState::Running | ComponentState::Initialized)
709 ) {
710 continue;
711 }
712 self.set_state(&name, ComponentState::Stopping);
713 let instance = {
714 let instances = self
715 .inner
716 .instances
717 .read()
718 .map_err(|_| RegistryError::LockPoisoned)?;
719 instances.get(&name).cloned()
720 };
721 let Some(instance) = instance else {
722 continue;
723 };
724 if let Err(e) = instance.stop().await
725 && first_error.is_none()
726 {
727 first_error = Some(RegistryError::Stop {
728 name: name.clone(),
729 message: e.to_string(),
730 });
731 }
732 self.set_state(&name, ComponentState::Stopped);
733 }
734 first_error.map_or(Ok(()), Err)
735 }
736
737 pub async fn replace_instance(
762 &self,
763 name: &str,
764 new_instance: Box<dyn AnyComponent>,
765 ) -> Result<Arc<dyn AnyComponent>, RegistryError> {
766 let old_instance = {
767 let instances = self
768 .inner
769 .instances
770 .read()
771 .map_err(|_| RegistryError::LockPoisoned)?;
772 instances
773 .get(name)
774 .cloned()
775 .ok_or_else(|| RegistryError::NotFound {
776 name: name.to_string(),
777 })?
778 };
779
780 if !matches!(self.state_of(name), Some(ComponentState::Running)) {
781 return Err(RegistryError::Reload {
782 name: name.to_string(),
783 message: "component is not in Running state".to_string(),
784 });
785 }
786
787 if let Err(e) = old_instance.pre_replace().await {
788 return Err(RegistryError::Reload {
789 name: name.to_string(),
790 message: format!("pre_replace hook failed: {e}"),
791 });
792 }
793
794 if let Err(e) = new_instance.start().await {
795 return Err(RegistryError::Reload {
796 name: name.to_string(),
797 message: format!("new instance start failed: {e}"),
798 });
799 }
800
801 let new_arc: Arc<dyn AnyComponent> = new_instance.into();
802 {
803 let mut instances = self
804 .inner
805 .instances
806 .write()
807 .map_err(|_| RegistryError::LockPoisoned)?;
808 instances.insert(name.to_string(), new_arc);
809 }
810 self.set_state(name, ComponentState::Running);
811
812 if let Err(e) = old_instance.post_replace().await {
813 tracing::warn!(
814 component = name,
815 error = %e,
816 "post_replace hook failed (best-effort; continuing)"
817 );
818 }
819
820 Ok(old_instance)
821 }
822
823 #[must_use]
827 pub async fn health(&self) -> HashMap<String, HealthStatus> {
828 let order = self.recompute_topo().unwrap_or_default();
829 let mut out = HashMap::new();
830 for name in order {
831 let instance = self
832 .inner
833 .instances
834 .read()
835 .ok()
836 .and_then(|m| m.get(&name).cloned());
837 if let Some(instance) = instance {
838 let h = instance.health().await;
839 out.insert(name, h);
840 } else {
841 out.insert(name, HealthStatus::unhealthy("not initialized"));
842 }
843 }
844 out
845 }
846
847 fn set_state(&self, name: &str, state: ComponentState) {
848 if let Ok(mut states) = self.inner.states.write() {
849 states.insert(name.to_string(), state);
850 }
851 }
852
853 fn invalidate_topo(&self) {
854 if let Ok(mut topo) = self.inner.topo.write() {
855 *topo = None;
856 }
857 }
858
859 pub fn recompute_topo(&self) -> Result<Vec<String>, RegistryError> {
862 if let Ok(topo) = self.inner.topo.read()
863 && let Some(order) = topo.as_ref()
864 {
865 return Ok(order.clone());
866 }
867 let descriptors = self
868 .inner
869 .descriptors
870 .read()
871 .map_err(|_| RegistryError::LockPoisoned)?;
872 if descriptors.is_empty() {
873 if let Ok(mut cached) = self.inner.topo.write() {
874 *cached = Some(Vec::new());
875 }
876 return Ok(Vec::new());
877 }
878 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
879 let mut in_degree: HashMap<String, usize> = HashMap::new();
880 for (name, desc) in descriptors.iter() {
881 graph.entry(name.clone()).or_default();
882 in_degree.entry(name.clone()).or_insert(0);
883 for dep in &desc.depends_on {
884 if !descriptors.contains_key(dep) {
885 return Err(RegistryError::MissingDependency {
886 name: name.clone(),
887 dep: dep.clone(),
888 });
889 }
890 graph.entry(dep.clone()).or_default().push(name.clone());
891 *in_degree.entry(name.clone()).or_insert(0) += 1;
892 }
893 }
894
895 let mut queue: VecDeque<String> = {
897 #[allow(clippy::filter_map_bool_then)]
898 in_degree
899 .iter()
900 .filter_map(|(n, d)| (*d == 0).then(|| n.clone()))
901 .collect()
902 };
903 let mut initial: Vec<String> = queue.drain(..).collect();
904 initial.sort_unstable();
905 for n in initial {
906 queue.push_back(n);
907 }
908
909 let mut order: Vec<String> = Vec::with_capacity(in_degree.len());
910 while let Some(n) = queue.pop_front() {
911 order.push(n.clone());
912 if let Some(deps) = graph.get(&n) {
913 for m in deps {
914 if let Some(d) = in_degree.get_mut(m) {
915 *d -= 1;
916 if *d == 0 {
917 queue.push_back(m.clone());
918 }
919 }
920 }
921 }
922 }
923
924 if order.len() != in_degree.len() {
925 let leftover: Vec<String> = {
926 #[allow(clippy::filter_map_bool_then)]
927 in_degree
928 .iter()
929 .filter_map(|(n, d)| (*d > 0).then(|| n.clone()))
930 .collect()
931 };
932 return Err(RegistryError::Cycle { cycle: leftover });
933 }
934
935 if let Ok(mut cached) = self.inner.topo.write() {
936 *cached = Some(order.clone());
937 }
938 Ok(order)
939 }
940}
941
942#[cfg(test)]
943mod tests {
944 use super::*;
945 use async_trait::async_trait;
946 use schemars::JsonSchema;
947 use serde::{Deserialize, Serialize};
948
949 #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
950 struct EchoConfig {
951 label: String,
952 }
953
954 struct EchoComponent {
955 label: String,
956 }
957
958 #[async_trait]
959 impl Component for EchoComponent {
960 const NAME: &'static str = "test.echo";
961 type Config = EchoConfig;
962 type Error = std::io::Error;
963
964 async fn init(cfg: &Self::Config, _ctx: &ComponentContext) -> Result<Self, Self::Error> {
965 Ok(Self {
966 label: cfg.label.clone(),
967 })
968 }
969 }
970
971 fn cfg(label: &str) -> serde_json::Value {
972 serde_json::json!({ "label": label })
973 }
974
975 #[tokio::test]
976 async fn register_init_start_stop_lifecycle() {
977 let reg = ComponentRegistry::new();
978 reg.register_typed::<EchoComponent>("a", cfg("a-label"))
979 .unwrap_or_else(|e| panic!("{e}"));
980 reg.register_typed::<EchoComponent>("b", cfg("b-label"))
981 .unwrap_or_else(|e| panic!("{e}"));
982
983 reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
984 assert!(reg.is_initialized("a"));
985 assert!(reg.is_initialized("b"));
986
987 reg.start_all().await.unwrap_or_else(|e| panic!("{e}"));
988 assert_eq!(reg.state_of("a"), Some(ComponentState::Running));
989
990 reg.stop_all().await.unwrap_or_else(|e| panic!("{e}"));
991 assert_eq!(reg.state_of("a"), Some(ComponentState::Stopped));
992 }
993
994 #[tokio::test]
995 async fn topo_respects_dependencies() {
996 struct Dep;
997 #[async_trait]
998 impl Component for Dep {
999 const NAME: &'static str = "test.dep";
1000 type Config = serde_json::Value;
1001 type Error = std::io::Error;
1002 async fn init(
1003 _cfg: &Self::Config,
1004 _ctx: &ComponentContext,
1005 ) -> Result<Self, Self::Error> {
1006 Ok(Dep)
1007 }
1008 }
1009 struct User;
1010 #[async_trait]
1011 impl Component for User {
1012 const NAME: &'static str = "test.user";
1013 type Config = serde_json::Value;
1014 type Error = std::io::Error;
1015 async fn init(
1016 _cfg: &Self::Config,
1017 _ctx: &ComponentContext,
1018 ) -> Result<Self, Self::Error> {
1019 Ok(User)
1020 }
1021 }
1022 let reg = ComponentRegistry::new();
1023 reg.register_typed::<User>("user", serde_json::json!({}))
1027 .unwrap_or_else(|e| panic!("{e}"));
1028 reg.register_typed::<Dep>("dep", serde_json::json!({}))
1029 .unwrap_or_else(|e| panic!("{e}"));
1030 reg.unregister("user").unwrap_or_else(|e| panic!("{e}"));
1033 reg.register_factory(
1034 ComponentDescriptor {
1035 name: "user".into(),
1036 depends_on: vec!["dep".into()],
1037 config: serde_json::json!({}),
1038 },
1039 Box::new(TypedFactory::<User>::new("user", vec!["dep".into()])),
1040 )
1041 .unwrap_or_else(|e| panic!("{e}"));
1042 let order = reg.recompute_topo().unwrap_or_else(|e| panic!("{e}"));
1043 assert_eq!(order, vec!["dep".to_string(), "user".to_string()]);
1044 }
1045
1046 #[tokio::test]
1047 async fn duplicate_registration_rejected() {
1048 let reg = ComponentRegistry::new();
1049 reg.register_typed::<EchoComponent>("a", cfg("a"))
1050 .unwrap_or_else(|e| panic!("{e}"));
1051 let err = match reg.register_typed::<EchoComponent>("a", cfg("b")) {
1052 Ok(_) => panic!("expected Err, got Ok"),
1053 Err(e) => e,
1054 };
1055 assert!(matches!(err, RegistryError::AlreadyRegistered { .. }));
1056 }
1057
1058 #[tokio::test]
1059 async fn missing_dependency_detected() {
1060 let reg = ComponentRegistry::new();
1061 let factory = TypedFactory::<EchoComponent>::new("a", vec!["missing".to_string()]);
1062 reg.register_factory(
1063 ComponentDescriptor {
1064 name: "a".into(),
1065 depends_on: vec!["missing".into()],
1066 config: cfg("a"),
1067 },
1068 Box::new(factory),
1069 )
1070 .unwrap_or_else(|e| panic!("{e}"));
1071 let err = match reg.init_all().await {
1072 Ok(_) => panic!("expected Err, got Ok"),
1073 Err(e) => e,
1074 };
1075 assert!(matches!(err, RegistryError::MissingDependency { .. }));
1076 }
1077
1078 #[tokio::test]
1079 async fn cycle_detected() {
1080 let reg = ComponentRegistry::new();
1081 let factory_a = TypedFactory::<EchoComponent>::new("a", vec!["b".to_string()]);
1082 let factory_b = TypedFactory::<EchoComponent>::new("b", vec!["a".to_string()]);
1083 reg.register_factory(
1084 ComponentDescriptor {
1085 name: "a".into(),
1086 depends_on: vec!["b".into()],
1087 config: cfg("a"),
1088 },
1089 Box::new(factory_a),
1090 )
1091 .unwrap_or_else(|e| panic!("{e}"));
1092 reg.register_factory(
1093 ComponentDescriptor {
1094 name: "b".into(),
1095 depends_on: vec!["a".into()],
1096 config: cfg("b"),
1097 },
1098 Box::new(factory_b),
1099 )
1100 .unwrap_or_else(|e| panic!("{e}"));
1101 let err = match reg.init_all().await {
1102 Ok(_) => panic!("expected Err, got Ok"),
1103 Err(e) => e,
1104 };
1105 assert!(matches!(err, RegistryError::Cycle { .. }));
1106 }
1107
1108 #[tokio::test]
1109 async fn health_aggregates_initialized_components() {
1110 let reg = ComponentRegistry::new();
1111 reg.register_typed::<EchoComponent>("a", cfg("a"))
1112 .unwrap_or_else(|e| panic!("{e}"));
1113 reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
1114 reg.start_all().await.unwrap_or_else(|e| panic!("{e}"));
1115 let h = reg.health().await;
1116 assert!(h.get("a").map(|s| s.is_healthy()).unwrap_or(false));
1117 }
1118
1119 #[tokio::test]
1120 async fn get_downcasts_to_concrete_type() {
1121 let reg = ComponentRegistry::new();
1122 reg.register_typed::<EchoComponent>("a", cfg("hello"))
1123 .unwrap_or_else(|e| panic!("{e}"));
1124 reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
1125 let c: Arc<EchoComponent> = reg.get("a").unwrap_or_else(|e| panic!("{e}"));
1126 assert_eq!(c.label, "hello");
1127 }
1128
1129 #[tokio::test]
1130 async fn get_returns_type_mismatch_on_wrong_type() {
1131 #[derive(Debug)]
1132 struct Other;
1133 #[async_trait]
1134 impl Component for Other {
1135 const NAME: &'static str = "test.other";
1136 type Config = serde_json::Value;
1137 type Error = std::io::Error;
1138 async fn init(
1139 _cfg: &Self::Config,
1140 _ctx: &ComponentContext,
1141 ) -> Result<Self, Self::Error> {
1142 Ok(Other)
1143 }
1144 }
1145 let reg = ComponentRegistry::new();
1146 reg.register_typed::<EchoComponent>("a", cfg("a"))
1147 .unwrap_or_else(|e| panic!("{e}"));
1148 reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
1149 let err = match reg.get::<Other>("a") {
1150 Ok(_) => panic!("expected Err, got Ok"),
1151 Err(e) => e,
1152 };
1153 assert!(matches!(err, RegistryError::TypeMismatch { .. }));
1154 }
1155
1156 #[tokio::test]
1157 async fn unregister_removes_state() {
1158 let reg = ComponentRegistry::new();
1159 reg.register_typed::<EchoComponent>("a", cfg("a"))
1160 .unwrap_or_else(|e| panic!("{e}"));
1161 reg.unregister("a").unwrap_or_else(|e| panic!("{e}"));
1162 assert!(!reg.is_initialized("a"));
1163 assert_eq!(reg.state_of("a"), None);
1164 }
1165
1166 #[tokio::test]
1167 async fn names_returns_topo_order() {
1168 let reg = ComponentRegistry::new();
1169 reg.register_typed::<EchoComponent>("zzz", cfg("z"))
1170 .unwrap_or_else(|e| panic!("{e}"));
1171 reg.register_typed::<EchoComponent>("aaa", cfg("a"))
1172 .unwrap_or_else(|e| panic!("{e}"));
1173 let names = reg.names();
1174 assert_eq!(names.first().map(String::as_str), Some("aaa"));
1177 assert_eq!(names.last().map(String::as_str), Some("zzz"));
1178 }
1179}