cqrs-rust-lib 0.7.0

An opinionated implementation of CQRS/Event Sourcing with pluggable storage backends (InMemory, PostgreSQL, MongoDB, SurrealDB)
Documentation
# Migration Guide: Domain Error Codes

## Overview

This guide describes the migration from `std::io::Error` (or any generic error type) to the structured `CqrsError` system with domain-specific error codes.

The new error system provides:

- **Structured error codes** per domain (e.g., `ACCOUNT_INSUFFICIENT_FUNDS`, `TODOLIST_TODO_NOT_FOUND`)
- **Internal numeric codes** for support/debugging (e.g., `10001`, `20001`)
- **Unified JSON format** for API responses
- **HTTP status mapping** per error variant
- **`define_domain_errors!` macro** for zero-boilerplate error domain definition

## Summary of Changes

### Before (Legacy)

```rust
use cqrs_rust_lib::Aggregate;

#[async_trait::async_trait]
impl Aggregate for MyAggregate {
    type Error = std::io::Error;

    fn error(_status: StatusCode, details: &str) -> Self::Error {
        std::io::Error::other(details.to_string())
    }
    // ...
}

// In command handlers, errors are generic strings:
async fn handle_update(&self, command: ...) -> Result<Vec<Self::Event>, Self::Error> {
    if invalid {
        return Err(std::io::Error::other("Something went wrong"));
    }
    // ...
}
```

### After (New)

```rust
use cqrs_rust_lib::{Aggregate, CqrsError, CqrsErrorCode, define_domain_errors};

// 1. Define domain errors
define_domain_errors! {
    domain: "my_domain",
    prefix: 10,
    errors: {
        InvalidInput => (1, StatusCode::BAD_REQUEST, "INVALID_INPUT"),
        NotAllowed   => (2, StatusCode::FORBIDDEN, "NOT_ALLOWED"),
        ResourceGone => (3, StatusCode::GONE, "RESOURCE_GONE"),
    }
}

// 2. Implement From<ErrorCode> for CqrsError
impl From<ErrorCode> for CqrsError {
    fn from(e: ErrorCode) -> Self {
        e.error(e.to_string())
    }
}

// 3. Use CqrsError as the aggregate error type
#[async_trait::async_trait]
impl Aggregate for MyAggregate {
    type Error = CqrsError;

    fn error(status: StatusCode, details: &str) -> Self::Error {
        CqrsError::from_status(status, details)
    }
    // ...
}

// 4. In command handlers, return structured domain errors:
async fn handle_update(&self, command: ...) -> Result<Vec<Self::Event>, Self::Error> {
    if invalid {
        return Err(ErrorCode::InvalidInput.error("Field 'name' is required"));
    }
    // ...
}
```

## Step-by-Step Migration

### Step 1: Add `thiserror` Dependency

The `define_domain_errors!` macro uses `thiserror` internally. Add it to your `Cargo.toml`:

```toml
[dependencies]
thiserror = "2"
```

### Step 2: Create an Error Module

Create a file `errors.rs` next to your aggregate (e.g., `src/my_domain/errors.rs`):

```rust
use cqrs_rust_lib::{define_domain_errors, CqrsError, CqrsErrorCode};
use http::StatusCode;

define_domain_errors! {
    domain: "my_domain",
    prefix: 10,                // Unique prefix for your domain (0-65)
    errors: {
        //   Variant       => (index, HTTP status,          display string)
        InvalidInput   => (1, StatusCode::BAD_REQUEST,   "INVALID_INPUT"),
        NotAllowed     => (2, StatusCode::FORBIDDEN,     "NOT_ALLOWED"),
        ResourceGone   => (3, StatusCode::GONE,          "RESOURCE_GONE"),
    }
}

impl From<ErrorCode> for CqrsError {
    fn from(e: ErrorCode) -> Self {
        e.error(e.to_string())
    }
}
```

