zerodds_corba_dnc/
container_host.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum HostError {
25 AlreadyInstalled(String),
27 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#[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#[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#[derive(Debug, Default)]
78pub struct ContainerHost {
79 containers: BTreeMap<ContainerType, Container>,
80 instances: BTreeMap<String, ContainerType>,
81}
82
83impl ContainerHost {
84 #[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 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 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 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 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 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 #[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 #[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}