k2db-api-server 0.1.1

Single-binary Rust server for the k2db API
// SPDX-FileCopyrightText: 2026 Alexander R. Croft
// SPDX-License-Identifier: MIT

use axum::Json;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use k2db::{K2DbError, ServiceError as K2ServiceError};
use k2db_api_contract::ProblemDetailsPayload;

#[derive(Debug, Clone)]
pub struct ApiError {
    status: StatusCode,
    payload: ProblemDetailsPayload,
}

impl ApiError {
    pub fn new(
        status: StatusCode,
        title: impl Into<String>,
        detail: impl Into<String>,
        trace: impl Into<Option<String>>,
    ) -> Self {
        let title = title.into();
        Self {
            status,
            payload: ProblemDetailsPayload {
                type_uri: Some(format!("urn:service-error:{title}")),
                title: Some(title),
                status: Some(status.as_u16()),
                detail: Some(detail.into()),
                trace: trace.into(),
                chain: None,
                extra: serde_json::Map::new(),
            },
        }
    }

    pub fn bad_request(detail: impl Into<String>, trace: &'static str) -> Self {
        Self::new(StatusCode::BAD_REQUEST, "bad_request", detail, Some(trace.to_owned()))
    }

    pub fn unauthorized(detail: impl Into<String>, trace: &'static str) -> Self {
        Self::new(StatusCode::UNAUTHORIZED, "unauthorized", detail, Some(trace.to_owned()))
    }

    pub fn forbidden(detail: impl Into<String>, trace: &'static str) -> Self {
        Self::new(StatusCode::FORBIDDEN, "forbidden", detail, Some(trace.to_owned()))
    }

    pub fn internal(detail: impl Into<String>, trace: &'static str) -> Self {
        Self::new(
            StatusCode::INTERNAL_SERVER_ERROR,
            "service_error",
            detail,
            Some(trace.to_owned()),
        )
    }

    pub fn service_unavailable(detail: impl Into<String>, trace: &'static str) -> Self {
        Self::new(
            StatusCode::SERVICE_UNAVAILABLE,
            "service_unavailable",
            detail,
            Some(trace.to_owned()),
        )
    }

    pub fn from_k2db(error: K2DbError) -> Self {
        let status = match error.service_error {
            K2ServiceError::BadRequest => StatusCode::BAD_REQUEST,
            K2ServiceError::NotFound => StatusCode::NOT_FOUND,
            K2ServiceError::ConfigurationError => StatusCode::INTERNAL_SERVER_ERROR,
            K2ServiceError::AlreadyExists => StatusCode::CONFLICT,
            K2ServiceError::ValidationError => StatusCode::BAD_REQUEST,
            K2ServiceError::BadGateway => StatusCode::BAD_GATEWAY,
            K2ServiceError::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
            K2ServiceError::SystemError => StatusCode::INTERNAL_SERVER_ERROR,
        };

        let title = match error.service_error {
            K2ServiceError::BadRequest => "bad_request",
            K2ServiceError::NotFound => "not_found",
            K2ServiceError::ConfigurationError => "configuration_error",
            K2ServiceError::AlreadyExists => "already_exists",
            K2ServiceError::ValidationError => "validation_error",
            K2ServiceError::BadGateway => "bad_gateway",
            K2ServiceError::ServiceUnavailable => "service_unavailable",
            K2ServiceError::SystemError => "service_error",
        };

        Self::new(status, title, error.message, error.key)
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let mut response = (self.status, Json(self.payload)).into_response();
        response.headers_mut().insert(
            header::CONTENT_TYPE,
            HeaderValue::from_static("application/problem+json"),
        );
        response
    }
}