durable-lambda-macro 1.2.0

Proc-macro for zero-boilerplate AWS Lambda durable execution handler registration
Documentation
# durable-lambda-macro

Proc-macro for zero-boilerplate AWS Lambda durable execution handler registration.

[![Docs.rs](https://docs.rs/durable-lambda-macro/badge.svg)](https://docs.rs/durable-lambda-macro)
[![Crates.io](https://img.shields.io/crates/v/durable-lambda-macro.svg)](https://crates.io/crates/durable-lambda-macro)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](https://github.com/pgdad/durable-rust#license)

## Overview

`durable-lambda-macro` is a **proc-macro crate** that provides the `#[durable_execution]` attribute macro. It eliminates all Lambda runtime boilerplate by generating a `main()` function that wires up AWS configuration, the Lambda client, the durable execution backend, and the Lambda runtime event loop.

This is one of four API styles for the [durable-rust](https://github.com/pgdad/durable-rust) SDK. All styles produce identical runtime behavior -- they differ only in ergonomics:

| Crate | Style | Best for |
|---|---|---|
| [`durable-lambda-closure`]https://crates.io/crates/durable-lambda-closure | Closure-native (recommended) | Simplest syntax, no traits or macros |
| **`durable-lambda-macro`** | **Proc-macro** | **Zero boilerplate with `#[durable_execution]`** |
| [`durable-lambda-trait`]https://crates.io/crates/durable-lambda-trait | Trait-based | OOP pattern, shared state via struct fields |
| [`durable-lambda-builder`]https://crates.io/crates/durable-lambda-builder | Builder-pattern | Most configurable, tracing/error hooks |

Choose the macro style when you want the absolute minimum boilerplate -- just annotate your handler function and the macro does the rest.

## Features

- **`#[durable_execution]` attribute macro** that transforms an async function into a complete Lambda binary
- **Zero boilerplate** -- no `#[tokio::main]`, no `service_fn`, no AWS config setup
- **Compile-time validation** of handler signature: second parameter must be `DurableContext`, return type must be `Result<_, DurableError>`
- **Generates `main()`** with Lambda runtime, AWS client, `RealBackend`, and event parsing
- **Full access to all 8 durable operations** via the generated `DurableContext`

## Getting Started

Add both `durable-lambda-macro` and `durable-lambda-core` to your `Cargo.toml`:

```toml
[dependencies]
durable-lambda-macro = "0.1"
durable-lambda-core = "0.1"
serde_json = "1"
```

Note: `durable-lambda-core` is needed for `DurableContext` and `DurableError` types. The `tokio` and `lambda_runtime` dependencies are handled by the generated code.

## Usage

### Basic Handler

Annotate an async function with `#[durable_execution]`:

```rust
use durable_lambda_macro::durable_execution;
use durable_lambda_core::context::DurableContext;
use durable_lambda_core::error::DurableError;

#[durable_execution]
async fn handler(
    event: serde_json::Value,
    mut ctx: DurableContext,
) -> Result<serde_json::Value, DurableError> {
    let order: Result<serde_json::Value, String> = ctx.step("validate", || async {
        Ok(serde_json::json!({"order_id": 42, "valid": true}))
    }).await?;

    let payment: Result<String, String> = ctx.step("charge", || async {
        Ok("tx-abc-123".to_string())
    }).await?;

    Ok(serde_json::json!({
        "order": order.unwrap(),
        "transaction": payment.unwrap(),
    }))
}
// main() is generated by the macro -- do not define it yourself
```

### Handler with All Operations

```rust
use durable_lambda_macro::durable_execution;
use durable_lambda_core::context::DurableContext;
use durable_lambda_core::error::DurableError;
use durable_lambda_core::types::StepOptions;

#[durable_execution]
async fn handler(
    event: serde_json::Value,
    mut ctx: DurableContext,
) -> Result<serde_json::Value, DurableError> {
    // Step with retries
    let result: Result<i32, String> = ctx.step_with_options(
        "flaky_call",
        StepOptions::new().retries(3).backoff_seconds(2),
        || async { Ok(42) },
    ).await?;

    // Wait
    ctx.wait("cooldown", 5).await?;

    // Replay-safe logging
    ctx.log("processing complete");

    Ok(serde_json::json!({"result": result.unwrap()}))
}
```

## What the Macro Generates

The `#[durable_execution]` attribute preserves your handler function and generates a `#[tokio::main] async fn main()` that:

1. Loads AWS configuration with default credentials
2. Creates an `aws_sdk_lambda::Client`
3. Wraps the client in a `RealBackend` for durable execution API calls
4. Registers with `lambda_runtime` via `service_fn` to receive invocations
5. On each invocation: parses durable execution metadata from the event, creates a `DurableContext`, and calls your handler

Conceptually, the generated code looks like:

```rust
// Your handler function is preserved as-is
async fn handler(event: serde_json::Value, mut ctx: DurableContext)
    -> Result<serde_json::Value, DurableError>
{
    // ... your code ...
}

// This is generated by the macro:
#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
    let client = aws_sdk_lambda::Client::new(&config);
    let backend = std::sync::Arc::new(RealBackend::new(client));

    lambda_runtime::run(service_fn(|event: LambdaEvent<serde_json::Value>| {
        let backend = backend.clone();
        async move {
            let invocation = parse_invocation(&payload)?;
            let ctx = DurableContext::new(backend, /* ... */).await?;
            let result = handler(invocation.user_event, ctx).await;
            wrap_handler_result(result)
        }
    })).await
}
```

## Compile-Time Validations

The macro validates your handler signature at compile time:

- The function must be `async`
- The second parameter must be `DurableContext`
- The return type must be `Result<serde_json::Value, DurableError>`

If any validation fails, you get a clear compile error pointing to the issue.

## Testing

Because the macro generates `main()`, you cannot directly call the handler in tests. Instead, extract your business logic into a separate function that takes `DurableContext` and test it with [`durable-lambda-testing`](https://crates.io/crates/durable-lambda-testing):

```rust
// In your handler module:
pub async fn process_order(
    event: serde_json::Value,
    mut ctx: DurableContext,
) -> Result<serde_json::Value, DurableError> {
    let result: Result<i32, String> = ctx.step("validate", || async { Ok(42) }).await?;
    Ok(serde_json::json!({"result": result.unwrap()}))
}

// In tests:
use durable_lambda_testing::prelude::*;

#[tokio::test]
async fn test_process_order() {
    let (mut ctx, calls, _ops) = MockDurableContext::new()
        .with_step_result("validate", r#"42"#)
        .build()
        .await;

    let result = process_order(serde_json::json!({}), ctx).await.unwrap();
    assert_eq!(result["result"], 42);
    assert_no_checkpoints(&calls).await;
}
```

## API Reference

| Item | Description |
|---|---|
| `#[durable_execution]` | Attribute macro for handler functions |
| `DurableContext` | Main context type (from `durable-lambda-core`) |
| `DurableError` | SDK error type (from `durable-lambda-core`) |
| `StepOptions` | Step configuration (from `durable-lambda-core`) |

Full API documentation: [docs.rs/durable-lambda-macro](https://docs.rs/durable-lambda-macro)

## License

Licensed under either of [MIT](https://github.com/pgdad/durable-rust/blob/main/LICENSE-MIT) or [Apache-2.0](https://github.com/pgdad/durable-rust/blob/main/LICENSE-APACHE) at your option.

## Repository

[https://github.com/pgdad/durable-rust](https://github.com/pgdad/durable-rust)