durable_lambda_builder/handler.rs
1//! Builder-pattern handler construction.
2//!
3//! Provides the [`DurableHandlerBuilder`] and [`handler`] entry point for
4//! builder-pattern durable Lambda handlers (FR35).
5//! Internally wires up `lambda_runtime`, AWS config, and `DurableContext` creation
6//! so users never interact with these directly.
7
8use std::future::Future;
9use std::sync::Arc;
10
11use durable_lambda_core::backend::RealBackend;
12use durable_lambda_core::context::DurableContext;
13use durable_lambda_core::error::DurableError;
14use durable_lambda_core::event::parse_invocation;
15use durable_lambda_core::response::wrap_handler_result;
16use lambda_runtime::{service_fn, LambdaEvent};
17
18use crate::context::BuilderContext;
19
20/// A builder for constructing durable Lambda handlers.
21///
22/// Created via the [`handler`] function. Call [`.run()`](Self::run) to start
23/// the Lambda runtime. Optionally configure tracing and error handling before
24/// calling `.run()` using the builder methods.
25///
26/// # Examples
27///
28/// ```no_run
29/// use durable_lambda_builder::prelude::*;
30///
31/// #[tokio::main]
32/// async fn main() -> Result<(), lambda_runtime::Error> {
33/// durable_lambda_builder::handler(|event: serde_json::Value, mut ctx: BuilderContext| async move {
34/// let result: Result<i32, String> = ctx.step("validate", || async { Ok(42) }).await?;
35/// Ok(serde_json::json!({"result": result.unwrap()}))
36/// })
37/// .run()
38/// .await
39/// }
40/// ```
41pub struct DurableHandlerBuilder<F, Fut>
42where
43 F: Fn(serde_json::Value, BuilderContext) -> Fut + Send + Sync + 'static,
44 Fut: Future<Output = Result<serde_json::Value, DurableError>> + Send,
45{
46 handler: F,
47 _phantom: std::marker::PhantomData<Fut>,
48 tracing_subscriber: Option<Box<dyn tracing::Subscriber + Send + Sync + 'static>>,
49 error_handler: Option<Box<dyn Fn(DurableError) -> DurableError + Send + Sync>>,
50}
51
52/// Create a new [`DurableHandlerBuilder`] from a handler function.
53///
54/// This is the entry point for the builder-pattern API. The returned builder
55/// can be configured and then executed with [`.run()`](DurableHandlerBuilder::run).
56///
57/// # Arguments
58///
59/// * `f` — An async function taking the user event and a `BuilderContext`,
60/// returning `Result<serde_json::Value, DurableError>`
61///
62/// # Examples
63///
64/// ```no_run
65/// use durable_lambda_builder::prelude::*;
66///
67/// #[tokio::main]
68/// async fn main() -> Result<(), lambda_runtime::Error> {
69/// durable_lambda_builder::handler(|event: serde_json::Value, mut ctx: BuilderContext| async move {
70/// let result: Result<i32, String> = ctx.step("validate", || async { Ok(42) }).await?;
71/// Ok(serde_json::json!({"result": result.unwrap()}))
72/// })
73/// .run()
74/// .await
75/// }
76/// ```
77pub fn handler<F, Fut>(f: F) -> DurableHandlerBuilder<F, Fut>
78where
79 F: Fn(serde_json::Value, BuilderContext) -> Fut + Send + Sync + 'static,
80 Fut: Future<Output = Result<serde_json::Value, DurableError>> + Send,
81{
82 DurableHandlerBuilder {
83 handler: f,
84 _phantom: std::marker::PhantomData,
85 tracing_subscriber: None,
86 error_handler: None,
87 }
88}
89
90impl<F, Fut> DurableHandlerBuilder<F, Fut>
91where
92 F: Fn(serde_json::Value, BuilderContext) -> Fut + Send + Sync + 'static,
93 Fut: Future<Output = Result<serde_json::Value, DurableError>> + Send,
94{
95 /// Configure a tracing subscriber to install before the Lambda runtime starts.
96 ///
97 /// The provided subscriber is installed via [`tracing::subscriber::set_global_default`]
98 /// when [`run()`](Self::run) is called, before any Lambda invocations are processed.
99 ///
100 /// # Arguments
101 ///
102 /// * `subscriber` — Any type implementing [`tracing::Subscriber`] + `Send + Sync + 'static`
103 ///
104 /// # Examples
105 ///
106 /// ```no_run
107 /// use durable_lambda_builder::prelude::*;
108 ///
109 /// #[tokio::main]
110 /// async fn main() -> Result<(), lambda_runtime::Error> {
111 /// durable_lambda_builder::handler(|event: serde_json::Value, mut ctx: BuilderContext| async move {
112 /// Ok(serde_json::json!({"ok": true}))
113 /// })
114 /// .with_tracing(tracing_subscriber::fmt().finish())
115 /// .run()
116 /// .await
117 /// }
118 /// ```
119 pub fn with_tracing(
120 mut self,
121 subscriber: impl tracing::Subscriber + Send + Sync + 'static,
122 ) -> Self {
123 self.tracing_subscriber = Some(Box::new(subscriber));
124 self
125 }
126
127 /// Configure a custom error handler to transform errors before they propagate.
128 ///
129 /// The provided function is called whenever the user handler returns an `Err(DurableError)`,
130 /// allowing error transformation, logging, or enrichment before the error is returned
131 /// to the Lambda runtime.
132 ///
133 /// # Arguments
134 ///
135 /// * `handler` — A closure `Fn(DurableError) -> DurableError` that transforms errors
136 ///
137 /// # Examples
138 ///
139 /// ```no_run
140 /// use durable_lambda_builder::prelude::*;
141 /// use durable_lambda_core::error::DurableError;
142 ///
143 /// #[tokio::main]
144 /// async fn main() -> Result<(), lambda_runtime::Error> {
145 /// durable_lambda_builder::handler(|event: serde_json::Value, mut ctx: BuilderContext| async move {
146 /// Ok(serde_json::json!({"ok": true}))
147 /// })
148 /// .with_error_handler(|e: DurableError| {
149 /// // Log or transform the error before it propagates.
150 /// e
151 /// })
152 /// .run()
153 /// .await
154 /// }
155 /// ```
156 pub fn with_error_handler(
157 mut self,
158 handler: impl Fn(DurableError) -> DurableError + Send + Sync + 'static,
159 ) -> Self {
160 self.error_handler = Some(Box::new(handler));
161 self
162 }
163
164 /// Consume the builder and start the Lambda runtime.
165 ///
166 /// This method:
167 /// 1. Installs the tracing subscriber (if configured via [`with_tracing`](Self::with_tracing))
168 /// 2. Initializes AWS configuration and creates a Lambda client
169 /// 3. Creates a [`RealBackend`] for durable execution API calls
170 /// 4. Registers with `lambda_runtime` to receive invocations
171 /// 5. On each invocation, extracts durable execution metadata from the event,
172 /// creates a [`BuilderContext`], and calls the user handler
173 /// 6. Routes handler errors through the error handler (if configured via
174 /// [`with_error_handler`](Self::with_error_handler))
175 ///
176 /// # Errors
177 ///
178 /// Returns `lambda_runtime::Error` if the Lambda runtime fails to start or
179 /// encounters a fatal error.
180 ///
181 /// # Panics
182 ///
183 /// Panics if a tracing subscriber is configured and a global default subscriber
184 /// has already been set (e.g., by another library or test framework).
185 ///
186 /// # Examples
187 ///
188 /// ```no_run
189 /// use durable_lambda_builder::prelude::*;
190 ///
191 /// #[tokio::main]
192 /// async fn main() -> Result<(), lambda_runtime::Error> {
193 /// durable_lambda_builder::handler(|event: serde_json::Value, mut ctx: BuilderContext| async move {
194 /// Ok(serde_json::json!({"ok": true}))
195 /// })
196 /// .run()
197 /// .await
198 /// }
199 /// ```
200 pub async fn run(self) -> Result<(), lambda_runtime::Error> {
201 // Install the tracing subscriber before Lambda runtime starts, if configured.
202 if let Some(subscriber) = self.tracing_subscriber {
203 tracing::subscriber::set_global_default(subscriber)
204 .expect("tracing subscriber already set");
205 }
206
207 let error_handler = self.error_handler;
208
209 let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
210 let client = aws_sdk_lambda::Client::new(&config);
211 let backend = Arc::new(RealBackend::new(client));
212
213 lambda_runtime::run(service_fn(|event: LambdaEvent<serde_json::Value>| {
214 let backend = backend.clone();
215 let handler = &self.handler;
216 let error_handler = &error_handler;
217 async move {
218 let (payload, _lambda_ctx) = event.into_parts();
219
220 // Parse all durable execution fields from the Lambda event.
221 let invocation = parse_invocation(&payload)
222 .map_err(Box::<dyn std::error::Error + Send + Sync>::from)?;
223
224 // Create DurableContext and wrap in BuilderContext.
225 let durable_ctx = DurableContext::new(
226 backend,
227 invocation.durable_execution_arn,
228 invocation.checkpoint_token,
229 invocation.operations,
230 invocation.next_marker,
231 )
232 .await
233 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
234
235 let builder_ctx = BuilderContext::new(durable_ctx);
236
237 // Call the user handler with owned context.
238 let result = handler(invocation.user_event, builder_ctx).await;
239
240 // Route errors through the custom error handler if configured.
241 let result = match result {
242 Ok(v) => Ok(v),
243 Err(e) => {
244 let transformed = if let Some(ref h) = error_handler {
245 h(e)
246 } else {
247 e
248 };
249 Err(transformed)
250 }
251 };
252
253 // Wrap the result in the durable execution invocation output envelope.
254 wrap_handler_result(result)
255 }
256 }))
257 .await
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::context::BuilderContext;
265 use tracing_subscriber::fmt;
266
267 #[test]
268 fn test_builder_construction_and_type_correctness() {
269 // Verify the handler() constructor creates a DurableHandlerBuilder
270 // and the type system enforces correct handler signatures.
271 let _builder = handler(
272 |_event: serde_json::Value, _ctx: BuilderContext| async move {
273 Ok(serde_json::json!({"ok": true}))
274 },
275 );
276 // If this compiles, the builder type is correct.
277 }
278
279 #[test]
280 fn test_builder_run_returns_future() {
281 // Verify that .run() returns a Future (type-level check).
282 let builder = handler(
283 |_event: serde_json::Value, _ctx: BuilderContext| async move {
284 Ok(serde_json::json!({"ok": true}))
285 },
286 );
287 // run() is async — calling it without .await produces a Future.
288 // We just verify the method exists and returns the right type.
289 let _future = builder.run();
290 // Drop without awaiting — we can't start lambda_runtime in tests.
291 }
292
293 #[test]
294 fn test_with_tracing_stores_subscriber() {
295 // Verify handler(fn).with_tracing(subscriber) compiles and stores the subscriber.
296 let subscriber = fmt().finish();
297 let _builder = handler(
298 |_event: serde_json::Value, _ctx: BuilderContext| async move {
299 Ok(serde_json::json!({"ok": true}))
300 },
301 )
302 .with_tracing(subscriber);
303 // If this compiles, the with_tracing() method exists and accepts a Subscriber.
304 }
305
306 #[test]
307 fn test_with_error_handler_stores_handler() {
308 // Verify handler(fn).with_error_handler(fn) compiles and stores the error handler.
309 let _builder = handler(
310 |_event: serde_json::Value, _ctx: BuilderContext| async move {
311 Ok(serde_json::json!({"ok": true}))
312 },
313 )
314 .with_error_handler(|e: DurableError| e);
315 // If this compiles, the with_error_handler() method exists and accepts a closure.
316 }
317
318 #[test]
319 fn test_builder_chaining() {
320 // Verify method chaining: handler(fn).with_tracing(sub).with_error_handler(fn) compiles.
321 let subscriber = fmt().finish();
322 let _builder = handler(
323 |_event: serde_json::Value, _ctx: BuilderContext| async move {
324 Ok(serde_json::json!({"ok": true}))
325 },
326 )
327 .with_tracing(subscriber)
328 .with_error_handler(|e: DurableError| e);
329 // If this compiles, method chaining is correctly supported.
330 }
331
332 #[test]
333 fn test_builder_without_config_backward_compatible() {
334 // Verify that handler(fn).run() still works without calling with_tracing or with_error_handler.
335 let builder = handler(
336 |_event: serde_json::Value, _ctx: BuilderContext| async move {
337 Ok(serde_json::json!({"ok": true}))
338 },
339 );
340 // Confirm .run() method still exists and returns a Future (backward compat).
341 let _future = builder.run();
342 // Drop without awaiting — we can't start lambda_runtime in tests.
343 }
344}