1use std::{collections::BTreeMap, fmt, rc::Rc};
8
9use futures::future::LocalBoxFuture;
10use lenso_app_plan::{ExecutionClassId, PluginInstancePlan, ResolvedAppPlan};
11use lenso_kernel::{
12 ActivateContext, InvocationContext, NativeEndpointSet, NativeEventEndpoint,
13 NativeRequestEndpoint, NativeRequestHandle, NativeStreamEndpoint, NoopPluginLifecycle,
14 PluginLifecycle, PreparedBinding, PreparedEventBinding, PreparedNativeApp,
15 PreparedNativePlugin, PreparedStreamBinding, RequestCapability, RuntimeFailure,
16};
17
18mod interaction;
19
20pub use interaction::*;
21
22pub const PROBE_CAPABILITY_ID: &str = "lenso.runtime.conformance.probe@1";
24pub const PROBE_DESCRIPTOR_VERSION: &str = "1.0.0";
26pub const PROBE_OPERATION: &str = "probe";
28
29pub const PROBE_PROVIDER_PACKAGE_ID: &str = "lenso.runtime.conformance.probe-provider";
31pub const ALTERNATE_PROBE_PROVIDER_PACKAGE_ID: &str =
33 "lenso.runtime.conformance.alternate-probe-provider";
34pub const PROBE_CONSUMER_PACKAGE_ID: &str = "lenso.runtime.conformance.probe-consumer";
36
37#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct ProbeRequest {
40 pub value: String,
41}
42
43#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct ProbeResponse {
46 pub value: String,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
51pub enum ProbeError {
52 EmptyValue,
53}
54
55#[derive(Debug)]
57pub struct Probe;
58
59impl RequestCapability for Probe {
60 type Request = ProbeRequest;
61 type Response = ProbeResponse;
62 type DomainError = ProbeError;
63
64 const ID: &'static str = PROBE_CAPABILITY_ID;
65 const DESCRIPTOR_VERSION: &'static str = PROBE_DESCRIPTOR_VERSION;
66}
67
68pub trait ProbeProvider: fmt::Debug + 'static {
70 fn probe(
71 &self,
72 context: InvocationContext,
73 request: ProbeRequest,
74 ) -> LocalBoxFuture<'static, Result<ProbeResponse, ProbeInvocationError>>;
75}
76
77#[derive(Debug)]
79pub struct ProbeEndpoint<P> {
80 provider: Rc<P>,
81}
82
83impl<P: ProbeProvider> ProbeEndpoint<P> {
84 pub fn new(provider: P) -> Self {
85 Self {
86 provider: Rc::new(provider),
87 }
88 }
89}
90
91impl<P: ProbeProvider> NativeRequestEndpoint for ProbeEndpoint<P> {
92 fn capability_id(&self) -> &'static str {
93 PROBE_CAPABILITY_ID
94 }
95
96 fn descriptor_version(&self) -> &'static str {
97 PROBE_DESCRIPTOR_VERSION
98 }
99
100 fn operations(&self) -> &'static [&'static str] {
101 &[PROBE_OPERATION]
102 }
103
104 fn invoke(
105 &self,
106 operation: &str,
107 request: Box<dyn std::any::Any>,
108 context: InvocationContext,
109 ) -> LocalBoxFuture<
110 'static,
111 Result<Result<Box<dyn std::any::Any>, Box<dyn std::any::Any>>, RuntimeFailure>,
112 > {
113 if operation != PROBE_OPERATION {
114 return Box::pin(futures::future::ready(Err(
115 RuntimeFailure::UnknownOperation {
116 capability: PROBE_CAPABILITY_ID,
117 operation: operation.to_owned(),
118 },
119 )));
120 }
121 let Ok(request) = request.downcast::<ProbeRequest>() else {
122 return Box::pin(futures::future::ready(Err(
123 RuntimeFailure::ProtocolViolation {
124 capability: PROBE_CAPABILITY_ID,
125 },
126 )));
127 };
128 let provider = Rc::clone(&self.provider);
129 Box::pin(async move {
130 match provider.probe(context, *request).await {
131 Ok(value) => Ok(Ok(Box::new(value) as Box<dyn std::any::Any>)),
132 Err(ProbeInvocationError::Domain(error)) => {
133 Ok(Err(Box::new(error) as Box<dyn std::any::Any>))
134 }
135 Err(ProbeInvocationError::Runtime(error)) => Err(error),
136 }
137 })
138 }
139}
140
141#[derive(Debug)]
143pub struct ProbeClient {
144 handle: NativeRequestHandle<Probe>,
145}
146
147impl ProbeClient {
148 pub fn new(handle: NativeRequestHandle<Probe>) -> Self {
149 Self { handle }
150 }
151
152 pub fn from_dependencies(
153 dependencies: &lenso_kernel::PluginDependencies,
154 ) -> Result<Self, RuntimeFailure> {
155 Ok(Self::new(dependencies.one::<Probe>()?))
156 }
157
158 pub async fn probe(
159 &self,
160 request: ProbeRequest,
161 ) -> Result<ProbeResponse, ProbeInvocationError> {
162 self.handle
163 .invoke(PROBE_OPERATION, request)
164 .await
165 .map_err(ProbeInvocationError::Runtime)?
166 .map_err(ProbeInvocationError::Domain)
167 }
168}
169
170#[derive(Clone, Debug, Eq, PartialEq)]
172pub enum ProbeInvocationError {
173 Domain(ProbeError),
174 Runtime(RuntimeFailure),
175}
176
177#[derive(Debug)]
179pub struct ConformancePlugin {
180 endpoints: NativeEndpointSet,
181 lifecycle: Rc<dyn PluginLifecycle>,
182}
183
184impl ConformancePlugin {
185 pub fn new(endpoints: Vec<Rc<dyn NativeRequestEndpoint>>) -> Self {
186 Self::with_lifecycle(endpoints, NoopPluginLifecycle)
187 }
188
189 pub fn with_lifecycle(
190 endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
191 lifecycle: impl PluginLifecycle,
192 ) -> Self {
193 Self::with_all_endpoints(endpoints, Vec::new(), Vec::new(), lifecycle)
194 }
195
196 pub fn with_all_endpoints(
198 request: Vec<Rc<dyn NativeRequestEndpoint>>,
199 stream: Vec<Rc<dyn NativeStreamEndpoint>>,
200 event: Vec<Rc<dyn NativeEventEndpoint>>,
201 lifecycle: impl PluginLifecycle,
202 ) -> Self {
203 Self {
204 endpoints: NativeEndpointSet::new(request, stream, event),
205 lifecycle: Rc::new(lifecycle),
206 }
207 }
208
209 pub fn with_stream_endpoints(
211 endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
212 lifecycle: impl PluginLifecycle,
213 ) -> Self {
214 Self::with_all_endpoints(Vec::new(), endpoints, Vec::new(), lifecycle)
215 }
216
217 pub fn with_event_endpoints(
219 endpoints: Vec<Rc<dyn NativeEventEndpoint>>,
220 lifecycle: impl PluginLifecycle,
221 ) -> Self {
222 Self::with_all_endpoints(Vec::new(), Vec::new(), endpoints, lifecycle)
223 }
224
225 fn prepared(&self) -> PreparedNativePlugin {
226 PreparedNativePlugin::with_endpoint_set_lifecycle(
227 self.endpoints.clone(),
228 self.lifecycle.clone(),
229 )
230 }
231}
232
233impl Default for ConformancePlugin {
234 fn default() -> Self {
235 Self::new(Vec::new())
236 }
237}
238
239pub trait ConformancePluginFactory: fmt::Debug + 'static {
241 fn package_id(&self) -> &'static str;
242
243 fn package_version(&self) -> &'static str {
244 ""
245 }
246
247 fn instantiate(
248 &self,
249 instance: &PluginInstancePlan,
250 ) -> Result<ConformancePlugin, RuntimeFailure>;
251}
252
253#[derive(Debug, Default)]
255pub struct ConformanceExecutionAdapter {
256 factories: Vec<Rc<dyn ConformancePluginFactory>>,
257}
258
259impl ConformanceExecutionAdapter {
260 pub fn new() -> Self {
261 Self::default()
262 }
263
264 #[must_use]
265 pub fn with_factory(mut self, factory: impl ConformancePluginFactory) -> Self {
266 self.factories.push(Rc::new(factory));
267 self
268 }
269
270 fn instantiate(
271 &self,
272 instance: &PluginInstancePlan,
273 ) -> Result<ConformancePlugin, RuntimeFailure> {
274 let matches = self
275 .factories
276 .iter()
277 .filter(|factory| {
278 factory.package_id() == instance.package_id()
279 && (instance.package_revision().is_empty()
280 || factory.package_version() == instance.package_revision())
281 })
282 .collect::<Vec<_>>();
283 match matches.as_slice() {
284 [] => Err(RuntimeFailure::MissingPluginFactory {
285 instance: instance.instance_key().to_owned(),
286 package_id: instance.package_id().to_owned(),
287 }),
288 [factory] => factory.instantiate(instance),
289 _ => Err(RuntimeFailure::InvalidResolvedPlan {
290 detail: format!(
291 "multiple conformance factories declare package `{}`",
292 instance.package_id()
293 ),
294 }),
295 }
296 }
297}
298
299impl lenso_kernel::NativeExecutionAdapter for ConformanceExecutionAdapter {
300 fn prepare(&self, plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
301 plan.validate()
302 .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
303 detail: error.to_string(),
304 })?;
305
306 let mut plugins = BTreeMap::new();
307 let mut generations = BTreeMap::new();
308 for instance in plan
309 .plugin_instances()
310 .iter()
311 .filter(|instance| instance.execution_class() == &ExecutionClassId::native_rust())
312 {
313 let plugin = self.instantiate(instance)?;
314 generations.insert(instance.instance_key().to_owned(), plugin.prepared());
315 plugins.insert(instance.instance_key().to_owned(), plugin);
316 }
317
318 let mut bindings = Vec::new();
319 let mut stream_bindings = Vec::new();
320 let mut event_bindings = Vec::new();
321 for binding in plan.capability_bindings() {
322 let Some(plugin) = plugins.get(binding.provider_instance()) else {
323 continue;
324 };
325 let provider = plan
326 .plugin_instance(binding.provider_instance())
327 .expect("validated binding provider should exist");
328 let descriptor = provider
329 .provided_capabilities()
330 .iter()
331 .find(|descriptor| descriptor.capability_id() == binding.capability_id())
332 .expect("validated binding descriptor should exist");
333
334 if !descriptor.request_operations().is_empty() {
335 let endpoint = find_endpoint(plugin.endpoints.request(), binding, "request")?;
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 = find_endpoint(plugin.endpoints.stream(), binding, "stream")?;
344 stream_bindings.push(PreparedStreamBinding::new(
345 binding.consumer_instance(),
346 binding.provider_instance(),
347 endpoint,
348 ));
349 }
350 if !descriptor.event_operations().is_empty() {
351 let endpoint = find_endpoint(plugin.endpoints.event(), binding, "Event")?;
352 event_bindings.push(PreparedEventBinding::new(
353 binding.consumer_instance(),
354 binding.provider_instance(),
355 endpoint,
356 ));
357 }
358 }
359
360 Ok(PreparedNativeApp::new(bindings, generations)
361 .with_stream_bindings(stream_bindings)
362 .with_event_bindings(event_bindings))
363 }
364
365 fn recreate(
366 &self,
367 plan: &ResolvedAppPlan,
368 instance_key: &str,
369 ) -> Result<PreparedNativePlugin, RuntimeFailure> {
370 let instance = plan.plugin_instance(instance_key).ok_or_else(|| {
371 RuntimeFailure::InvalidResolvedPlan {
372 detail: format!("unknown Plugin Instance `{instance_key}`"),
373 }
374 })?;
375 Ok(self.instantiate(instance)?.prepared())
376 }
377}
378
379trait ConformanceEndpoint {
380 fn capability_id(&self) -> &'static str;
381 fn descriptor_version(&self) -> &'static str;
382}
383
384impl ConformanceEndpoint for dyn NativeRequestEndpoint {
385 fn capability_id(&self) -> &'static str {
386 NativeRequestEndpoint::capability_id(self)
387 }
388
389 fn descriptor_version(&self) -> &'static str {
390 NativeRequestEndpoint::descriptor_version(self)
391 }
392}
393
394impl ConformanceEndpoint for dyn NativeStreamEndpoint {
395 fn capability_id(&self) -> &'static str {
396 NativeStreamEndpoint::capability_id(self)
397 }
398
399 fn descriptor_version(&self) -> &'static str {
400 NativeStreamEndpoint::descriptor_version(self)
401 }
402}
403
404impl ConformanceEndpoint for dyn NativeEventEndpoint {
405 fn capability_id(&self) -> &'static str {
406 NativeEventEndpoint::capability_id(self)
407 }
408
409 fn descriptor_version(&self) -> &'static str {
410 NativeEventEndpoint::descriptor_version(self)
411 }
412}
413
414fn find_endpoint<T>(
415 endpoints: &[Rc<T>],
416 binding: &lenso_app_plan::CapabilityBinding,
417 interaction: &str,
418) -> Result<Rc<T>, RuntimeFailure>
419where
420 T: ConformanceEndpoint + ?Sized,
421{
422 endpoints
423 .iter()
424 .find(|endpoint| {
425 endpoint.capability_id() == binding.capability_id()
426 && endpoint.descriptor_version() == binding.descriptor_version()
427 })
428 .cloned()
429 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
430 detail: format!(
431 "Capability `{}` version `{}` has no {interaction} endpoint on provider `{}`",
432 binding.capability_id(),
433 binding.descriptor_version(),
434 binding.provider_instance()
435 ),
436 })
437}
438
439#[derive(Debug)]
441pub struct ProbeConsumerFactory;
442
443impl ConformancePluginFactory for ProbeConsumerFactory {
444 fn package_id(&self) -> &'static str {
445 PROBE_CONSUMER_PACKAGE_ID
446 }
447
448 fn package_version(&self) -> &'static str {
449 env!("CARGO_PKG_VERSION")
450 }
451
452 fn instantiate(
453 &self,
454 _instance: &PluginInstancePlan,
455 ) -> Result<ConformancePlugin, RuntimeFailure> {
456 Ok(ConformancePlugin::with_lifecycle(
457 Vec::new(),
458 ProbeConsumerLifecycle,
459 ))
460 }
461}
462
463#[derive(Debug)]
464struct ProbeConsumerLifecycle;
465
466impl PluginLifecycle for ProbeConsumerLifecycle {
467 fn activate(&self, context: ActivateContext) -> lenso_kernel::PluginFuture {
468 let client = (context.dependencies().len() == 1)
469 .then(|| ProbeClient::from_dependencies(context.dependencies()));
470 Box::pin(async move {
471 let Some(client) = client else {
472 return Ok(());
473 };
474 match client?
475 .probe(ProbeRequest {
476 value: "activation".to_owned(),
477 })
478 .await
479 {
480 Ok(_) => Ok(()),
481 Err(ProbeInvocationError::Runtime(error)) => Err(error),
482 Err(ProbeInvocationError::Domain(error)) => Err(RuntimeFailure::PluginFailure {
483 detail: format!("probe activation dependency returned {error:?}"),
484 }),
485 }
486 })
487 }
488}
489
490#[derive(Debug)]
492pub struct ProbeProviderFactory;
493
494impl ConformancePluginFactory for ProbeProviderFactory {
495 fn package_id(&self) -> &'static str {
496 PROBE_PROVIDER_PACKAGE_ID
497 }
498
499 fn package_version(&self) -> &'static str {
500 env!("CARGO_PKG_VERSION")
501 }
502
503 fn instantiate(
504 &self,
505 _instance: &PluginInstancePlan,
506 ) -> Result<ConformancePlugin, RuntimeFailure> {
507 Ok(ConformancePlugin::new(vec![Rc::new(ProbeEndpoint::new(
508 EchoProbe("Echo"),
509 ))]))
510 }
511}
512
513#[derive(Debug)]
515pub struct AlternateProbeProviderFactory;
516
517impl ConformancePluginFactory for AlternateProbeProviderFactory {
518 fn package_id(&self) -> &'static str {
519 ALTERNATE_PROBE_PROVIDER_PACKAGE_ID
520 }
521
522 fn package_version(&self) -> &'static str {
523 env!("CARGO_PKG_VERSION")
524 }
525
526 fn instantiate(
527 &self,
528 _instance: &PluginInstancePlan,
529 ) -> Result<ConformancePlugin, RuntimeFailure> {
530 Ok(ConformancePlugin::new(vec![Rc::new(ProbeEndpoint::new(
531 EchoProbe("Alternate"),
532 ))]))
533 }
534}
535
536#[derive(Debug)]
537struct EchoProbe(&'static str);
538
539impl ProbeProvider for EchoProbe {
540 fn probe(
541 &self,
542 _context: InvocationContext,
543 request: ProbeRequest,
544 ) -> LocalBoxFuture<'static, Result<ProbeResponse, ProbeInvocationError>> {
545 let prefix = self.0;
546 Box::pin(async move {
547 if request.value.is_empty() {
548 Err(ProbeInvocationError::Domain(ProbeError::EmptyValue))
549 } else {
550 Ok(ProbeResponse {
551 value: format!("{prefix}: {}", request.value),
552 })
553 }
554 })
555 }
556}