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 supports_runtime_profile(&self, authoring_version: u32, profile: &str) -> bool {
301 matches!(
302 (authoring_version, profile),
303 (1, "lenso.native-authoring@1" | "lenso.native-rust@1")
304 | (2, "lenso.native-authoring@2")
305 )
306 }
307 fn prepare(&self, plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
308 plan.validate()
309 .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
310 detail: error.to_string(),
311 })?;
312
313 let mut plugins = BTreeMap::new();
314 let mut generations = BTreeMap::new();
315 for instance in plan
316 .plugin_instances()
317 .iter()
318 .filter(|instance| instance.execution_class() == &ExecutionClassId::native_rust())
319 {
320 let plugin = self.instantiate(instance)?;
321 generations.insert(instance.instance_key().to_owned(), plugin.prepared());
322 plugins.insert(instance.instance_key().to_owned(), plugin);
323 }
324
325 let mut bindings = Vec::new();
326 let mut stream_bindings = Vec::new();
327 let mut event_bindings = Vec::new();
328 for binding in plan.capability_bindings() {
329 let Some(plugin) = plugins.get(binding.provider_instance()) else {
330 continue;
331 };
332 let provider = plan
333 .plugin_instance(binding.provider_instance())
334 .expect("validated binding provider should exist");
335 let descriptor = provider
336 .provided_capabilities()
337 .iter()
338 .find(|descriptor| descriptor.capability_id() == binding.capability_id())
339 .expect("validated binding descriptor should exist");
340
341 if !descriptor.request_operations().is_empty() {
342 let endpoint = find_endpoint(plugin.endpoints.request(), binding, "request")?;
343 bindings.push(
344 PreparedBinding::new(
345 binding.consumer_instance(),
346 binding.provider_instance(),
347 endpoint,
348 )
349 .with_requirement_id(binding.requirement_id()),
350 );
351 }
352 if !descriptor.stream_operations().is_empty() {
353 let endpoint = find_endpoint(plugin.endpoints.stream(), binding, "stream")?;
354 stream_bindings.push(
355 PreparedStreamBinding::new(
356 binding.consumer_instance(),
357 binding.provider_instance(),
358 endpoint,
359 )
360 .with_requirement_id(binding.requirement_id()),
361 );
362 }
363 if !descriptor.event_operations().is_empty() {
364 let endpoint = find_endpoint(plugin.endpoints.event(), binding, "Event")?;
365 event_bindings.push(
366 PreparedEventBinding::new(
367 binding.consumer_instance(),
368 binding.provider_instance(),
369 endpoint,
370 )
371 .with_requirement_id(binding.requirement_id()),
372 );
373 }
374 }
375
376 Ok(PreparedNativeApp::new(bindings, generations)
377 .with_stream_bindings(stream_bindings)
378 .with_event_bindings(event_bindings))
379 }
380
381 fn recreate(
382 &self,
383 plan: &ResolvedAppPlan,
384 instance_key: &str,
385 ) -> Result<PreparedNativePlugin, RuntimeFailure> {
386 let instance = plan.plugin_instance(instance_key).ok_or_else(|| {
387 RuntimeFailure::InvalidResolvedPlan {
388 detail: format!("unknown Plugin Instance `{instance_key}`"),
389 }
390 })?;
391 Ok(self.instantiate(instance)?.prepared())
392 }
393}
394
395trait ConformanceEndpoint {
396 fn capability_id(&self) -> &'static str;
397 fn descriptor_version(&self) -> &'static str;
398}
399
400impl ConformanceEndpoint for dyn NativeRequestEndpoint {
401 fn capability_id(&self) -> &'static str {
402 NativeRequestEndpoint::capability_id(self)
403 }
404
405 fn descriptor_version(&self) -> &'static str {
406 NativeRequestEndpoint::descriptor_version(self)
407 }
408}
409
410impl ConformanceEndpoint for dyn NativeStreamEndpoint {
411 fn capability_id(&self) -> &'static str {
412 NativeStreamEndpoint::capability_id(self)
413 }
414
415 fn descriptor_version(&self) -> &'static str {
416 NativeStreamEndpoint::descriptor_version(self)
417 }
418}
419
420impl ConformanceEndpoint for dyn NativeEventEndpoint {
421 fn capability_id(&self) -> &'static str {
422 NativeEventEndpoint::capability_id(self)
423 }
424
425 fn descriptor_version(&self) -> &'static str {
426 NativeEventEndpoint::descriptor_version(self)
427 }
428}
429
430fn find_endpoint<T>(
431 endpoints: &[Rc<T>],
432 binding: &lenso_app_plan::CapabilityBinding,
433 interaction: &str,
434) -> Result<Rc<T>, RuntimeFailure>
435where
436 T: ConformanceEndpoint + ?Sized,
437{
438 endpoints
439 .iter()
440 .find(|endpoint| {
441 endpoint.capability_id() == binding.capability_id()
442 && endpoint.descriptor_version() == binding.descriptor_version()
443 })
444 .cloned()
445 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
446 detail: format!(
447 "Capability `{}` version `{}` has no {interaction} endpoint on provider `{}`",
448 binding.capability_id(),
449 binding.descriptor_version(),
450 binding.provider_instance()
451 ),
452 })
453}
454
455#[derive(Debug)]
457pub struct ProbeConsumerFactory;
458
459impl ConformancePluginFactory for ProbeConsumerFactory {
460 fn package_id(&self) -> &'static str {
461 PROBE_CONSUMER_PACKAGE_ID
462 }
463
464 fn package_version(&self) -> &'static str {
465 env!("CARGO_PKG_VERSION")
466 }
467
468 fn instantiate(
469 &self,
470 _instance: &PluginInstancePlan,
471 ) -> Result<ConformancePlugin, RuntimeFailure> {
472 Ok(ConformancePlugin::with_lifecycle(
473 Vec::new(),
474 ProbeConsumerLifecycle,
475 ))
476 }
477}
478
479#[derive(Debug)]
480struct ProbeConsumerLifecycle;
481
482impl PluginLifecycle for ProbeConsumerLifecycle {
483 fn activate(&self, context: ActivateContext) -> lenso_kernel::PluginFuture {
484 let client = (context.dependencies().len() == 1)
485 .then(|| ProbeClient::from_dependencies(context.dependencies()));
486 Box::pin(async move {
487 let Some(client) = client else {
488 return Ok(());
489 };
490 match client?
491 .probe(ProbeRequest {
492 value: "activation".to_owned(),
493 })
494 .await
495 {
496 Ok(_) => Ok(()),
497 Err(ProbeInvocationError::Runtime(error)) => Err(error),
498 Err(ProbeInvocationError::Domain(error)) => Err(RuntimeFailure::PluginFailure {
499 detail: format!("probe activation dependency returned {error:?}"),
500 }),
501 }
502 })
503 }
504}
505
506#[derive(Debug)]
508pub struct ProbeProviderFactory;
509
510impl ConformancePluginFactory for ProbeProviderFactory {
511 fn package_id(&self) -> &'static str {
512 PROBE_PROVIDER_PACKAGE_ID
513 }
514
515 fn package_version(&self) -> &'static str {
516 env!("CARGO_PKG_VERSION")
517 }
518
519 fn instantiate(
520 &self,
521 _instance: &PluginInstancePlan,
522 ) -> Result<ConformancePlugin, RuntimeFailure> {
523 Ok(ConformancePlugin::new(vec![Rc::new(ProbeEndpoint::new(
524 EchoProbe("Echo"),
525 ))]))
526 }
527}
528
529#[derive(Debug)]
531pub struct AlternateProbeProviderFactory;
532
533impl ConformancePluginFactory for AlternateProbeProviderFactory {
534 fn package_id(&self) -> &'static str {
535 ALTERNATE_PROBE_PROVIDER_PACKAGE_ID
536 }
537
538 fn package_version(&self) -> &'static str {
539 env!("CARGO_PKG_VERSION")
540 }
541
542 fn instantiate(
543 &self,
544 _instance: &PluginInstancePlan,
545 ) -> Result<ConformancePlugin, RuntimeFailure> {
546 Ok(ConformancePlugin::new(vec![Rc::new(ProbeEndpoint::new(
547 EchoProbe("Alternate"),
548 ))]))
549 }
550}
551
552#[derive(Debug)]
553struct EchoProbe(&'static str);
554
555impl ProbeProvider for EchoProbe {
556 fn probe(
557 &self,
558 _context: InvocationContext,
559 request: ProbeRequest,
560 ) -> LocalBoxFuture<'static, Result<ProbeResponse, ProbeInvocationError>> {
561 let prefix = self.0;
562 Box::pin(async move {
563 if request.value.is_empty() {
564 Err(ProbeInvocationError::Domain(ProbeError::EmptyValue))
565 } else {
566 Ok(ProbeResponse {
567 value: format!("{prefix}: {}", request.value),
568 })
569 }
570 })
571 }
572}