Skip to main content

lenso_native_adapter/
lib.rs

1//! Native Rust Execution Adapter for statically linked Module packages.
2
3use std::{collections::BTreeMap, rc::Rc};
4
5use lenso_app_plan::{ExecutionClassId, ResolvedAppPlan};
6use lenso_kernel::{
7    ModuleLifecycle, NativeEndpointSet, NativeEventEndpoint, NativeExecutionAdapter,
8    NativeRequestEndpoint, NativeStreamEndpoint, NoopModuleLifecycle, PreparedBinding,
9    PreparedEventBinding, PreparedNativeApp, PreparedNativeModule, PreparedStreamBinding,
10    RuntimeFailure,
11};
12
13/// Endpoints created for one statically linked Module Instance generation.
14#[derive(Debug)]
15pub struct NativeModuleInstance {
16    endpoints: NativeEndpointSet,
17    lifecycle: Rc<dyn ModuleLifecycle>,
18}
19
20impl NativeModuleInstance {
21    /// Creates a generation from its exact declared endpoint set.
22    pub fn new(endpoints: Vec<Rc<dyn NativeRequestEndpoint>>) -> Self {
23        Self::with_lifecycle(endpoints, NoopModuleLifecycle)
24    }
25
26    /// Creates a generation with its exact endpoints and lifecycle Interface.
27    pub fn with_lifecycle(
28        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
29        lifecycle: impl ModuleLifecycle,
30    ) -> Self {
31        Self {
32            endpoints: NativeEndpointSet::new(endpoints, Vec::new(), Vec::new()),
33            lifecycle: Rc::new(lifecycle),
34        }
35    }
36
37    /// Creates a generation with request and bidirectional stream endpoints.
38    pub fn with_endpoints(
39        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
40        stream_endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
41        lifecycle: impl ModuleLifecycle,
42    ) -> Self {
43        Self {
44            endpoints: NativeEndpointSet::new(endpoints, stream_endpoints, Vec::new()),
45            lifecycle: Rc::new(lifecycle),
46        }
47    }
48
49    /// Creates a generation containing only bidirectional stream endpoints.
50    pub fn with_stream_endpoints(
51        stream_endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
52        lifecycle: impl ModuleLifecycle,
53    ) -> Self {
54        Self::with_endpoints(Vec::new(), stream_endpoints, lifecycle)
55    }
56
57    /// Creates a generation containing only ephemeral Event endpoints.
58    pub fn with_event_endpoints(
59        event_endpoints: Vec<Rc<dyn NativeEventEndpoint>>,
60        lifecycle: impl ModuleLifecycle,
61    ) -> Self {
62        Self {
63            endpoints: NativeEndpointSet::new(Vec::new(), Vec::new(), event_endpoints),
64            lifecycle: Rc::new(lifecycle),
65        }
66    }
67
68    /// Creates a generation with request, stream, and ephemeral Event endpoints.
69    pub fn with_all_endpoints(
70        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
71        stream_endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
72        event_endpoints: Vec<Rc<dyn NativeEventEndpoint>>,
73        lifecycle: impl ModuleLifecycle,
74    ) -> Self {
75        Self {
76            endpoints: NativeEndpointSet::new(endpoints, stream_endpoints, event_endpoints),
77            lifecycle: Rc::new(lifecycle),
78        }
79    }
80
81    /// Returns the lifecycle Interface for this generation.
82    pub fn lifecycle(&self) -> Rc<dyn ModuleLifecycle> {
83        self.lifecycle.clone()
84    }
85
86    /// Returns the exact endpoint set created for this generation.
87    pub fn endpoints(&self) -> &[Rc<dyn NativeRequestEndpoint>] {
88        self.endpoints.request()
89    }
90
91    /// Returns the exact bidirectional stream endpoint set created for this generation.
92    pub fn stream_endpoints(&self) -> &[Rc<dyn NativeStreamEndpoint>] {
93        self.endpoints.stream()
94    }
95
96    /// Returns the exact ephemeral Event endpoint set created for this Instance.
97    pub fn event_endpoints(&self) -> &[Rc<dyn NativeEventEndpoint>] {
98        self.endpoints.event()
99    }
100}
101
102impl Default for NativeModuleInstance {
103    fn default() -> Self {
104        Self::new(Vec::new())
105    }
106}
107
108/// Adapter-specific factory for a statically linked native Rust Module.
109pub trait NativeModuleFactory: std::fmt::Debug + 'static {
110    /// Package identity selected by the Resolved App Plan.
111    fn package_id(&self) -> &'static str;
112    /// Exact statically linked Cargo package version.
113    fn package_version(&self) -> &'static str {
114        ""
115    }
116    /// Creates a fresh Module Instance generation.
117    fn instantiate(
118        &self,
119        context: NativeModuleFactoryContext<'_>,
120    ) -> Result<NativeModuleInstance, RuntimeFailure>;
121}
122
123/// Immutable Plan input supplied when a native factory creates one generation.
124#[derive(Clone, Copy, Debug)]
125pub struct NativeModuleFactoryContext<'a> {
126    instance_key: &'a str,
127    entrypoint: &'a str,
128    configuration: &'a str,
129}
130
131impl<'a> NativeModuleFactoryContext<'a> {
132    fn from_plan(instance: &'a lenso_app_plan::ModuleInstancePlan) -> Self {
133        Self {
134            instance_key: instance.instance_key(),
135            entrypoint: instance.entrypoint(),
136            configuration: instance.configuration(),
137        }
138    }
139
140    /// Returns the App-local Module Instance key.
141    pub const fn instance_key(self) -> &'a str {
142        self.instance_key
143    }
144
145    /// Returns the exact package entrypoint selected before boot.
146    pub const fn entrypoint(self) -> &'a str {
147        self.entrypoint
148    }
149
150    /// Returns opaque Module-owned configuration selected before boot.
151    pub const fn configuration(self) -> &'a str {
152        self.configuration
153    }
154}
155
156/// Statically linked native Module factories available to an App binary.
157#[derive(Debug, Default)]
158pub struct NativeModuleRegistry {
159    factories: Vec<Rc<dyn NativeModuleFactory>>,
160}
161
162type NativeInstances = BTreeMap<String, NativeModuleInstance>;
163type PreparedGenerations = BTreeMap<String, PreparedNativeModule>;
164type NativeBindings = (
165    Vec<PreparedBinding>,
166    Vec<PreparedStreamBinding>,
167    Vec<PreparedEventBinding>,
168);
169
170fn factory_matches(
171    factory: &dyn NativeModuleFactory,
172    expected: &lenso_app_plan::ModuleInstancePlan,
173) -> bool {
174    factory.package_id() == expected.package_id()
175        && (expected.package_revision().is_empty()
176            || factory.package_version() == expected.package_revision())
177}
178
179impl NativeModuleRegistry {
180    /// Creates an empty linked-factory registry.
181    pub fn new() -> Self {
182        Self::default()
183    }
184    /// Adds one statically linked factory.
185    #[must_use]
186    pub fn with_factory(mut self, factory: impl NativeModuleFactory) -> Self {
187        self.factories.push(Rc::new(factory));
188        self
189    }
190
191    fn prepare_instances(
192        &self,
193        plan: &ResolvedAppPlan,
194    ) -> Result<(NativeInstances, PreparedGenerations), RuntimeFailure> {
195        let mut instances = BTreeMap::new();
196        let mut generations = BTreeMap::new();
197        for expected in plan
198            .module_instances()
199            .iter()
200            .filter(|instance| instance.execution_class() == &ExecutionClassId::native_rust())
201        {
202            let matching_factories: Vec<_> = self
203                .factories
204                .iter()
205                .filter(|factory| factory_matches(factory.as_ref(), expected))
206                .collect();
207            let factory = match matching_factories.as_slice() {
208                [] => {
209                    return Err(RuntimeFailure::MissingModuleFactory {
210                        instance: expected.instance_key().to_owned(),
211                        package_id: expected.package_id().to_owned(),
212                    });
213                }
214                [factory] => *factory,
215                _ => {
216                    return invalid(format!(
217                        "multiple statically linked factories declare package `{}`",
218                        expected.package_id()
219                    ));
220                }
221            };
222            let generation =
223                factory.instantiate(NativeModuleFactoryContext::from_plan(expected))?;
224            generations.insert(
225                expected.instance_key().to_owned(),
226                PreparedNativeModule::with_endpoint_set_lifecycle(
227                    generation.endpoints.clone(),
228                    generation.lifecycle(),
229                ),
230            );
231            if instances
232                .insert(expected.instance_key().to_owned(), generation)
233                .is_some()
234            {
235                return invalid(format!(
236                    "duplicate Module Instance `{}`",
237                    expected.instance_key()
238                ));
239            }
240        }
241        Ok((instances, generations))
242    }
243}
244
245impl NativeExecutionAdapter for NativeModuleRegistry {
246    fn prepare(&self, plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
247        plan.validate()
248            .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
249                detail: error.to_string(),
250            })?;
251
252        let (instances, generations) = self.prepare_instances(plan)?;
253        let (bindings, stream_bindings, event_bindings) = prepare_bindings(plan, &instances)?;
254        Ok(PreparedNativeApp::new(bindings, generations)
255            .with_stream_bindings(stream_bindings)
256            .with_event_bindings(event_bindings))
257    }
258
259    fn recreate(
260        &self,
261        plan: &ResolvedAppPlan,
262        instance_key: &str,
263    ) -> Result<PreparedNativeModule, RuntimeFailure> {
264        let expected = plan
265            .module_instances()
266            .iter()
267            .find(|instance| instance.instance_key() == instance_key)
268            .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
269                detail: format!("unknown Module Instance `{instance_key}`"),
270            })?;
271        let matching_factories: Vec<_> = self
272            .factories
273            .iter()
274            .filter(|factory| factory_matches(factory.as_ref(), expected))
275            .collect();
276        let factory = match matching_factories.as_slice() {
277            [] => {
278                return Err(RuntimeFailure::MissingModuleFactory {
279                    instance: expected.instance_key().to_owned(),
280                    package_id: expected.package_id().to_owned(),
281                });
282            }
283            [factory] => *factory,
284            _ => {
285                return invalid(format!(
286                    "multiple statically linked factories declare package `{}`",
287                    expected.package_id()
288                ));
289            }
290        };
291        let generation = factory.instantiate(NativeModuleFactoryContext::from_plan(expected))?;
292        Ok(PreparedNativeModule::with_endpoint_set_lifecycle(
293            generation.endpoints.clone(),
294            generation.lifecycle(),
295        ))
296    }
297}
298
299fn prepare_bindings(
300    plan: &ResolvedAppPlan,
301    instances: &NativeInstances,
302) -> Result<NativeBindings, RuntimeFailure> {
303    let mut bindings = Vec::new();
304    let mut stream_bindings = Vec::new();
305    let mut event_bindings = Vec::new();
306    for binding in plan.capability_bindings() {
307        if !instances.contains_key(binding.provider_instance()) {
308            continue;
309        }
310        let provider = plan
311            .module_instance(binding.provider_instance())
312            .expect("validated binding provider should exist");
313        let descriptor = provider
314            .provided_capabilities()
315            .iter()
316            .find(|descriptor| descriptor.capability_id() == binding.capability_id())
317            .expect("validated binding descriptor should exist");
318        if !descriptor.request_operations().is_empty() {
319            let endpoint = instances
320                .get(binding.provider_instance())
321                .and_then(|instance| {
322                    instance.endpoints.request().iter().find(|endpoint| {
323                        endpoint.capability_id() == binding.capability_id()
324                            && endpoint.descriptor_version() == binding.descriptor_version()
325                    })
326                })
327                .cloned()
328                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
329                    detail: format!(
330                        "Capability `{}` version `{}` has no request endpoint on provider `{}`",
331                        binding.capability_id(),
332                        binding.descriptor_version(),
333                        binding.provider_instance()
334                    ),
335                })?;
336            bindings.push(PreparedBinding::new(
337                binding.consumer_instance(),
338                binding.provider_instance(),
339                endpoint,
340            ));
341        }
342        if !descriptor.stream_operations().is_empty() {
343            let endpoint = instances
344                .get(binding.provider_instance())
345                .and_then(|instance| {
346                    instance.endpoints.stream().iter().find(|endpoint| {
347                        endpoint.capability_id() == binding.capability_id()
348                            && endpoint.descriptor_version() == binding.descriptor_version()
349                    })
350                })
351                .cloned()
352                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
353                    detail: format!(
354                        "Capability `{}` version `{}` has no stream endpoint on provider `{}`",
355                        binding.capability_id(),
356                        binding.descriptor_version(),
357                        binding.provider_instance()
358                    ),
359                })?;
360            stream_bindings.push(PreparedStreamBinding::new(
361                binding.consumer_instance(),
362                binding.provider_instance(),
363                endpoint,
364            ));
365        }
366        if !descriptor.event_operations().is_empty() {
367            let endpoint = instances
368                .get(binding.provider_instance())
369                .and_then(|instance| {
370                    instance.endpoints.event().iter().find(|endpoint| {
371                        endpoint.capability_id() == binding.capability_id()
372                            && endpoint.descriptor_version() == binding.descriptor_version()
373                    })
374                })
375                .cloned()
376                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
377                    detail: format!(
378                        "Capability `{}` version `{}` has no Event endpoint on provider `{}`",
379                        binding.capability_id(),
380                        binding.descriptor_version(),
381                        binding.provider_instance()
382                    ),
383                })?;
384            event_bindings.push(PreparedEventBinding::new(
385                binding.consumer_instance(),
386                binding.provider_instance(),
387                endpoint,
388            ));
389        }
390    }
391    Ok((bindings, stream_bindings, event_bindings))
392}
393
394fn invalid<T>(detail: String) -> Result<T, RuntimeFailure> {
395    Err(RuntimeFailure::InvalidResolvedPlan { detail })
396}