cruxi 0.2.0

Minimal, transport-agnostic hexagonal architecture framework
Documentation
# cruxi

A transport-agnostic core framework for building hexagonal Rust services.

## Purpose

`cruxi` defines the core ports and building blocks for a 4-layer architecture:

- `Handler`: inbound adapter boundary
- `Service`: application orchestration
- `Repository`: domain persistence boundary
- `Provider`: infrastructure boundary
- `Validator`: reusable validation abstraction
- `Context`: request-scoped metadata, cancellation, deadlines


```
┌──────────────────────────────────────┐
│ INBOUND TRANSPORTS                   │
│ (HTTP, gRPC, MQTT, TCP)              │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ HANDLER (Inbound Adapter)            │
│ • Receives requests                  │
│ • Validates transport format         │
│ • Delegates to Service               │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ SERVICE (Application Layer)          │
│ • Business logic orchestration       │
│ • Authorization checks               │
│ • Coordinates Repositories           │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ REPOSITORY (Domain Layer)            │
│ • Domain validation                  │
│ • Transactional integrity            │
│ • Calls Providers                    │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ PROVIDER (Infrastructure)            │
│ • Database I/O                       │
│ • HTTP API calls                     │
│ • Message queues                     │
└──────────────────────────────────────┘
```

## Design Principles

    Zero external dependencies in the core (only std + thiserror)
    Generic type safety via trait generics on Req/Resp
    Pattern matching for all control flow and error handling
    No .unwrap() - explicit error handling throughout

## Features

    async - Enables async trait variants (requires async-trait)


## Usage

Use function adapters (`HandlerFn`, `ServiceFn`, `RepositoryFn`, `ProviderFn`, `ValidatorFn`) for quick composition, or implement traits directly for richer behavior.

Enable the `async` feature to use async trait variants (`AsyncHandler`, `AsyncService`, etc.).

### Sync/Async Method Disambiguation

When both sync and async traits are in scope under `all-features`, use
Universal Function Call Syntax (UFCS) style calls to disambiguate method resolution:

```rust
use cruxi::{AsyncHandler, Context, Handler, HandlerFn};

let handler = HandlerFn::new(|_ctx: &Context, req: i32| -> Result<i32, &'static str> {
    Ok(req * 2)
});
let ctx = Context::new();

// Sync call
let sync_result = Handler::handle(&handler, &ctx, 21);
assert_eq!(sync_result.ok(), Some(42));

// Async call
let async_future = AsyncHandler::handle(&handler, &ctx, 21);
drop(async_future);
```

This avoids `multiple applicable items in scope` errors and makes call intent explicit.

### Context done reasons (short example)

Use `done_reason()` to make cancellation/timeout handling explicit:

```rust
use cruxi::{Context, DoneReason};
use std::time::Duration;

let mut cancelled = Context::new();
cancelled.cancel_with_reason(DoneReason::Shutdown);
assert_eq!(cancelled.done_reason(), Some(DoneReason::Shutdown));

let expired = Context::with_timeout(Duration::from_nanos(1));
std::thread::sleep(Duration::from_millis(1));
assert_eq!(expired.done_reason(), Some(DoneReason::DeadlineExceeded));
```

Use `is_cancelled()` when you only want explicit cancellation/shutdown,
not deadline expiry:

```rust
use cruxi::{Context, DoneReason};
use std::time::Duration;

let mut cancelled = Context::new();
cancelled.cancel_with_reason(DoneReason::Shutdown);
assert!(cancelled.is_cancelled());

let expired = Context::with_timeout(Duration::from_nanos(1));
std::thread::sleep(Duration::from_millis(1));
assert!(!expired.is_cancelled());
```

Use `cancellation_handle()` when cancellation must be triggered from a shared owner:

```rust
use cruxi::{Context, DoneReason};

let ctx = Context::new();
let handle = ctx.cancellation_handle();
handle.cancel_with_reason(DoneReason::Shutdown);

assert_eq!(ctx.done_reason(), Some(DoneReason::Shutdown));
assert!(handle.is_done());
assert!(handle.is_cancelled());
assert!(!handle.deadline_exceeded());
```

