ts-webapi 0.4.11

Library for my web API projects
Documentation
//! A standardised error response from the API.

use core::panic::Location;

use bytes::Bytes;
use http::{HeaderValue, Response, StatusCode, header};
use serde::{Deserialize, Serialize};
use ts_error::Report;

/// An error response from a route handler.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ErrorResponse {
    /// A bad request response.
    BadRequest(BadRequest),
    /// A not found response.
    NotFound,
    /// An internal server error response.
    InternalServerError,
}

impl ErrorResponse {
    /// Create a new not found response.
    pub fn not_found() -> Self {
        Self::NotFound
    }

    /// Create a new internal server error response.
    pub fn internal_server_error() -> Self {
        Self::InternalServerError
    }

    /// Create a basic bad request.
    pub fn bad_request<S1: ToString, S2: ToString>(pointer: S1, detail: S2) -> Self {
        Self::BadRequest(BadRequest::basic(pointer, detail))
    }

    /// Convert the bad request into a response.
    ///
    /// ## Panics
    /// * If a `BadRequest` has no problems.
    /// * If the response built is invalid.
    #[track_caller]
    pub fn into_response<B: From<Bytes>>(self) -> Response<B> {
        match self {
            Self::BadRequest(bad_request) => bad_request.into_response(),
            Self::NotFound => Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(B::from(Bytes::new()))
                .expect("response should be valid"),
            Self::InternalServerError => Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(B::from(Bytes::new()))
                .expect("response should be valid"),
        }
    }
}

impl From<BadRequest> for ErrorResponse {
    fn from(value: BadRequest) -> Self {
        Self::BadRequest(value)
    }
}

impl<E: core::error::Error> From<E> for ErrorResponse {
    #[track_caller]
    fn from(value: E) -> Self {
        let error = Report::new(value);
        let location = Location::caller();
        log::error!("[{location}] {error}");
        Self::InternalServerError
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// A problem detailing part of the error response.
pub struct Problem {
    /// A JSON path that identifies the part of the request that was the cause of the problem.
    pub pointer: String,
    /// A human-readable explanation specific to this occurrence of the problem.
    pub detail: String,
}
impl Problem {
    /// Create a new problem from a pointer and some details.
    pub fn new<S1: ToString, S2: ToString>(pointer: S1, detail: S2) -> Self {
        Self {
            pointer: pointer.to_string(),
            detail: detail.to_string(),
        }
    }
}

/// JSON payload for a bad request.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BadRequest {
    /// The problems for the bad request.
    pub problems: Vec<Problem>,
}

impl BadRequest {
    /// A basic, single problem bad request.
    pub fn basic<S1: ToString, S2: ToString>(pointer: S1, detail: S2) -> Self {
        Self {
            problems: vec![Problem::new(pointer, detail)],
        }
    }

    /// An empty bad request.
    pub fn new() -> Self {
        Self { problems: vec![] }
    }

    /// Add a problem to the bad request.
    pub fn add_problem(&mut self, problem: Problem) {
        self.problems.push(problem);
    }

    /// If the
    pub fn has_problems(&self) -> bool {
        !self.problems.is_empty()
    }

    /// Convert the bad request into a response.
    ///
    /// ## Panics
    /// * If the bad request has no problems
    #[track_caller]
    pub fn into_response<B: From<Bytes>>(self) -> Response<B> {
        assert!(
            self.has_problems(),
            "{:?} An empty bad request cannot be returned",
            Location::caller()
        );

        let json =
            serde_json::to_string(&self).expect("error response should always be serializable");

        Response::builder()
            .status(StatusCode::BAD_REQUEST)
            .header(
                header::CONTENT_TYPE,
                HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
            )
            .body(B::from(Bytes::from(json)))
            .expect("error response should produce a valid response")
    }
}

#[cfg(feature = "axum")]
impl axum::response::IntoResponse for BadRequest {
    fn into_response(self) -> axum::response::Response {
        self.into_response::<axum::body::Body>().into_response()
    }
}

#[cfg(feature = "axum")]
impl axum::response::IntoResponse for ErrorResponse {
    fn into_response(self) -> axum::response::Response {
        self.into_response()
    }
}