Skip to main content

lenso_module_authoring/
lib.rs

1//! Runtime-neutral authoring primitives for strongly typed Lenso Modules.
2
3use std::{cell::OnceCell, ops::Deref, rc::Rc};
4
5/// One Module operation failure with an explicit Domain/Runtime split.
6///
7/// Ordinary operations can return `Result<T, DomainError>` directly. Use this
8/// type only when Module code must deliberately surface an Adapter-specific
9/// runtime failure in addition to its Capability-defined Domain Errors.
10#[derive(Clone, Debug, PartialEq)]
11pub enum ModuleError<DomainError, RuntimeError> {
12    /// An expected Capability-defined business rejection.
13    Domain(DomainError),
14    /// An infrastructure or execution failure outside the Capability contract.
15    Runtime(RuntimeError),
16}
17
18impl<DomainError, RuntimeError> ModuleError<DomainError, RuntimeError> {
19    /// Creates a Capability-defined Domain Error.
20    pub const fn domain(error: DomainError) -> Self {
21        Self::Domain(error)
22    }
23
24    /// Creates an Adapter-specific Runtime Error.
25    pub const fn runtime(error: RuntimeError) -> Self {
26        Self::Runtime(error)
27    }
28
29    /// Maps the Domain Error while preserving the Runtime Error.
30    pub fn map_domain<Other>(
31        self,
32        map: impl FnOnce(DomainError) -> Other,
33    ) -> ModuleError<Other, RuntimeError> {
34        match self {
35            Self::Domain(error) => ModuleError::Domain(map(error)),
36            Self::Runtime(error) => ModuleError::Runtime(error),
37        }
38    }
39}
40
41/// A generated, strongly typed client for one required Capability.
42///
43/// Capability binding generators implement this trait for their client type so
44/// Module authoring frontends can connect typed Ports without knowing the
45/// Capability's operation kinds or handle layout. Implementations must use only
46/// the supplied Plan-owned dependencies; they must not perform discovery.
47pub trait CapabilityClient: Sized + 'static {
48    /// Adapter-owned dependency view used to connect this client.
49    type Dependencies: ?Sized;
50    /// Adapter-owned failure returned when connection cannot complete.
51    type Error;
52
53    /// Stable Capability identity required by this client.
54    const CAPABILITY_ID: &'static str;
55    /// Exact Descriptor version understood by this generated client.
56    const DESCRIPTOR_VERSION: &'static str;
57
58    /// Connects this client to one Module Instance's resolved dependencies.
59    fn from_dependencies(dependencies: &Self::Dependencies) -> Result<Self, Self::Error>;
60
61    /// Creates the adapter failure for an invalid second connection attempt.
62    fn already_connected() -> Self::Error;
63}
64
65/// A typed, lifecycle-bound Capability requirement declared by a Module.
66///
67/// Generated Module glue connects the Port during activation. Module behavior
68/// can then call the generated Capability client directly through `Deref`.
69/// A fresh Module generation owns fresh Ports; reconnecting one Port is an
70/// invalid lifecycle transition.
71pub struct Port<C: CapabilityClient> {
72    client: Rc<OnceCell<C>>,
73}
74
75impl<C: CapabilityClient> Port<C> {
76    /// Creates a disconnected typed Port.
77    #[must_use]
78    pub fn new() -> Self {
79        Self {
80            client: Rc::new(OnceCell::new()),
81        }
82    }
83
84    /// Connects the Port from this Module Instance's resolved dependencies.
85    pub fn connect(&self, dependencies: &C::Dependencies) -> Result<(), C::Error> {
86        let client = C::from_dependencies(dependencies)?;
87        self.client.set(client).map_err(|_| C::already_connected())
88    }
89
90    /// Returns whether lifecycle activation connected this Port.
91    #[must_use]
92    pub fn is_connected(&self) -> bool {
93        self.client.get().is_some()
94    }
95}
96
97impl<C: CapabilityClient> Clone for Port<C> {
98    fn clone(&self) -> Self {
99        Self {
100            client: Rc::clone(&self.client),
101        }
102    }
103}
104
105impl<C: CapabilityClient> std::fmt::Debug for Port<C> {
106    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        formatter
108            .debug_struct("Port")
109            .field("capability_id", &C::CAPABILITY_ID)
110            .field("descriptor_version", &C::DESCRIPTOR_VERSION)
111            .field("connected", &self.is_connected())
112            .finish()
113    }
114}
115
116impl<C: CapabilityClient> Default for Port<C> {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl<C: CapabilityClient> Deref for Port<C> {
123    type Target = C;
124
125    fn deref(&self) -> &Self::Target {
126        self.client.get().unwrap_or_else(|| {
127            panic!(
128                "Capability Port {} was used before Module activation",
129                C::CAPABILITY_ID
130            )
131        })
132    }
133}
134
135/// Common imports for a Module authoring frontend.
136pub mod prelude {
137    pub use crate::{CapabilityClient, ModuleError, Port};
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[derive(Debug, Eq, PartialEq)]
145    struct ExampleClient(u64);
146
147    #[derive(Debug, Eq, PartialEq)]
148    enum ExampleError {
149        AlreadyConnected,
150    }
151
152    impl CapabilityClient for ExampleClient {
153        type Dependencies = ();
154        type Error = ExampleError;
155
156        const CAPABILITY_ID: &'static str = "example.echo@1";
157        const DESCRIPTOR_VERSION: &'static str = "1.0.0";
158
159        fn from_dependencies(_dependencies: &Self::Dependencies) -> Result<Self, Self::Error> {
160            Ok(Self(42))
161        }
162
163        fn already_connected() -> Self::Error {
164            ExampleError::AlreadyConnected
165        }
166    }
167
168    #[test]
169    fn port_connects_once_and_is_shared_by_module_clones() {
170        let port = Port::<ExampleClient>::new();
171        let module_clone = port.clone();
172        assert!(!port.is_connected());
173
174        port.connect(&())
175            .expect("the generated client should connect");
176
177        assert!(module_clone.is_connected());
178        assert_eq!(module_clone.0, 42);
179        assert_eq!(port.connect(&()), Err(ExampleError::AlreadyConnected));
180    }
181
182    #[test]
183    fn module_error_preserves_runtime_failures_while_mapping_domain_errors() {
184        let domain = ModuleError::<_, &str>::domain("missing").map_domain(str::len);
185        assert_eq!(domain, ModuleError::Domain(7));
186
187        let runtime = ModuleError::<&str, _>::runtime("cancelled").map_domain(str::len);
188        assert_eq!(runtime, ModuleError::Runtime("cancelled"));
189    }
190}