**Key parameters:**
- `domain`: String identifier for the domain (used in JSON `domain` field)
- `prefix`: Unique numeric prefix (0-65). Internal codes = `prefix * 1000 + index`
- `errors`: Variants with `(index, StatusCode, display_string)`

Register the module in your `mod.rs`:

```rust
pub mod errors;
```

### Step 3: Update the Aggregate Error Type

**Before:**
```rust
impl Aggregate for MyAggregate {
    type Error = std::io::Error;

    fn error(_status: StatusCode, details: &str) -> Self::Error {
        std::io::Error::other(details.to_string())
    }
}
```

**After:**
```rust
use cqrs_rust_lib::{CqrsError, CqrsErrorCode};

impl Aggregate for MyAggregate {
    type Error = CqrsError;

    fn error(status: StatusCode, details: &str) -> Self::Error {
        CqrsError::from_status(status, details)
    }
}
```

### Step 4: Add Domain Validation in Command Handlers

Replace generic errors with domain-specific ones:

**Before:**
```rust
async fn handle_update(&self, command: UpdateCommand, ...) -> Result<Vec<Self::Event>, Self::Error> {
    match command {
        UpdateCommand::Withdraw { amount } => {
            // No validation, or generic error:
            Ok(vec![Events::Withdrawn { amount }])
        }
    }
}
```

**After:**
```rust
use super::errors::ErrorCode;

async fn handle_update(&self, command: UpdateCommand, ...) -> Result<Vec<Self::Event>, Self::Error> {
    match command {
        UpdateCommand::Withdraw { amount } => {
            if self.balance < amount {
                return Err(ErrorCode::InsufficientFunds.error(
                    format!("Cannot withdraw {}, balance is {}", amount, self.balance)
                ));
            }
            Ok(vec![Events::Withdrawn { amount }])
        }
    }
}
```

### Step 5: Update Imports

Update your aggregate file imports:

**Before:**
```rust
use cqrs_rust_lib::{Aggregate, CommandHandler, CqrsContext};
use std::io::ErrorKind;
```

**After:**
```rust
use cqrs_rust_lib::{Aggregate, CommandHandler, CqrsContext, CqrsError, CqrsErrorCode};
use super::errors::ErrorCode;
```

### Step 6: Verify

```bash
cargo build
cargo test
```

## Complete Examples

See the example projects for complete implementations:

- **Bank** (`example/bank/src/account/errors.rs`): Domain errors for insufficient funds, invalid amounts, account closed
- **TodoList** (`example/todolist/src/todolist/errors.rs`): Domain errors for todo not found, already resolved, empty title

## API Response Format

When a domain error is returned, the API response is:

```json
{
  "domain": "account",
  "code": "ACCOUNT_INSUFFICIENT_FUNDS",
  "internalCode": 10001,
  "status": 400,
  "message": "Cannot withdraw 500, balance is 200"
}
```

The `internalCode` is computed as `prefix * 1000 + index` (e.g., `10 * 1000 + 1 = 10001`).

## Prefix Registry

Each domain must have a unique prefix. Here is the convention:

| Prefix | Domain |
|--------|--------|
| 0 | `infrastructure` (reserved) |
| 1 | `generic` (reserved) |
| 2-9 | Reserved for future cqrs-rust-lib use |
| 10+ | Application domains |

## Checklist for Migration

- [ ] Add `thiserror = "2"` to `Cargo.toml`
- [ ] Create `errors.rs` with `define_domain_errors!` macro
- [ ] Register `pub mod errors;` in `mod.rs`
- [ ] Add `impl From<ErrorCode> for CqrsError`
- [ ] Change `type Error = std::io::Error` to `type Error = CqrsError` in `impl Aggregate`
- [ ] Update `fn error(...)` to use `CqrsError::from_status(status, details)`
- [ ] Replace generic errors with `ErrorCode::Variant.error("message")` in command handlers
- [ ] Import `CqrsErrorCode` trait (needed for `.error()` method)
- [ ] Import domain `ErrorCode` in aggregate file
- [ ] Run `cargo build` to verify compilation
- [ ] Run tests to verify behavior

