2 unstable releases
Uses new Rust 2024
| 0.2.0 | Apr 27, 2026 |
|---|---|
| 0.1.0 | Apr 9, 2026 |
#235 in Email
230KB
4K
SLoC
cruxi-adapters
Transport adapters that bridge web frameworks to cruxi context and handlers.
Purpose
This crate provides optional adapters for:
axumfeature: request extraction + handler bridgetonicfeature: gRPC context extraction + error conversion
Usage
Enable only the transport you need:
cruxi-adapters = { version = "0.1", features = ["axum"] }cruxi-adapters = { version = "0.1", features = ["tonic"] }
Example (Axum, Typed Error Mapping)
use axum::{Json, http::StatusCode};
use cruxi_adapters::axum::{
ApiError, AxumErrorResponse, CruxiContext, ErrorCause, handle_with_mapper,
};
use cruxi_logging::Logger;
use std::sync::Arc;
#[derive(Debug)]
enum CreateUserError {
ValidationFailed { issues: Vec<FieldIssue> },
FanoutFailed { failures: Vec<ProviderFailure> },
Conflict { email: String },
}
#[derive(Debug)]
struct FieldIssue {
code: &'static str,
field: &'static str,
reason: &'static str,
}
#[derive(Debug)]
struct ProviderFailure {
code: &'static str,
destination: &'static str,
reason: String,
}
impl std::fmt::Display for CreateUserError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ValidationFailed { .. } => write!(f, "invalid input"),
Self::FanoutFailed { .. } => write!(f, "fanout transaction failed"),
Self::Conflict { .. } => write!(f, "user already exists"),
}
}
}
impl std::error::Error for CreateUserError {}
impl CreateUserError {
fn status_code(&self) -> StatusCode {
match self {
Self::ValidationFailed { .. } => StatusCode::BAD_REQUEST,
Self::FanoutFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
Self::Conflict { .. } => StatusCode::CONFLICT,
}
}
fn to_api_error(&self, instance: uuid::Uuid) -> ApiError {
match self {
Self::ValidationFailed { issues } => ApiError::new(
"USER-CREATE-000400",
"Validation failed",
"One or more fields are invalid",
)
.with_instance(instance)
.with_causes(issues.iter().map(|issue| {
ErrorCause::new(
issue.code,
format!("Invalid field '{}'", issue.field),
issue.reason,
)
.with_instance(instance)
})),
Self::FanoutFailed { failures } => ApiError::new(
"USER-CREATE-000500",
"Transaction failed",
"One or more provider destinations failed",
)
.with_instance(instance)
.with_causes(failures.iter().map(|failure| {
ErrorCause::new(
failure.code,
format!("Provider '{}'", failure.destination),
failure.reason.clone(),
)
.with_instance(instance)
})),
Self::Conflict { .. } => ApiError::new(
"USER-CREATE-000409",
"Conflict",
"A user with this identity already exists",
)
.with_instance(instance),
}
}
}
async fn create_user(
ctx: CruxiContext,
body: Json<CreateUserRequest>,
) -> Result<Json<User>, AxumErrorResponse> {
let logger = ctx.context().get::<Arc<dyn Logger>>().cloned();
handle_with_mapper(ctx, body, my_cruxi_handler(), |err| {
let instance = uuid::Uuid::now_v7();
if let Some(logger) = &logger {
logger.error(
"create_user failed",
&[("instance", &instance), ("error", err)],
);
}
AxumErrorResponse::new(err.status_code(), err.to_api_error(instance))
})
.await
}
Use handle(...) only for unexpected/internal failures. It intentionally
collapses all handler errors into a generic internal response (500 + canonical
error body).
Use handle_with_mapper(...) for domain/business failures so status and error
code mapping is explicit and transport-appropriate.
When your handler error type is cruxi::CodedError, you can use the built-in
default mapper:
use cruxi_adapters::axum::{handle_with_mapper, map_coded_error};
// ...
// handle_with_mapper(ctx, body, handler, map_coded_error).await
For gRPC, follow the same rule:
- Use
into_status(...)only for unexpected/internal failures (generic internal status). - Use
into_status_with_mapper(...)for domain/business failures so gRPC code/message mapping is explicit.
For CodedError, use the built-in default mapper:
use cruxi_adapters::tonic::{into_status_with_mapper, map_coded_error};
// ...
// into_status_with_mapper(coded_error, map_coded_error)
Runnable examples:
cargo run -p cruxi-adapters --features axum --example default_axum_coded_error_mappingcargo run -p cruxi-adapters --features tonic --example default_tonic_coded_error_mapping
Example (Tonic, Typed Error Mapping)
use cruxi_adapters::tonic::into_status_with_mapper;
use tonic::{Code, Status};
#[derive(Debug)]
enum LookupError {
UserNotFound,
Unauthorized,
}
impl std::fmt::Display for LookupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UserNotFound => write!(f, "user not found"),
Self::Unauthorized => write!(f, "access denied"),
}
}
}
impl std::error::Error for LookupError {}
fn map_lookup_error(err: &LookupError) -> Status {
match err {
LookupError::UserNotFound => Status::new(Code::NotFound, "USER-LOOKUP-000404"),
LookupError::Unauthorized => Status::new(Code::Unauthenticated, "AUTHZ-CHECK-000401"),
}
}
// Explicit domain/business mapping path:
let status = into_status_with_mapper(LookupError::UserNotFound, map_lookup_error);
assert_eq!(status.code(), Code::NotFound);
Dependencies
~2.1–5.5MB
~81K SLoC