apollo-errors 0.7.0

Structured error handling with automatic format conversion
Documentation
//! Structured error handling with multi-format output.
//!
//! Define errors once and render them to JSON, GraphQL, HTML, or plain text.
//!
//! # Quick Start
//!
//! ```rust
//! use apollo_errors::{Error, FormatConfig};
//! use miette::Diagnostic;
//!
//! #[derive(Debug, Error, Diagnostic)]
//! pub enum AuthError {
//!     #[error("Invalid credentials for user {username}")]
//!     #[diagnostic(code(auth::invalid_credentials))]
//!     InvalidCredentials {
//!         #[extension]
//!         username: String,
//!     },
//! }
//!
//! let error = AuthError::InvalidCredentials {
//!     username: "alice".to_string(),
//! };
//!
//! let config = FormatConfig::default();
//!
//! // Render to different formats
//! let json = error.to_json(config).unwrap();
//! let graphql = error.to_graphql(config).unwrap();
//! let jsonrpc = error.to_jsonrpc(config).unwrap();
//! let html = error.to_html(config);
//! let text = error.to_text(config);
//! ```
//!
//! # Defining Errors
//!
//! Errors require three derives: `Debug`, `Error`, and `Diagnostic`.
//!
//! ```rust,ignore
//! #[derive(Debug, Error, Diagnostic)]
//! pub enum MyError {
//!     #[error("Something went wrong")]
//!     #[diagnostic(code(service::something_wrong))]
//!     SomethingWrong,
//! }
//! ```
//!
//! # Attributes
//!
//! ## Error Message
//!
//! Use `#[error("...")]` to define the error message. Field interpolation is supported:
//!
//! ```rust,ignore
//! #[error("Failed to connect to {host}:{port}")]
//! ConnectionFailed { host: String, port: u16 },
//! ```
//!
//! ## Error Code
//!
//! Use `#[diagnostic(code(...))]` to define a unique error code. Codes must have
//! at least two `::` separated segments, all lowercase:
//!
//! ```rust,ignore
//! #[diagnostic(code(db::connection_failed))]
//! #[diagnostic(code(auth::invalid_token))]
//! ```
//!
//! ## Extension Fields
//!
//! Mark fields with `#[extension]` to include them in JSON and GraphQL output:
//!
//! ```rust,ignore
//! InvalidPort {
//!     #[extension]
//!     port: u16,
//!     #[extension]
//!     config_file: String,
//! },
//! ```
//!
//! ## HTTP Status
//!
//! Specify an HTTP status code (defaults to 500):
//!
//! ```rust,ignore
//! #[http_status(404)]
//! NotFound,
//! ```
//!
//! ## HTTP Headers
//!
//! Mark fields to be returned as HTTP response headers. Supported types are `u16`, `u32`,
//! `u64`, `i16`, `i32`, `i64`, `bool`, and `HeaderValue`:
//!
//! ```rust,ignore
//! #[http_status(429)]
//! RateLimitExceeded {
//!     #[http_header("Retry-After")]
//!     retry_after: u64,
//!     #[http_header("X-RateLimit-Remaining")]
//!     remaining: u32,
//! },
//! ```
//!
//! Header names are validated at compile time against RFC 7230. Headers are
//! automatically set when using [`tower_http::ErrorLayer`]. For `Option<T>` fields,
//! the header is only included when the value is `Some`.
//!
//! ## JSON-RPC Code
//!
//! Specify a JSON-RPC 2.0 error code (defaults to -32000, "Server error"):
//!
//! ```rust,ignore
//! #[jsonrpc_code(-32602)]
//! InvalidParams { param: String },
//! ```
//!
//! Reserved JSON-RPC codes:
//! - `-32700`: Parse error
//! - `-32600`: Invalid Request
//! - `-32601`: Method not found
//! - `-32602`: Invalid params
//! - `-32603`: Internal error
//! - `-32000` to `-32099`: Server error (available for application use)
//!
//! ## Help Text and URLs
//!
//! Provide additional context for users:
//!
//! ```rust,ignore
//! #[diagnostic(
//!     code(config::invalid),
//!     help("Check your configuration file"),
//!     url("https://docs.example.com/errors/config-invalid")
//! )]
//! ```
//!
//! ## Error Chaining
//!
//! Use `#[source]` to chain errors, or `#[from]` to also generate a `From` impl:
//!
//! ```rust,ignore
//! #[error("Database operation failed")]
//! #[diagnostic(code(db::operation_failed))]
//! DatabaseError {
//!     #[from]
//!     source: std::io::Error,
//! },
//! ```
//!
//! # Dynamic Dispatch
//!
//! Format any `std::error::Error` using the extension traits:
//!
//! ```rust
//! use apollo_errors::{ErrorExt, HeapErrorExt, FormatConfig};
//!
//! fn handle_error(error: Box<dyn std::error::Error + Send + Sync>) {
//!     let json = error.to_json(FormatConfig::default()).unwrap();
//!     println!("{}", json);
//! }
//! ```

