lenso_runtime_conformance/
lib.rs1use std::{collections::BTreeMap, fmt, rc::Rc};
8
9use futures::future::LocalBoxFuture;
10use lenso_app_plan::{ExecutionClassId, ModuleInstancePlan, ResolvedAppPlan};
11use lenso_kernel::{
12 ActivateContext, InvocationContext, ModuleLifecycle, NativeRequestEndpoint,
13 NativeRequestHandle, NoopModuleLifecycle, PreparedBinding, PreparedNativeApp,
14 PreparedNativeModule, RequestCapability, RuntimeFailure,
15};
16
17pub const PROBE_CAPABILITY_ID: &str = "lenso.runtime.conformance.probe@1";
19pub const PROBE_DESCRIPTOR_VERSION: &str = "1.0.0";
21pub const PROBE_OPERATION: &str = "probe";
23
24pub const PROBE_PROVIDER_PACKAGE_ID: &str = "lenso.runtime.conformance.probe-provider";
26pub const ALTERNATE_PROBE_PROVIDER_PACKAGE_ID: &str =
28 "lenso.runtime.conformance.alternate-probe-provider";
29pub const PROBE_CONSUMER_PACKAGE_ID: &str = "lenso.runtime.conformance.probe-consumer";
31
32#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct ProbeRequest {
35 pub value: String,
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct ProbeResponse {
41 pub value: String,
42}
43
44#[derive(Clone, Debug, Eq, PartialEq)]
46pub enum ProbeError {
47 EmptyValue,
48}
49
50#[derive(Debug)]
52pub struct Probe;
53
54impl RequestCapability for Probe {
55 type Request = ProbeRequest;
56 type Response = ProbeResponse;
57 type DomainError = ProbeError;
58
59 const ID: &'static str = PROBE_CAPABILITY_ID;
60 const DESCRIPTOR_VERSION: &'static str = PROBE_DESCRIPTOR_VERSION;
61}
62
63pub trait ProbeProvider: fmt::Debug + 'static {
65 fn probe(
66 &self,
67 context: InvocationContext,
68 request: ProbeRequest,
69 ) -> LocalBoxFuture<'static, Result<ProbeResponse, ProbeInvocationError>>;
70}
71
72#[derive(Debug)]
74pub struct ProbeEndpoint<P> {
75 provider: Rc<P>,
76}
77
78impl<P: ProbeProvider> ProbeEndpoint<P> {
79 pub fn new(provider: P) -> Self {
80 Self {
81 provider: Rc::new(provider),
82 }
83 }
84}
85
86impl<P: ProbeProvider> NativeRequestEndpoint for ProbeEndpoint<P> {
87 fn capability_id(&self) -> &'static str {
88 PROBE_CAPABILITY_ID
89 }
90
91 fn descriptor_version(&self) -> &'static str {
92 PROBE_DESCRIPTOR_VERSION
93 }
94
95 fn operations(&self) -> &'static [&'static str] {
96 &[PROBE_OPERATION]
97 }
98
99 fn invoke(
100 &self,
101 operation: &str,
102 request: Box<dyn std::any::Any>,
103 context: InvocationContext,
104 ) -> LocalBoxFuture<
105 'static,
106 Result<Result<Box<dyn std::any::Any>, Box<dyn std::any::Any>>, RuntimeFailure>,
107 > {
108 if operation != PROBE_OPERATION {
109 return Box::pin(futures::future::ready(Err(
110 RuntimeFailure::UnknownOperation {
111 capability: PROBE_CAPABILITY_ID,
112 operation: operation.to_owned(),
113 },
114 )));
115 }
116 let Ok(request) = request.downcast::<ProbeRequest>() else {
117 return Box::pin(futures::future::ready(Err(
118 RuntimeFailure::ProtocolViolation {
119 capability: PROBE_CAPABILITY_ID,
120 },
121 )));
122 };
123 let provider = Rc::clone(&self.provider);
124 Box::pin(async move {
125 match provider.probe(context, *request).await {
126 Ok(value) => Ok(Ok(Box::new(value) as Box<dyn std::any::Any>)),
127 Err(ProbeInvocationError::Domain(error)) => {
128 Ok(Err(Box::new(error) as Box<dyn std::any::Any>))
129 }
130 Err(ProbeInvocationError::Runtime(error)) => Err(error),
131 }
132 })
133 }
134}
135
136#[derive(Debug)]
138pub struct ProbeClient {
139 handle: NativeRequestHandle<Probe>,
140}
141
142impl ProbeClient {
143 pub fn new(handle: NativeRequestHandle<Probe>) -> Self {
144 Self { handle }
145 }
146
147 pub fn from_dependencies(
148 dependencies: &lenso_kernel::ModuleDependencies,
149 ) -> Result<Self, RuntimeFailure> {
150 Ok(Self::new(dependencies.one::<Probe>()?))
151 }
152
153 pub async fn probe(
154 &self,
155 request: ProbeRequest,
156 ) -> Result<ProbeResponse, ProbeInvocationError> {
157 self.handle
158 .invoke(PROBE_OPERATION, request)
159 .await
160 .map_err(ProbeInvocationError::Runtime)?
161 .map_err(ProbeInvocationError::Domain)
162 }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
167pub enum ProbeInvocationError {
168 Domain(ProbeError),
169 Runtime(RuntimeFailure),
170}
171
172#[derive(Debug)]
174pub struct ConformanceModule {
175 endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
176 lifecycle: Rc<dyn ModuleLifecycle>,
177}
178
179impl ConformanceModule {
180 pub fn new(endpoints: Vec<Rc<dyn NativeRequestEndpoint>>) -> Self {
181 Self::with_lifecycle(endpoints, NoopModuleLifecycle)
182 }
183
184 pub fn with_lifecycle(
185 endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
186 lifecycle: impl ModuleLifecycle,
187 ) -> Self {
188 Self {
189 endpoints,
190 lifecycle: Rc::new(lifecycle),
191 }
192 }
193
194 fn prepared(&self) -> PreparedNativeModule {
195 PreparedNativeModule::with_lifecycle(self.endpoints.clone(), self.lifecycle.clone())
196 }
197}
198
199impl Default for ConformanceModule {
200 fn default() -> Self {
201 Self::new(Vec::new())
202 }
203}
204
205pub trait ConformanceModuleFactory: fmt::Debug + 'static {
207 fn package_id(&self) -> &'static str;
208
209 fn package_version(&self) -> &'static str {
210 ""
211 }
212
213 fn instantiate(
214 &self,
215 instance: &ModuleInstancePlan,
216 ) -> Result<ConformanceModule, RuntimeFailure>;
217}
218
219#[derive(Debug, Default)]
221pub struct ConformanceExecutionAdapter {
222 factories: Vec<Rc<dyn ConformanceModuleFactory>>,
223}
224
225impl ConformanceExecutionAdapter {
226 pub fn new() -> Self {
227 Self::default()
228 }
229
230 #[must_use]
231 pub fn with_factory(mut self, factory: impl ConformanceModuleFactory) -> Self {
232 self.factories.push(Rc::new(factory));
233 self
234 }
235
236 fn instantiate(
237 &self,
238 instance: &ModuleInstancePlan,
239 ) -> Result<ConformanceModule, RuntimeFailure> {
240 let matches = self
241 .factories
242 .iter()
243 .filter(|factory| {
244 factory.package_id() == instance.package_id()
245 && (instance.package_revision().is_empty()
246 || factory.package_version() == instance.package_revision())
247 })
248 .collect::<Vec<_>>();
249 match matches.as_slice() {
250 [] => Err(RuntimeFailure::MissingModuleFactory {
251 instance: instance.instance_key().to_owned(),
252 package_id: instance.package_id().to_owned(),
253 }),
254 [factory] => factory.instantiate(instance),
255 _ => Err(RuntimeFailure::InvalidResolvedPlan {
256 detail: format!(
257 "multiple conformance factories declare package `{}`",
258 instance.package_id()
259 ),
260 }),
261 }
262 }
263}
264
265impl lenso_kernel::NativeExecutionAdapter for ConformanceExecutionAdapter {
266 fn prepare(&self, plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
267 plan.validate()
268 .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
269 detail: error.to_string(),
270 })?;
271
272 let mut modules = BTreeMap::new();
273 let mut generations = BTreeMap::new();
274 for instance in plan
275 .module_instances()
276 .iter()
277 .filter(|instance| instance.execution_class() == &ExecutionClassId::native_rust())
278 {
279 let module = self.instantiate(instance)?;
280 generations.insert(instance.instance_key().to_owned(), module.prepared());
281 modules.insert(instance.instance_key().to_owned(), module);
282 }
283
284 let mut bindings = Vec::new();
285 for binding in plan.capability_bindings() {
286 let Some(module) = modules.get(binding.provider_instance()) else {
287 continue;
288 };
289 let endpoint = module
290 .endpoints
291 .iter()
292 .find(|endpoint| {
293 endpoint.capability_id() == binding.capability_id()
294 && endpoint.descriptor_version() == binding.descriptor_version()
295 })
296 .cloned()
297 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
298 detail: format!(
299 "Capability `{}` version `{}` has no request endpoint on provider `{}`",
300 binding.capability_id(),
301 binding.descriptor_version(),
302 binding.provider_instance()
303 ),
304 })?;
305 bindings.push(PreparedBinding::new(
306 binding.consumer_instance(),
307 binding.provider_instance(),
308 endpoint,
309 ));
310 }
311
312 Ok(PreparedNativeApp::new(bindings, generations))
313 }
314
315 fn recreate(
316 &self,
317 plan: &ResolvedAppPlan,
318 instance_key: &str,
319 ) -> Result<PreparedNativeModule, RuntimeFailure> {
320 let instance = plan.module_instance(instance_key).ok_or_else(|| {
321 RuntimeFailure::InvalidResolvedPlan {
322 detail: format!("unknown Module Instance `{instance_key}`"),
323 }
324 })?;
325 Ok(self.instantiate(instance)?.prepared())
326 }
327}
328
329#[derive(Debug)]
331pub struct ProbeConsumerFactory;
332
333impl ConformanceModuleFactory for ProbeConsumerFactory {
334 fn package_id(&self) -> &'static str {
335 PROBE_CONSUMER_PACKAGE_ID
336 }
337
338 fn package_version(&self) -> &'static str {
339 env!("CARGO_PKG_VERSION")
340 }
341
342 fn instantiate(
343 &self,
344 _instance: &ModuleInstancePlan,
345 ) -> Result<ConformanceModule, RuntimeFailure> {
346 Ok(ConformanceModule::with_lifecycle(
347 Vec::new(),
348 ProbeConsumerLifecycle,
349 ))
350 }
351}
352
353#[derive(Debug)]
354struct ProbeConsumerLifecycle;
355
356impl ModuleLifecycle for ProbeConsumerLifecycle {
357 fn activate(&self, context: ActivateContext) -> lenso_kernel::ModuleFuture {
358 let client = (context.dependencies().len() == 1)
359 .then(|| ProbeClient::from_dependencies(context.dependencies()));
360 Box::pin(async move {
361 let Some(client) = client else {
362 return Ok(());
363 };
364 match client?
365 .probe(ProbeRequest {
366 value: "activation".to_owned(),
367 })
368 .await
369 {
370 Ok(_) => Ok(()),
371 Err(ProbeInvocationError::Runtime(error)) => Err(error),
372 Err(ProbeInvocationError::Domain(error)) => Err(RuntimeFailure::ModuleFailure {
373 detail: format!("probe activation dependency returned {error:?}"),
374 }),
375 }
376 })
377 }
378}
379
380#[derive(Debug)]
382pub struct ProbeProviderFactory;
383
384impl ConformanceModuleFactory for ProbeProviderFactory {
385 fn package_id(&self) -> &'static str {
386 PROBE_PROVIDER_PACKAGE_ID
387 }
388
389 fn package_version(&self) -> &'static str {
390 env!("CARGO_PKG_VERSION")
391 }
392
393 fn instantiate(
394 &self,
395 _instance: &ModuleInstancePlan,
396 ) -> Result<ConformanceModule, RuntimeFailure> {
397 Ok(ConformanceModule::new(vec![Rc::new(ProbeEndpoint::new(
398 EchoProbe("Echo"),
399 ))]))
400 }
401}
402
403#[derive(Debug)]
405pub struct AlternateProbeProviderFactory;
406
407impl ConformanceModuleFactory for AlternateProbeProviderFactory {
408 fn package_id(&self) -> &'static str {
409 ALTERNATE_PROBE_PROVIDER_PACKAGE_ID
410 }
411
412 fn package_version(&self) -> &'static str {
413 env!("CARGO_PKG_VERSION")
414 }
415
416 fn instantiate(
417 &self,
418 _instance: &ModuleInstancePlan,
419 ) -> Result<ConformanceModule, RuntimeFailure> {
420 Ok(ConformanceModule::new(vec![Rc::new(ProbeEndpoint::new(
421 EchoProbe("Alternate"),
422 ))]))
423 }
424}
425
426#[derive(Debug)]
427struct EchoProbe(&'static str);
428
429impl ProbeProvider for EchoProbe {
430 fn probe(
431 &self,
432 _context: InvocationContext,
433 request: ProbeRequest,
434 ) -> LocalBoxFuture<'static, Result<ProbeResponse, ProbeInvocationError>> {
435 let prefix = self.0;
436 Box::pin(async move {
437 if request.value.is_empty() {
438 Err(ProbeInvocationError::Domain(ProbeError::EmptyValue))
439 } else {
440 Ok(ProbeResponse {
441 value: format!("{prefix}: {}", request.value),
442 })
443 }
444 })
445 }
446}