Skip to main content

lenso_native_adapter/
facilities.rs

1//! Immutable Host attachments supplied to one selected Plugin instance.
2use 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    /// Registers an owner factory called when a generation constructs its typed input.
64    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    /// Returns explicit absence when no attachment exists; present inputs stay type checked.
103    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}