artur 0.2.0

Universal config-driven Rust HTTP gateway and package orchestrator
Documentation
use axum::{
    Json,
    http::{HeaderMap, HeaderValue, StatusCode},
    response::IntoResponse,
};
use serde::Serialize;

#[derive(Debug, thiserror::Error)]
pub enum ArturError {
    #[error("configuration error: {0}")]
    Config(String),
    #[error("request error: {0}")]
    Request(String),
    #[error("not found: {0}")]
    NotFound(String),
    #[error("process error: {0}")]
    Process(String),
    #[error("store error: {0}")]
    Store(String),
    #[error("forbidden: {0}")]
    Forbidden(String),
    #[error("payment required: {0}")]
    PaymentRequired(String),
    #[error("too many requests: {0}")]
    TooManyRequests(String),
    #[error("payload too large: {0}")]
    PayloadTooLarge(String),
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("http client error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("toml parse error: {0}")]
    Toml(#[from] toml::de::Error),
    #[error("json error: {0}")]
    Json(#[from] serde_json::Error),
    #[error("sqlite error: {0}")]
    Sqlite(#[from] rusqlite::Error),
}

pub type Result<T> = std::result::Result<T, ArturError>;

#[derive(Debug, Serialize)]
pub struct ErrorBody {
    pub error: String,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub x402_version: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub accepts: Option<serde_json::Value>,
}

impl ArturError {
    pub fn status(&self) -> StatusCode {
        match self {
            Self::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
            Self::Request(_) => StatusCode::BAD_REQUEST,
            Self::NotFound(_) => StatusCode::NOT_FOUND,
            Self::Process(_) => StatusCode::BAD_GATEWAY,
            Self::Store(_) => StatusCode::BAD_GATEWAY,
            Self::Forbidden(_) => StatusCode::FORBIDDEN,
            Self::PaymentRequired(_) => StatusCode::PAYMENT_REQUIRED,
            Self::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS,
            Self::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
            Self::Io(_) => StatusCode::INTERNAL_SERVER_ERROR,
            Self::Http(_) => StatusCode::BAD_GATEWAY,
            Self::Toml(_) => StatusCode::BAD_REQUEST,
            Self::Json(_) => StatusCode::BAD_REQUEST,
            Self::Sqlite(_) => StatusCode::BAD_GATEWAY,
        }
    }

    pub fn code(&self) -> &'static str {
        match self {
            Self::Config(_) => "config_error",
            Self::Request(_) => "bad_request",
            Self::NotFound(_) => "not_found",
            Self::Process(_) => "process_error",
            Self::Store(_) => "store_error",
            Self::Forbidden(_) => "forbidden",
            Self::PaymentRequired(_) => "payment_required",
            Self::TooManyRequests(_) => "too_many_requests",
            Self::PayloadTooLarge(_) => "payload_too_large",
            Self::Io(_) => "io_error",
            Self::Http(_) => "http_error",
            Self::Toml(_) => "toml_error",
            Self::Json(_) => "json_error",
            Self::Sqlite(_) => "sqlite_error",
        }
    }
}

impl IntoResponse for ArturError {
    fn into_response(self) -> axum::response::Response {
        let status = self.status();
        let is_payment_required = matches!(self, Self::PaymentRequired(_));
        let accepts = if is_payment_required {
            Some(serde_json::json!([{
                "scheme": "x402-native",
                "description": "Submit a valid x-payment header for this request, or top up the referenced space balance and retry.",
                "headers": ["x-payment"]
            }]))
        } else {
            None
        };
        let body = ErrorBody {
            error: self.code().to_string(),
            message: self.to_string(),
            x402_version: is_payment_required.then_some(1),
            accepts: accepts.clone(),
        };
        if is_payment_required {
            let mut headers = HeaderMap::new();
            headers.insert("x402-version", HeaderValue::from_static("1"));
            if let Some(accepts) = accepts
                && let Ok(value) = HeaderValue::from_str(&accepts.to_string())
            {
                headers.insert("payment-required", value);
            }
            (status, headers, Json(body)).into_response()
        } else {
            (status, Json(body)).into_response()
        }
    }
}