nido-svc-common 0.1.0-alpha.1

Shared health, error, OpenAPI, SSE, and middleware primitives for nido-*-svc crates
Documentation
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! RFC 7807 problem-JSON error type for nido-*-svc crates.

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde::Serialize;

/// RFC 7807 problem JSON body.
#[derive(Debug, Clone, Serialize)]
pub struct ProblemJson {
    #[serde(rename = "type")]
    pub type_url: String,
    pub title: String,
    pub status: u16,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub instance: Option<String>,
}

impl ProblemJson {
    fn new(type_suffix: &str, title: &str, status: u16) -> Self {
        Self {
            type_url: format!("https://nido.local/errors/{type_suffix}"),
            title: title.to_owned(),
            status,
            detail: None,
            instance: None,
        }
    }

    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
        self.detail = Some(detail.into());
        self
    }

    pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
        self.instance = Some(instance.into());
        self
    }
}

/// Unified service error.  Implements `IntoResponse` so handlers can return
/// `Result<_, NidoSvcError>` directly.
#[derive(Debug, thiserror::Error)]
pub enum NidoSvcError {
    #[error("not found: {0}")]
    NotFound(String),
    #[error("invalid argument: {0}")]
    InvalidArgument(String),
    #[error("unauthorized")]
    Unauthorized,
    #[error("internal error: {0}")]
    Internal(#[from] anyhow::Error),
}

impl NidoSvcError {
    fn to_problem(&self) -> (StatusCode, ProblemJson) {
        match self {
            NidoSvcError::NotFound(msg) => (
                StatusCode::NOT_FOUND,
                ProblemJson::new("not-found", "Not Found", 404).with_detail(msg.clone()),
            ),
            NidoSvcError::InvalidArgument(msg) => (
                StatusCode::BAD_REQUEST,
                ProblemJson::new("invalid-argument", "Bad Request", 400).with_detail(msg.clone()),
            ),
            NidoSvcError::Unauthorized => (
                StatusCode::UNAUTHORIZED,
                ProblemJson::new("unauthorized", "Unauthorized", 401),
            ),
            NidoSvcError::Internal(e) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                ProblemJson::new("internal-error", "Internal Server Error", 500)
                    .with_detail(e.to_string()),
            ),
        }
    }
}

impl IntoResponse for NidoSvcError {
    fn into_response(self) -> Response {
        let (status, body) = self.to_problem();
        (status, Json(body)).into_response()
    }
}