// Re-export the derive macro
pub use apollo_errors_derive::Error;

// Re-export miette for user convenience
pub use miette;

// Re-export http crate for Error trait return types
pub use http;

mod catalog;
mod error;
mod ext;
mod html;
mod metadata;
mod registry;

pub use catalog::CatalogErrorEntry as ErrorMetadata;
pub use catalog::CatalogVariantEntry as ErrorVariantMetadata;
pub use catalog::error_catalog;
pub use error::Error;
pub use ext::{ErrorExt, HeapErrorExt};
pub use metadata::{
    CodeCase, CodeMetadata, FieldCase, FieldMetadata as ErrorFieldMetadata, FormatConfig,
};

#[cfg(feature = "tower")]
pub mod tower_http;

#[doc(hidden)]
pub mod private {
    pub use crate::error::{diagnostic_code, diagnostic_help};
    pub use crate::html::HtmlEscaped;
    pub use crate::metadata::*;
    pub use crate::registry::*;
    pub use linkme;
    pub use serde_json;

    /// Default JSON-RPC error code when not specified (-32000 = "Server error")
    pub const DEFAULT_JSONRPC_CODE: i32 = -32000;

    // ToHeaderValue trait for http_header attribute support
    use http::HeaderValue;

    pub trait ToHeaderValue {
        fn to_header_value(&self) -> Option<HeaderValue>;
    }

    impl ToHeaderValue for HeaderValue {
        fn to_header_value(&self) -> Option<HeaderValue> {
            Some(self.clone())
        }
    }

    impl ToHeaderValue for bool {
        fn to_header_value(&self) -> Option<HeaderValue> {
            Some(HeaderValue::from_static(if *self {
                "true"
            } else {
                "false"
            }))
        }
    }

    macro_rules! impl_to_header_value_for_int {
        ($($ty:ty),*) => {
            $(
                impl ToHeaderValue for $ty {
                    fn to_header_value(&self) -> Option<HeaderValue> {
                        Some(HeaderValue::from(*self))
                    }
                }
            )*
        };
    }

    impl_to_header_value_for_int!(u16, u32, u64, i16, i32, i64);

    /// Convert a pre-validated HTTP status u16 to [`http::StatusCode`].
    ///
    /// Every `#[http_status(...)]` value is validated by `apollo-errors-derive` at
    /// macro expansion time, so the conversion always succeeds and the `expect` is
    /// unreachable in practice.
    #[inline]
    pub fn http_status_from_u16(code: u16) -> http::StatusCode {
        http::StatusCode::from_u16(code)
            .expect("BUG: http_status value was not validated at derive macro expansion time")
    }

    /// Converts a source error reference to `&dyn Error + 'static`.
    ///
    /// This trait exists to handle `Box<dyn Error + Send + Sync>` in `#[source]` fields
    /// without requiring the blanket `impl<E: Error> Error for Box<E>` (which requires
    /// `E: Sized` and fails for `dyn Error + Send + Sync`). Method-call auto-deref walks
    /// through `Box<T>` to `T = dyn Error + Send + Sync`, where the specific impls below apply.
    pub trait AsDynError<'a> {
        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a);
    }

    impl<'a, T: ::std::error::Error + 'a> AsDynError<'a> for T {
        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a) {
            self
        }
    }

    impl<'a> AsDynError<'a> for dyn ::std::error::Error + 'a {
        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a) {
            self
        }
    }

    impl<'a> AsDynError<'a> for dyn ::std::error::Error + Send + 'a {
        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a) {
            self
        }
    }

    impl<'a> AsDynError<'a> for dyn ::std::error::Error + Send + Sync + 'a {
        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a) {
            self
        }
    }

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

        #[test]
        fn http_status_from_u16_converts_correctly() {
            assert_eq!(http_status_from_u16(200), http::StatusCode::OK);
            assert_eq!(http_status_from_u16(201), http::StatusCode::CREATED);
            assert_eq!(
                http_status_from_u16(301),
                http::StatusCode::MOVED_PERMANENTLY
            );
            assert_eq!(http_status_from_u16(400), http::StatusCode::BAD_REQUEST);
            assert_eq!(http_status_from_u16(404), http::StatusCode::NOT_FOUND);
            assert_eq!(
                http_status_from_u16(422),
                http::StatusCode::UNPROCESSABLE_ENTITY
            );
            assert_eq!(
                http_status_from_u16(500),
                http::StatusCode::INTERNAL_SERVER_ERROR
            );
            assert_eq!(
                http_status_from_u16(503),
                http::StatusCode::SERVICE_UNAVAILABLE
            );
        }

        #[test]
        #[should_panic(
            expected = "BUG: http_status value was not validated at derive macro expansion time"
        )]
        fn http_status_from_u16_panics_on_invalid_code() {
            http_status_from_u16(99);
        }
    }
}