lenso_native_adapter/
facilities.rs1use std::{
3 any::{Any, TypeId},
4 collections::BTreeMap,
5 fmt,
6 rc::Rc,
7};
8
9use lenso_app_plan::ResolvedAppPlan;
10use lenso_kernel::RuntimeFailure;
11
12#[derive(Clone, Default)]
13pub struct NativeFacilities {
14 values: BTreeMap<String, FacilityValue>,
15}
16
17#[derive(Clone)]
18enum FacilityValue {
19 Value(Rc<dyn Any>),
20 Factory {
21 type_id: TypeId,
22 create: Rc<dyn Fn() -> Result<Rc<dyn Any>, RuntimeFailure>>,
23 },
24}
25
26impl fmt::Debug for NativeFacilities {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 formatter
29 .debug_struct("NativeFacilities")
30 .field("names", &self.values.keys())
31 .finish()
32 }
33}
34
35impl NativeFacilities {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn with<T: Any>(
41 mut self,
42 name: impl Into<String>,
43 value: T,
44 ) -> Result<Self, RuntimeFailure> {
45 let name = name.into();
46 if name.is_empty()
47 || !name
48 .bytes()
49 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
50 {
51 return Err(invalid("invalid Host facility name"));
52 }
53 if self
54 .values
55 .insert(name.clone(), FacilityValue::Value(Rc::new(value)))
56 .is_some()
57 {
58 return Err(invalid(format!("duplicate Host facility `{name}`")));
59 }
60 Ok(self)
61 }
62
63 pub fn with_factory<T: Any>(
65 mut self,
66 name: impl Into<String>,
67 create: impl Fn() -> Result<T, RuntimeFailure> + 'static,
68 ) -> Result<Self, RuntimeFailure> {
69 let name = name.into();
70 if name.is_empty()
71 || !name
72 .bytes()
73 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
74 {
75 return Err(invalid("invalid Host facility name"));
76 }
77 if self.values.contains_key(&name) {
78 return Err(invalid(format!("duplicate Host facility `{name}`")));
79 }
80 self.values.insert(
81 name,
82 FacilityValue::Factory {
83 type_id: TypeId::of::<T>(),
84 create: Rc::new(move || create().map(|value| Rc::new(value) as Rc<dyn Any>)),
85 },
86 );
87 Ok(self)
88 }
89
90 pub fn require<T: Any + Clone>(&self, name: &str) -> Result<T, RuntimeFailure> {
91 let missing = || invalid(format!("missing or incompatible Host facility `{name}`"));
92 let value = match self.values.get(name).ok_or_else(missing)? {
93 FacilityValue::Value(value) => value.clone(),
94 FacilityValue::Factory { type_id, create } if *type_id == TypeId::of::<T>() => {
95 create()?
96 }
97 FacilityValue::Factory { .. } => return Err(missing()),
98 };
99 value.downcast_ref::<T>().cloned().ok_or_else(missing)
100 }
101
102 pub fn optional<T: Any + Clone>(&self, name: &str) -> Result<Option<T>, RuntimeFailure> {
104 if self.values.contains_key(name) {
105 self.require(name).map(Some)
106 } else {
107 Ok(None)
108 }
109 }
110}
111
112#[derive(Clone, Debug, Default)]
113pub struct NativeInstanceFacilities {
114 values: BTreeMap<String, NativeFacilities>,
115 empty: NativeFacilities,
116}
117
118impl NativeInstanceFacilities {
119 pub fn new() -> Self {
120 Self::default()
121 }
122
123 pub fn with(
124 mut self,
125 instance: impl Into<String>,
126 facilities: NativeFacilities,
127 ) -> Result<Self, RuntimeFailure> {
128 let instance = instance.into();
129 if instance.is_empty() || self.values.insert(instance.clone(), facilities).is_some() {
130 return Err(invalid(format!(
131 "invalid or duplicate facilities for Instance `{instance}`"
132 )));
133 }
134 Ok(self)
135 }
136
137 pub(crate) fn for_instance(&self, instance: &str) -> &NativeFacilities {
138 self.values.get(instance).unwrap_or(&self.empty)
139 }
140
141 pub(crate) fn validate(&self, plan: &ResolvedAppPlan) -> Result<(), RuntimeFailure> {
142 for instance in self.values.keys() {
143 if !plan
144 .plugin_instances()
145 .iter()
146 .any(|selected| selected.instance_key() == instance)
147 {
148 return Err(invalid(format!(
149 "Host facilities name an unselected Instance `{instance}`"
150 )));
151 }
152 }
153 Ok(())
154 }
155}
156
157fn invalid(detail: impl Into<String>) -> RuntimeFailure {
158 RuntimeFailure::InvalidResolvedPlan {
159 detail: detail.into(),
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn attachments_are_typed_and_isolated_without_exposing_values() {
169 let first = NativeFacilities::new()
170 .with("state", "private-primary".to_owned())
171 .unwrap();
172 let second = NativeFacilities::new().with("state", 7_u64).unwrap();
173 let catalog = NativeInstanceFacilities::new()
174 .with("store/primary", first)
175 .unwrap()
176 .with("store/secondary", second)
177 .unwrap();
178 assert_eq!(
179 catalog
180 .for_instance("store/primary")
181 .require::<String>("state")
182 .unwrap(),
183 "private-primary"
184 );
185 assert_eq!(
186 catalog
187 .for_instance("store/secondary")
188 .require::<u64>("state")
189 .unwrap(),
190 7
191 );
192 assert!(
193 catalog
194 .for_instance("store/primary")
195 .require::<u64>("state")
196 .is_err()
197 );
198 assert!(
199 catalog
200 .for_instance("store/unselected")
201 .require::<String>("state")
202 .is_err()
203 );
204 assert!(!format!("{catalog:?}").contains("private-primary"));
205 }
206
207 #[test]
208 fn duplicate_and_unselected_attachments_fail_closed() {
209 assert!(
210 NativeFacilities::new()
211 .with("state", 1_u64)
212 .unwrap()
213 .with("state", 2_u64)
214 .is_err()
215 );
216 let catalog = NativeInstanceFacilities::new()
217 .with("store/primary", NativeFacilities::new())
218 .unwrap();
219 let empty = lenso_app_plan::AppComposition::new(vec![], vec![])
220 .resolve()
221 .unwrap();
222 assert!(catalog.validate(&empty).is_err());
223 }
224
225 #[test]
226 fn owner_factory_creates_distinct_generation_inputs_and_does_not_run_for_wrong_types() {
227 let created = Rc::new(std::cell::Cell::new(0_u64));
228 let count = created.clone();
229 let facilities = NativeFacilities::new()
230 .with_factory("state", move || {
231 count.set(count.get() + 1);
232 Ok(count.get())
233 })
234 .unwrap();
235 assert!(facilities.require::<String>("state").is_err());
236 assert_eq!(created.get(), 0);
237 assert_eq!(facilities.require::<u64>("state").unwrap(), 1);
238 assert_eq!(facilities.require::<u64>("state").unwrap(), 2);
239 }
240}