Skip to main content

zerodds_corba_dnc/
container_host.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! ContainerHost — bindet einen [`zerodds_corba_ccm::container::Container`] an
5//! einen D&C-Plan-Application-Run.
6//!
7//! Spec D&C §11 (Container Programming Model) legt fest, dass jede
8//! Component-Instance einen Container braucht. ContainerHost ist die
9//! Brücke: nimmt den Plan-Output (`NodeApplication`) entgegen und
10//! installiert die Component-Instances in einen CCM-Container.
11
12use alloc::boxed::Box;
13use alloc::collections::BTreeMap;
14use alloc::string::String;
15use alloc::vec::Vec;
16
17use zerodds_corba_ccm::cidl::CompositionCategory;
18use zerodds_corba_ccm::cif::ComponentExecutor;
19use zerodds_corba_ccm::container::{Container, ContainerType, LifecycleState};
20use zerodds_corba_ccm::context::ComponentContext;
21
22/// ContainerHost-Fehler.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum HostError {
25    /// Instance schon installiert.
26    AlreadyInstalled(String),
27    /// Container-Operation lieferte Fehler.
28    ContainerError(String),
29}
30
31impl core::fmt::Display for HostError {
32    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33        match self {
34            Self::AlreadyInstalled(s) => write!(f, "instance `{s}` already installed"),
35            Self::ContainerError(s) => write!(f, "container error: {s}"),
36        }
37    }
38}
39
40#[cfg(feature = "std")]
41impl std::error::Error for HostError {}
42
43/// Default-Executor — fuer Plan-Boot, wenn der Caller keine konkrete
44/// Executor-Implementation reicht. Implementiert `ComponentExecutor`
45/// mit Default-Verhalten.
46#[derive(Default)]
47pub struct PlanExecutor {
48    ctx: Option<Box<dyn ComponentContext>>,
49}
50
51impl core::fmt::Debug for PlanExecutor {
52    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53        f.debug_struct("PlanExecutor")
54            .field("has_context", &self.ctx.is_some())
55            .finish()
56    }
57}
58
59impl ComponentExecutor for PlanExecutor {
60    fn set_context(&mut self, context: Box<dyn ComponentContext>) {
61        self.ctx = Some(context);
62    }
63}
64
65/// Default-Context — anonyme Identity.
66#[derive(Debug, Default, Clone, Copy)]
67pub struct AnonContext;
68
69impl ComponentContext for AnonContext {
70    fn get_caller_principal(&self) -> Option<Vec<u8>> {
71        None
72    }
73}
74
75/// ContainerHost — pro Node ein Host, der je `CompositionCategory`
76/// einen Container haelt.
77#[derive(Debug, Default)]
78pub struct ContainerHost {
79    containers: BTreeMap<ContainerType, Container>,
80    instances: BTreeMap<String, ContainerType>,
81}
82
83impl ContainerHost {
84    /// Konstruktor.
85    #[must_use]
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    fn category_to_type(c: CompositionCategory) -> ContainerType {
91        match c {
92            CompositionCategory::Session => ContainerType::Session,
93            CompositionCategory::Service => ContainerType::Service,
94            CompositionCategory::Process => ContainerType::Process,
95            CompositionCategory::Entity => ContainerType::Entity,
96        }
97    }
98
99    fn ensure_container(&mut self, kind: ContainerType) -> &Container {
100        self.containers
101            .entry(kind)
102            .or_insert_with(|| Container::new(kind))
103    }
104
105    /// Installiert eine Component-Instance mit Default-Executor +
106    /// Anon-Context. Caller, der einen konkreten Executor hat, kann
107    /// `install_with` benutzen.
108    ///
109    /// # Errors
110    /// `HostError::AlreadyInstalled` wenn die Instance schon existiert.
111    pub fn install(
112        &mut self,
113        instance_name: &str,
114        category: CompositionCategory,
115    ) -> Result<(), HostError> {
116        self.install_with(
117            instance_name,
118            category,
119            Box::<PlanExecutor>::default(),
120            Box::new(AnonContext),
121        )
122    }
123
124    /// Installiert mit konkretem Executor + Context.
125    ///
126    /// # Errors
127    /// `HostError::AlreadyInstalled` wenn die Instance schon existiert.
128    pub fn install_with(
129        &mut self,
130        instance_name: &str,
131        category: CompositionCategory,
132        executor: Box<dyn ComponentExecutor>,
133        context: Box<dyn ComponentContext>,
134    ) -> Result<(), HostError> {
135        if self.instances.contains_key(instance_name) {
136            return Err(HostError::AlreadyInstalled(instance_name.into()));
137        }
138        let kind = Self::category_to_type(category);
139        let c = self.ensure_container(kind);
140        c.install_component(instance_name.into(), executor, context)
141            .map_err(|e| HostError::ContainerError(format_cif(&e)))?;
142        self.instances.insert(instance_name.into(), kind);
143        Ok(())
144    }
145
146    /// Aktiviert eine Instance — `ccm_activate`.
147    ///
148    /// # Errors
149    /// Wenn die Instance unbekannt oder die Container-Transition
150    /// fehlschlaegt.
151    pub fn activate(&self, instance_name: &str) -> Result<(), HostError> {
152        let kind = *self.instances.get(instance_name).ok_or_else(|| {
153            HostError::ContainerError(alloc::format!("unknown instance `{instance_name}`"))
154        })?;
155        let c = self
156            .containers
157            .get(&kind)
158            .ok_or_else(|| HostError::ContainerError("container vanished".into()))?;
159        c.activate(instance_name)
160            .map_err(|e| HostError::ContainerError(format_cif(&e)))
161    }
162
163    /// Passiviert eine Instance — `ccm_passivate`.
164    ///
165    /// # Errors
166    /// Wenn die Instance unbekannt oder die Container-Transition
167    /// fehlschlaegt.
168    pub fn passivate(&self, instance_name: &str) -> Result<(), HostError> {
169        let kind = *self.instances.get(instance_name).ok_or_else(|| {
170            HostError::ContainerError(alloc::format!("unknown instance `{instance_name}`"))
171        })?;
172        let c = self
173            .containers
174            .get(&kind)
175            .ok_or_else(|| HostError::ContainerError("container vanished".into()))?;
176        c.passivate(instance_name)
177            .map_err(|e| HostError::ContainerError(format_cif(&e)))
178    }
179
180    /// Tear-down einer Instance — `ccm_remove`.
181    ///
182    /// # Errors
183    /// Wenn die Instance unbekannt oder die Container-Transition
184    /// fehlschlaegt.
185    pub fn remove(&mut self, instance_name: &str) -> Result<(), HostError> {
186        let kind = self.instances.remove(instance_name).ok_or_else(|| {
187            HostError::ContainerError(alloc::format!("unknown instance `{instance_name}`"))
188        })?;
189        let c = self
190            .containers
191            .get(&kind)
192            .ok_or_else(|| HostError::ContainerError("container vanished".into()))?;
193        c.remove(instance_name)
194            .map_err(|e| HostError::ContainerError(format_cif(&e)))
195    }
196
197    /// Aktueller Lifecycle-State einer Instance.
198    #[must_use]
199    pub fn state(&self, instance_name: &str) -> Option<LifecycleState> {
200        let kind = self.instances.get(instance_name)?;
201        self.containers.get(kind)?.state_of(instance_name)
202    }
203
204    /// Liste aller Instance-Namen.
205    #[must_use]
206    pub fn instances(&self) -> Vec<String> {
207        self.instances.keys().cloned().collect()
208    }
209}
210
211fn format_cif<E: core::fmt::Debug>(e: &E) -> String {
212    alloc::format!("{e:?}")
213}
214
215#[cfg(test)]
216#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
217mod tests {
218    use super::*;
219    use alloc::string::ToString;
220
221    #[test]
222    fn install_creates_container_on_demand() {
223        let mut host = ContainerHost::new();
224        host.install("e1", CompositionCategory::Session).unwrap();
225        assert!(host.instances().contains(&"e1".to_string()));
226        assert_eq!(host.state("e1"), Some(LifecycleState::Configured));
227    }
228
229    #[test]
230    fn install_then_activate_reaches_active() {
231        let mut host = ContainerHost::new();
232        host.install("e1", CompositionCategory::Session).unwrap();
233        host.activate("e1").unwrap();
234        assert_eq!(host.state("e1"), Some(LifecycleState::Active));
235    }
236
237    #[test]
238    fn full_lifecycle_round_trip() {
239        let mut host = ContainerHost::new();
240        host.install("e1", CompositionCategory::Session).unwrap();
241        host.activate("e1").unwrap();
242        host.passivate("e1").unwrap();
243        assert_eq!(host.state("e1"), Some(LifecycleState::Passive));
244        host.remove("e1").unwrap();
245        assert!(host.state("e1").is_none());
246    }
247
248    #[test]
249    fn duplicate_install_rejected() {
250        let mut host = ContainerHost::new();
251        host.install("e1", CompositionCategory::Session).unwrap();
252        let err = host
253            .install("e1", CompositionCategory::Session)
254            .unwrap_err();
255        assert!(matches!(err, HostError::AlreadyInstalled(_)));
256    }
257
258    #[test]
259    fn remove_unknown_fails_cleanly() {
260        let mut host = ContainerHost::new();
261        assert!(host.remove("nope").is_err());
262    }
263
264    #[test]
265    fn entity_and_session_share_host() {
266        let mut host = ContainerHost::new();
267        host.install("a", CompositionCategory::Session).unwrap();
268        host.install("b", CompositionCategory::Entity).unwrap();
269        assert_eq!(host.instances().len(), 2);
270    }
271}