Skip to main content

lenso_plugin_authoring/
lib.rs

1//! Runtime-neutral authoring primitives for strongly typed Lenso Plugins.
2
3use std::{cell::OnceCell, marker::PhantomData, ops::Deref, rc::Rc};
4
5/// Generated exact Capability reference used by dependency declarations.
6///
7/// It carries contract identity and a zero-sized client type witness without
8/// selecting a provider or depending on any product-specific SDK.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub struct CapabilityReference<C> {
11    capability_id: &'static str,
12    descriptor_version: &'static str,
13    descriptor_digest: &'static str,
14    client: PhantomData<fn() -> C>,
15}
16
17impl<C> CapabilityReference<C> {
18    #[must_use]
19    pub const fn new(
20        capability_id: &'static str,
21        descriptor_version: &'static str,
22        descriptor_digest: &'static str,
23    ) -> Self {
24        Self {
25            capability_id,
26            descriptor_version,
27            descriptor_digest,
28            client: PhantomData,
29        }
30    }
31
32    #[must_use]
33    pub const fn capability_id(&self) -> &'static str {
34        self.capability_id
35    }
36
37    #[must_use]
38    pub const fn descriptor_version(&self) -> &'static str {
39        self.descriptor_version
40    }
41
42    #[must_use]
43    pub const fn descriptor_digest(&self) -> &'static str {
44        self.descriptor_digest
45    }
46}
47
48/// One Plugin operation failure with an explicit Domain/Runtime split.
49///
50/// Ordinary operations can return `Result<T, DomainError>` directly. Use this
51/// type only when Plugin code must deliberately surface an Adapter-specific
52/// runtime failure in addition to its Capability-defined Domain Errors.
53#[derive(Clone, Debug, PartialEq)]
54pub enum PluginError<DomainError, RuntimeError> {
55    /// An expected Capability-defined business rejection.
56    Domain(DomainError),
57    /// An infrastructure or execution failure outside the Capability contract.
58    Runtime(RuntimeError),
59}
60
61impl<DomainError, RuntimeError> PluginError<DomainError, RuntimeError> {
62    /// Creates a Capability-defined Domain Error.
63    pub const fn domain(error: DomainError) -> Self {
64        Self::Domain(error)
65    }
66
67    /// Creates an Adapter-specific Runtime Error.
68    pub const fn runtime(error: RuntimeError) -> Self {
69        Self::Runtime(error)
70    }
71
72    /// Maps the Domain Error while preserving the Runtime Error.
73    pub fn map_domain<Other>(
74        self,
75        map: impl FnOnce(DomainError) -> Other,
76    ) -> PluginError<Other, RuntimeError> {
77        match self {
78            Self::Domain(error) => PluginError::Domain(map(error)),
79            Self::Runtime(error) => PluginError::Runtime(error),
80        }
81    }
82}
83
84/// A generated, strongly typed client for one required Capability.
85///
86/// Capability binding generators implement this trait for their client type so
87/// Plugin authoring frontends can connect typed Ports without knowing the
88/// Capability's operation kinds or handle layout. Implementations must use only
89/// the supplied Plan-owned dependencies; they must not perform discovery.
90pub trait CapabilityClient: Sized + 'static {
91    /// Adapter-owned dependency view used to connect this client.
92    type Dependencies: ?Sized;
93    /// Adapter-owned failure returned when connection cannot complete.
94    type Error;
95
96    /// Stable Capability identity required by this client.
97    const CAPABILITY_ID: &'static str;
98    /// Exact Descriptor version understood by this generated client.
99    const DESCRIPTOR_VERSION: &'static str;
100
101    /// Connects this client to one Plugin Instance's resolved dependencies.
102    fn from_dependencies(dependencies: &Self::Dependencies) -> Result<Self, Self::Error>;
103
104    /// Connects this client to one source-named requirement.
105    ///
106    /// Generated clients override this method to narrow the dependency view by
107    /// exact requirement identity. The default preserves older projections.
108    fn from_requirement(
109        dependencies: &Self::Dependencies,
110        _requirement_id: &str,
111    ) -> Result<Self, Self::Error> {
112        Self::from_dependencies(dependencies)
113    }
114
115    /// Creates the adapter failure for an invalid second connection attempt.
116    fn already_connected() -> Self::Error;
117}
118
119/// A generated Capability client that can be connected to every explicitly
120/// bound provider in deterministic Resolved App Plan order.
121pub trait CapabilityClientMany: CapabilityClient {
122    /// Connects one typed client per bound provider without performing discovery.
123    fn many_from_dependencies(
124        dependencies: &Self::Dependencies,
125    ) -> Result<Vec<BoundCapabilityClient<Self>>, Self::Error>;
126
127    /// Connects every provider bound to one source-named requirement.
128    fn many_from_requirement(
129        dependencies: &Self::Dependencies,
130        _requirement_id: &str,
131    ) -> Result<Vec<BoundCapabilityClient<Self>>, Self::Error> {
132        Self::many_from_dependencies(dependencies)
133    }
134}
135
136/// One typed Capability client paired with its App-local provider Instance key.
137#[derive(Debug)]
138pub struct BoundCapabilityClient<C> {
139    provider_instance: String,
140    client: C,
141}
142
143impl<C> BoundCapabilityClient<C> {
144    /// Creates one Plan-bound client entry.
145    #[must_use]
146    pub fn new(provider_instance: impl Into<String>, client: C) -> Self {
147        Self {
148            provider_instance: provider_instance.into(),
149            client,
150        }
151    }
152
153    /// Returns the App-local provider Instance key selected by Composition.
154    #[must_use]
155    pub fn provider_instance(&self) -> &str {
156        &self.provider_instance
157    }
158
159    /// Returns the generated typed client.
160    #[must_use]
161    pub const fn client(&self) -> &C {
162        &self.client
163    }
164}
165
166impl<C> Deref for BoundCapabilityClient<C> {
167    type Target = C;
168
169    fn deref(&self) -> &Self::Target {
170        &self.client
171    }
172}
173
174/// A typed, lifecycle-bound Capability requirement declared by a Plugin.
175///
176/// Generated Plugin glue connects the Port during activation. Plugin behavior
177/// can then call the generated Capability client directly through `Deref`.
178/// A fresh Plugin generation owns fresh Ports; reconnecting one Port is an
179/// invalid lifecycle transition.
180pub struct Port<C: CapabilityClient> {
181    client: Rc<OnceCell<C>>,
182}
183
184impl<C: CapabilityClient> Port<C> {
185    /// Creates a disconnected typed Port.
186    #[must_use]
187    pub fn new() -> Self {
188        Self {
189            client: Rc::new(OnceCell::new()),
190        }
191    }
192
193    /// Connects the Port from this Plugin Instance's resolved dependencies.
194    pub fn connect(&self, dependencies: &C::Dependencies) -> Result<(), C::Error> {
195        let client = C::from_dependencies(dependencies)?;
196        self.client.set(client).map_err(|_| C::already_connected())
197    }
198
199    /// Connects the Port through one exact source declaration.
200    pub fn connect_requirement(
201        &self,
202        dependencies: &C::Dependencies,
203        requirement_id: &str,
204    ) -> Result<(), C::Error> {
205        let client = C::from_requirement(dependencies, requirement_id)?;
206        self.client.set(client).map_err(|_| C::already_connected())
207    }
208
209    /// Returns whether lifecycle activation connected this Port.
210    #[must_use]
211    pub fn is_connected(&self) -> bool {
212        self.client.get().is_some()
213    }
214}
215
216impl<C: CapabilityClient> Clone for Port<C> {
217    fn clone(&self) -> Self {
218        Self {
219            client: Rc::clone(&self.client),
220        }
221    }
222}
223
224impl<C: CapabilityClient> std::fmt::Debug for Port<C> {
225    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        formatter
227            .debug_struct("Port")
228            .field("capability_id", &C::CAPABILITY_ID)
229            .field("descriptor_version", &C::DESCRIPTOR_VERSION)
230            .field("connected", &self.is_connected())
231            .finish()
232    }
233}
234
235impl<C: CapabilityClient> Default for Port<C> {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241impl<C: CapabilityClient> Deref for Port<C> {
242    type Target = C;
243
244    fn deref(&self) -> &Self::Target {
245        self.client.get().unwrap_or_else(|| {
246            panic!(
247                "Capability Port {} was used before Plugin activation",
248                C::CAPABILITY_ID
249            )
250        })
251    }
252}
253
254/// A typed, lifecycle-bound `many` Capability requirement declared by a Plugin.
255///
256/// Generated Plugin glue connects one client per explicitly bound provider during
257/// activation. Entries retain their provider Instance keys and resolved order.
258pub struct ManyPort<C: CapabilityClientMany> {
259    clients: Rc<OnceCell<Vec<BoundCapabilityClient<C>>>>,
260}
261
262impl<C: CapabilityClientMany> ManyPort<C> {
263    /// Creates a disconnected typed `many` Port.
264    #[must_use]
265    pub fn new() -> Self {
266        Self {
267            clients: Rc::new(OnceCell::new()),
268        }
269    }
270
271    /// Connects the Port from this Plugin Instance's resolved dependencies.
272    pub fn connect(&self, dependencies: &C::Dependencies) -> Result<(), C::Error> {
273        let clients = C::many_from_dependencies(dependencies)?;
274        self.clients
275            .set(clients)
276            .map_err(|_| C::already_connected())
277    }
278
279    /// Connects only providers selected for one exact source declaration.
280    pub fn connect_requirement(
281        &self,
282        dependencies: &C::Dependencies,
283        requirement_id: &str,
284    ) -> Result<(), C::Error> {
285        let clients = C::many_from_requirement(dependencies, requirement_id)?;
286        self.clients
287            .set(clients)
288            .map_err(|_| C::already_connected())
289    }
290
291    /// Returns whether lifecycle activation connected this Port.
292    #[must_use]
293    pub fn is_connected(&self) -> bool {
294        self.clients.get().is_some()
295    }
296}
297
298impl<C: CapabilityClientMany> Clone for ManyPort<C> {
299    fn clone(&self) -> Self {
300        Self {
301            clients: Rc::clone(&self.clients),
302        }
303    }
304}
305
306impl<C: CapabilityClientMany> std::fmt::Debug for ManyPort<C> {
307    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        formatter
309            .debug_struct("ManyPort")
310            .field("capability_id", &C::CAPABILITY_ID)
311            .field("descriptor_version", &C::DESCRIPTOR_VERSION)
312            .field("connected", &self.is_connected())
313            .field("provider_count", &self.clients.get().map(Vec::len))
314            .finish()
315    }
316}
317
318impl<C: CapabilityClientMany> Default for ManyPort<C> {
319    fn default() -> Self {
320        Self::new()
321    }
322}
323
324impl<C: CapabilityClientMany> Deref for ManyPort<C> {
325    type Target = [BoundCapabilityClient<C>];
326
327    fn deref(&self) -> &Self::Target {
328        self.clients.get().map_or_else(
329            || {
330                panic!(
331                    "Capability ManyPort {} was used before Plugin activation",
332                    C::CAPABILITY_ID
333                )
334            },
335            Vec::as_slice,
336        )
337    }
338}
339
340/// Common imports for a Plugin authoring frontend.
341pub mod prelude {
342    pub use crate::{
343        BoundCapabilityClient, CapabilityClient, CapabilityClientMany, CapabilityReference,
344        ManyPort, PluginError, Port,
345    };
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[derive(Debug, Eq, PartialEq)]
353    struct ExampleClient(u64);
354
355    #[derive(Debug, Eq, PartialEq)]
356    enum ExampleError {
357        AlreadyConnected,
358    }
359
360    impl CapabilityClient for ExampleClient {
361        type Dependencies = ();
362        type Error = ExampleError;
363
364        const CAPABILITY_ID: &'static str = "example.echo@1";
365        const DESCRIPTOR_VERSION: &'static str = "1.0.0";
366
367        fn from_dependencies(_dependencies: &Self::Dependencies) -> Result<Self, Self::Error> {
368            Ok(Self(42))
369        }
370
371        fn already_connected() -> Self::Error {
372            ExampleError::AlreadyConnected
373        }
374    }
375
376    impl CapabilityClientMany for ExampleClient {
377        fn many_from_dependencies(
378            _dependencies: &Self::Dependencies,
379        ) -> Result<Vec<BoundCapabilityClient<Self>>, Self::Error> {
380            Ok(vec![
381                BoundCapabilityClient::new("alpha", Self(1)),
382                BoundCapabilityClient::new("beta", Self(2)),
383            ])
384        }
385    }
386
387    #[test]
388    fn port_connects_once_and_is_shared_by_plugin_clones() {
389        let port = Port::<ExampleClient>::new();
390        let plugin_clone = port.clone();
391        assert!(!port.is_connected());
392
393        port.connect(&())
394            .expect("the generated client should connect");
395
396        assert!(plugin_clone.is_connected());
397        assert_eq!(plugin_clone.0, 42);
398        assert_eq!(port.connect(&()), Err(ExampleError::AlreadyConnected));
399    }
400
401    #[test]
402    fn many_port_preserves_provider_identity_and_resolved_order() {
403        let port = ManyPort::<ExampleClient>::new();
404        let plugin_clone = port.clone();
405        assert!(!port.is_connected());
406
407        port.connect(&())
408            .expect("the generated clients should connect");
409
410        assert!(plugin_clone.is_connected());
411        assert_eq!(plugin_clone[0].provider_instance(), "alpha");
412        assert_eq!(plugin_clone[0].client().0, 1);
413        assert_eq!(plugin_clone[1].provider_instance(), "beta");
414        assert_eq!(plugin_clone[1].client().0, 2);
415        assert_eq!(port.connect(&()), Err(ExampleError::AlreadyConnected));
416    }
417
418    #[test]
419    fn plugin_error_preserves_runtime_failures_while_mapping_domain_errors() {
420        let domain = PluginError::<_, &str>::domain("missing").map_domain(str::len);
421        assert_eq!(domain, PluginError::Domain(7));
422
423        let runtime = PluginError::<&str, _>::runtime("cancelled").map_domain(str::len);
424        assert_eq!(runtime, PluginError::Runtime("cancelled"));
425    }
426
427    #[derive(Debug, Eq, PartialEq)]
428    struct NamedClient(String);
429
430    impl CapabilityClient for NamedClient {
431        type Dependencies = ();
432        type Error = ExampleError;
433
434        const CAPABILITY_ID: &'static str = "example.store@1";
435        const DESCRIPTOR_VERSION: &'static str = "1.0.0";
436
437        fn from_dependencies(_dependencies: &Self::Dependencies) -> Result<Self, Self::Error> {
438            Ok(Self("legacy".to_owned()))
439        }
440
441        fn from_requirement(
442            _dependencies: &Self::Dependencies,
443            requirement_id: &str,
444        ) -> Result<Self, Self::Error> {
445            Ok(Self(requirement_id.to_owned()))
446        }
447
448        fn already_connected() -> Self::Error {
449            ExampleError::AlreadyConnected
450        }
451    }
452
453    impl CapabilityClientMany for NamedClient {
454        fn many_from_dependencies(
455            _dependencies: &Self::Dependencies,
456        ) -> Result<Vec<BoundCapabilityClient<Self>>, Self::Error> {
457            Ok(Vec::new())
458        }
459
460        fn many_from_requirement(
461            _dependencies: &Self::Dependencies,
462            requirement_id: &str,
463        ) -> Result<Vec<BoundCapabilityClient<Self>>, Self::Error> {
464            Ok(vec![BoundCapabilityClient::new(
465                format!("{requirement_id}-provider"),
466                Self(requirement_id.to_owned()),
467            )])
468        }
469    }
470
471    #[test]
472    fn named_ports_pass_the_exact_source_requirement_to_generated_clients() {
473        let source = Port::<NamedClient>::new();
474        let destination = Port::<NamedClient>::new();
475        source.connect_requirement(&(), "source").unwrap();
476        destination.connect_requirement(&(), "destination").unwrap();
477        assert_eq!(source.0, "source");
478        assert_eq!(destination.0, "destination");
479
480        let stores = ManyPort::<NamedClient>::new();
481        stores.connect_requirement(&(), "stores").unwrap();
482        assert_eq!(stores[0].provider_instance(), "stores-provider");
483        assert_eq!(stores[0].0, "stores");
484    }
485
486    #[test]
487    fn generated_capability_reference_is_typed_and_provider_free() {
488        let reference = CapabilityReference::<NamedClient>::new(
489            "example.store@1",
490            "1.0.0",
491            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
492        );
493        assert_eq!(reference.capability_id(), "example.store@1");
494        assert_eq!(reference.descriptor_version(), "1.0.0");
495        assert_eq!(
496            reference.descriptor_digest(),
497            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
498        );
499    }
500}