Skip to main content

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}