## AI Agent Instructions

For AI agents performing this migration automatically:

### Detection

Search for aggregates using generic error types:

```bash
grep -rn "type Error = std::io::Error" --include="*.rs"
grep -rn "type Error = " --include="*.rs" | grep -v "CqrsError"
```

### Context Gathering

Before migrating, the agent must:

1. **Read the aggregate file** to understand the domain (fields, events, commands)
2. **Identify all command handlers** and their business rules
3. **List existing error sites** (places that return `Err(...)`)
4. **Choose a unique prefix** (check existing `define_domain_errors!` for used prefixes)

### Transformation Algorithm

1. **Create `errors.rs`** in the same module as the aggregate:
   - Choose a `domain` name matching the aggregate context (lowercase, e.g., `"account"`, `"todolist"`)
   - Choose a unique `prefix` (10+ for application domains)
   - Define error variants based on the domain's business rules:
     - Validation errors → `StatusCode::BAD_REQUEST`
     - Not found errors → `StatusCode::NOT_FOUND`
     - Conflict/duplicate errors → `StatusCode::CONFLICT`
     - State errors (closed, expired) → `StatusCode::GONE` or `StatusCode::BAD_REQUEST`
   - Add `impl From<ErrorCode> for CqrsError`

2. **Update `mod.rs`** to register the error module:
   ```rust
   pub mod errors;
   ```

3. **Update the aggregate**:
   - Change `type Error = std::io::Error``type Error = CqrsError`
   - Change `fn error(...)``CqrsError::from_status(status, details)`
   - Add imports: `CqrsError`, `CqrsErrorCode`, and `ErrorCode`
   - Remove `use std::io::ErrorKind` or similar imports

4. **Add domain validation** in command handlers:
   - Analyze each command and identify business invariants
   - Add validation checks before event emission
   - Return `ErrorCode::Variant.error("descriptive message")` for violations
   - Include relevant context in error messages (IDs, amounts, etc.)

5. **Update `Cargo.toml`**:
   - Add `thiserror = "2"` to dependencies

### Error Variant Design Guidelines

For the AI agent choosing error variants:

- **Name variants after the business rule violation**, not the HTTP status (e.g., `InsufficientFunds` not `BadRequest`)
- **One variant per distinct business error** (don't reuse a generic variant for different rules)
- **Error messages should include context**: IDs, values, limits
- **Common patterns**:
  - `EntityNotFound``NOT_FOUND` (404)
  - `InvalidField` / `EmptyField``BAD_REQUEST` (400)
  - `EntityAlreadyExists` / `DuplicateSlug``CONFLICT` (409)
  - `EntityClosed` / `EntityExpired``GONE` (410) or `BAD_REQUEST` (400)
  - `InsufficientX` / `LimitExceeded``BAD_REQUEST` (400)
  - `NotAllowed` / `OperationForbidden``FORBIDDEN` (403)

### Validation

After transformation:

1. `cargo build` must succeed
2. `cargo test` must pass
3. Verify JSON response format by inspecting the error serialization
4. Check that all error variants have unique `(prefix, index)` pairs

### Example Prompt for AI Migration

```
Migrate the aggregate in `src/my_domain/aggregate.rs` from `std::io::Error` to domain-specific
`CqrsError` codes:

1. Read the aggregate to understand the domain (fields, events, commands, business rules)
2. Create `src/my_domain/errors.rs` using `define_domain_errors!` macro with prefix N
3. Register `pub mod errors;` in `src/my_domain/mod.rs`
4. Update the aggregate:
   - `type Error = CqrsError`
   - `fn error(status, details) -> CqrsError::from_status(status, details)`
   - Add domain validation in command handlers
5. Add `thiserror = "2"` to Cargo.toml if not present
6. Verify with `cargo build && cargo test`
```