masterror 0.27.3

Application error types and response mapping
Documentation
// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
//
// SPDX-License-Identifier: MIT

//! Transport mapping descriptors generated by `#[derive(Masterror)]`.
//!
//! The derive macro produces compile-time tables describing how each domain
//! error maps to transport-specific representations. Use these helpers to
//! integrate with HTTP, gRPC or RFC 7807 problem+json responses without
//! duplicating per-variant logic.

use crate::{AppCode, AppErrorKind};

/// HTTP mapping for a domain error.
///
/// Stores the stable public [`AppCode`] and semantic [`AppErrorKind`]. The
/// HTTP status code can be derived from the kind via
/// [`AppErrorKind::http_status`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HttpMapping {
    code: AppCode,
    kind: AppErrorKind
}

impl HttpMapping {
    /// Create a new HTTP mapping entry.
    #[must_use]
    pub const fn new(code: AppCode, kind: AppErrorKind) -> Self {
        Self {
            code,
            kind
        }
    }

    /// Stable machine-readable error code.
    #[must_use]
    pub fn code(&self) -> &AppCode {
        &self.code
    }

    /// Semantic application error category.
    #[must_use]
    pub const fn kind(&self) -> AppErrorKind {
        self.kind
    }

    /// Derive the HTTP status code from the error kind.
    #[must_use]
    pub fn status(&self) -> u16 {
        self.kind.http_status()
    }
}

/// gRPC mapping for a domain error.
///
/// Stores the [`AppCode`], [`AppErrorKind`] and a gRPC status code (as `i32`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrpcMapping {
    code:   AppCode,
    kind:   AppErrorKind,
    status: i32
}

impl GrpcMapping {
    /// Create a new gRPC mapping entry.
    #[must_use]
    pub const fn new(code: AppCode, kind: AppErrorKind, status: i32) -> Self {
        Self {
            code,
            kind,
            status
        }
    }

    /// Stable machine-readable error code.
    #[must_use]
    pub fn code(&self) -> &AppCode {
        &self.code
    }

    /// Semantic application error category.
    #[must_use]
    pub const fn kind(&self) -> AppErrorKind {
        self.kind
    }

    /// gRPC status code (matching `tonic::Code` discriminants).
    #[must_use]
    pub const fn status(&self) -> i32 {
        self.status
    }
}

/// RFC 7807 problem+json mapping.
///
/// Associates an error with the [`AppCode`], [`AppErrorKind`] and a canonical
/// problem `type` URI.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProblemMapping {
    code:   AppCode,
    kind:   AppErrorKind,
    r#type: &'static str
}

impl ProblemMapping {
    /// Create a new problem+json mapping entry.
    #[must_use]
    pub const fn new(code: AppCode, kind: AppErrorKind, type_uri: &'static str) -> Self {
        Self {
            code,
            kind,
            r#type: type_uri
        }
    }

    /// Stable machine-readable error code.
    #[must_use]
    pub fn code(&self) -> &AppCode {
        &self.code
    }

    /// Semantic application error category.
    #[must_use]
    pub const fn kind(&self) -> AppErrorKind {
        self.kind
    }

    /// Canonical problem `type` URI.
    #[must_use]
    pub const fn type_uri(&self) -> &'static str {
        self.r#type
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn http_mapping_new_creates_mapping() {
        let mapping = HttpMapping::new(AppCode::NotFound, AppErrorKind::NotFound);
        assert_eq!(mapping.code(), &AppCode::NotFound);
        assert_eq!(mapping.kind(), AppErrorKind::NotFound);
    }

    #[test]
    fn http_mapping_status_derives_from_kind() {
        let mapping = HttpMapping::new(AppCode::BadRequest, AppErrorKind::BadRequest);
        assert_eq!(mapping.status(), 400);
    }

    #[test]
    fn http_mapping_clone_works() {
        let mapping = HttpMapping::new(AppCode::Internal, AppErrorKind::Internal);
        let cloned = mapping.clone();
        assert_eq!(mapping, cloned);
    }

    #[test]
    fn http_mapping_debug_works() {
        let mapping = HttpMapping::new(AppCode::NotFound, AppErrorKind::NotFound);
        let debug = format!("{:?}", mapping);
        assert!(debug.contains("HttpMapping"));
    }

    #[test]
    fn grpc_mapping_new_creates_mapping() {
        let mapping = GrpcMapping::new(AppCode::NotFound, AppErrorKind::NotFound, 5);
        assert_eq!(mapping.code(), &AppCode::NotFound);
        assert_eq!(mapping.kind(), AppErrorKind::NotFound);
        assert_eq!(mapping.status(), 5);
    }

    #[test]
    fn grpc_mapping_clone_works() {
        let mapping = GrpcMapping::new(AppCode::Internal, AppErrorKind::Internal, 13);
        let cloned = mapping.clone();
        assert_eq!(mapping, cloned);
    }

    #[test]
    fn grpc_mapping_debug_works() {
        let mapping = GrpcMapping::new(AppCode::BadRequest, AppErrorKind::BadRequest, 3);
        let debug = format!("{:?}", mapping);
        assert!(debug.contains("GrpcMapping"));
    }

    #[test]
    fn problem_mapping_new_creates_mapping() {
        let mapping = ProblemMapping::new(
            AppCode::NotFound,
            AppErrorKind::NotFound,
            "https://example.com/errors/not-found"
        );
        assert_eq!(mapping.code(), &AppCode::NotFound);
        assert_eq!(mapping.kind(), AppErrorKind::NotFound);
        assert_eq!(mapping.type_uri(), "https://example.com/errors/not-found");
    }

    #[test]
    fn problem_mapping_clone_works() {
        let mapping = ProblemMapping::new(
            AppCode::Internal,
            AppErrorKind::Internal,
            "https://example.com/errors/internal"
        );
        let cloned = mapping.clone();
        assert_eq!(mapping, cloned);
    }

    #[test]
    fn problem_mapping_debug_works() {
        let mapping = ProblemMapping::new(
            AppCode::BadRequest,
            AppErrorKind::BadRequest,
            "https://example.com/errors/bad-request"
        );
        let debug = format!("{:?}", mapping);
        assert!(debug.contains("ProblemMapping"));
    }
}