Search

Lib.rs

› Email | Development tools › Debugging › Cruxi
#transport-adapter #cruxi #tonic #axum #logging #status-code #grpc #mapper #web-framework

cruxi-adapters

Transport adapters for Cruxi framework (Axum, Tonic)

Owned by ioneyed.

  • Install
  • API reference
  • GitLab repo (cruxiple)

2 unstable releases

Uses new Rust 2024

0.2.0 Apr 27, 2026
0.1.0 Apr 9, 2026

#235 in Email

MIT license

230KB
4K SLoC

cruxi-adapters

Transport adapters that bridge web frameworks to cruxi context and handlers.

Purpose

This crate provides optional adapters for:

  • axum feature: request extraction + handler bridge
  • tonic feature: 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_mapping
  • cargo 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

  • axum? axum 0.8
  • axum? cruxi-api
  • axum? cruxi-logging
  • axum? serde+derive
  • cruxi
  • tokio+full
  • optional tonic 0.13
Related: cruxi, cruxi-api, cruxi-authz, cruxi-authz-openfga, cruxi-authz-permify, cruxi-authz-spicedb, cruxi-breaker, cruxi-clock, cruxi-config, cruxi-jwt, cruxi-logging, cruxi-metrics, cruxi-middleware, cruxi-retry, cruxi-skywalking
See also: tower_governor, tonic-prost, tonic-middleware, tonic-web, tonic-async-interceptor, nominal-api, console-api, tonic-build, welog_rs, tinc-cel, ahp-ws

Lib.rs is an unofficial list of Rust/Cargo crates, created by kornelski. It contains data from multiple sources, including heuristics, and manually curated data. Content of this page is not necessarily endorsed by the authors of the crate. This site is not affiliated with nor endorsed by the Rust Project. If something is missing or incorrect, please file a bug.