### Deadline propagation (short example)

Use child contexts to propagate the earliest deadline across layers:

```rust
use cruxi::Context;
use std::time::Duration;

let parent = Context::with_timeout(Duration::from_secs(30));
let child = parent.child_with_timeout(Duration::from_secs(5));

assert!(parent.deadline().is_some());
assert!(child.deadline().is_some());
assert!(child.time_remaining() <= parent.time_remaining());
```

### Typed metadata accessors (short example)

Prefer typed metadata helpers for request context consistency:

```rust
use cruxi::ContextBuilder;

let ctx = ContextBuilder::new()
    .with_request_id("req-900")
    .with_trace_id("trace-900")
    .with_principal("user-42")
    .with_tenant("tenant-7")
    .with_scopes(["tasks:read", "tasks:write"])
    .build();

assert_eq!(ctx.request_id(), Some("req-900"));
assert_eq!(ctx.trace_id(), Some("trace-900"));
assert_eq!(ctx.principal(), Some("user-42"));
assert_eq!(ctx.tenant(), Some("tenant-7"));
assert_eq!(ctx.request_id_value().map(|v| v.as_str()), Some("req-900"));
assert!(ctx.has_scope("tasks:read"));
assert!(!ctx.has_scope("tasks:delete"));
let scopes = ctx
    .scopes_value()
    .map(|value| value.iter().collect::<Vec<_>>())
    .unwrap_or_default();
assert_eq!(scopes, vec!["tasks:read", "tasks:write"]);
```

Typed wrappers also work directly:
`with_request_id(RequestId::new(...))`, `with_tenant(Tenant::new(...))`,
`with_scopes(Scopes::from_values([...]))`.

For scope processing, `Scopes` also exposes:
`iter()` for borrowed traversal and `into_vec()` when ownership is needed.
You can also construct it via `Scopes::from(vec![...])` or
`Scopes::from([\"scope:a\", \"scope:b\"])`.
Identity wrappers expose `into_inner()` when owned `String` extraction is needed.

`Principal` and `Tenant` are intended for authorization and data-isolation
checks in service/repository layers. See runnable example:
`cargo run --example context_identity_scoping`.

### Error mapping contract (short example)

Map transport-agnostic error classes to adapter-specific decisions:

```rust
use cruxi::{CodedError, ErrorClass, ErrorClassMapper, ErrorMappingContext, map_coded_error};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Decision {
    Client,
    Server,
}

struct Mapper;

impl ErrorClassMapper for Mapper {
    type Decision = Decision;

    fn map(&self, context: ErrorMappingContext) -> Self::Decision {
        match context.class {
            ErrorClass::Validation
            | ErrorClass::Authentication
            | ErrorClass::Authorization
            | ErrorClass::NotFound
            | ErrorClass::Conflict
            | ErrorClass::RateLimited => Decision::Client,
            ErrorClass::Timeout | ErrorClass::Unavailable | ErrorClass::Internal | ErrorClass::Unknown => Decision::Server,
        }
    }
}

let err = CodedError::new("TASK-UPDATE-000404").with_class(ErrorClass::NotFound);
assert_eq!(map_coded_error(&err, &Mapper), Decision::Client);
```

## Example

```rust
use cruxi::{Context, Handler, HandlerFn};

#[derive(Clone)]
struct GetUser {
    id: u64,
}

#[derive(Debug)]
struct UserLookupError;

impl std::fmt::Display for UserLookupError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "user lookup failed")
    }
}

impl std::error::Error for UserLookupError {}

let handler: HandlerFn<_, String, UserLookupError> = HandlerFn::new(|_ctx: &Context, req: GetUser| {
    Ok(format!("user:{}", req.id))
});

let result = handler.handle(&Context::new(), GetUser { id: 42 });
assert_eq!(result.ok().as_deref(), Some("user:42"));
```