# 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 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 {}
assert_eq!(result.ok().as_deref(), Some("user:42"));
```