Skip to main content

alien_error/
lib.rs

1//! Alien-Error – minimal clean version with context-based API.
2//! Provides:
3//!   • `AlienErrorMetadata` trait (implemented by enums via #[derive(AlienError)])
4//!   • `AlienError<T>` container (generic over error type)
5//!   • `.context()` extension method for AlienError Results
6//!   • `.into_alien_error()` for converting std errors
7//!   • `Result<T>` alias
8//!   • OpenAPI schema generation (with `openapi` feature)
9//!   • Axum IntoResponse implementation (with `axum` feature)
10//!
11//! Use `.context(YourError::Variant { ... })` on AlienError Results to wrap errors.
12//! Use `.into_alien_error()` on std::error::Error Results to convert them first.
13//!
14//! ## OpenAPI Schema Generation
15//!
16//! When the `openapi` feature is enabled, the `AlienError` struct implements
17//! `utoipa::ToSchema`, allowing it to be used in OpenAPI documentation:
18//!
19//! ```rust,ignore
20//! use utoipa::OpenApi;
21//! use alien_error::AlienError;
22//!
23//! #[derive(OpenApi)]
24//! #[openapi(components(schemas(AlienError)))]
25//! struct ApiDoc;
26//! ```
27//!
28//! ## Axum Integration
29//!
30//! When the `axum` feature is enabled, `AlienError` implements `axum::response::IntoResponse`,
31//! allowing it to be returned directly from Axum handlers. By default, the `IntoResponse`
32//! implementation uses external response behavior (sanitizes internal errors).
33//!
34//! For different use cases, you can choose between:
35//!
36//! ### External API Responses (Default)
37//! ```rust,ignore
38//! use axum::response::IntoResponse;
39//! use alien_error::{AlienError, AlienErrorData};
40//!
41//! // Default behavior - sanitizes internal errors for security
42//! async fn api_handler() -> Result<String, AlienError<MyError>> {
43//!     Err(AlienError::new(MyError::InternalDatabaseError {
44//!         credentials: "secret".to_string()
45//!     }))
46//! }
47//! // Returns: HTTP 500 with {"code": "GENERIC_ERROR", "message": "Internal server error"}
48//! ```
49//!
50//! ### Explicit External Responses
51//! ```rust,ignore
52//! async fn api_handler() -> impl IntoResponse {
53//!     let error = AlienError::new(MyError::InternalDatabaseError {
54//!         credentials: "secret".to_string()
55//!     });
56//!     error.into_external_response() // Explicitly sanitize
57//! }
58//! ```
59//!
60//! ### Internal Service Communication
61//! ```rust,ignore
62//! async fn internal_handler() -> impl IntoResponse {
63//!     let error = AlienError::new(MyError::InternalDatabaseError {
64//!         credentials: "secret".to_string()
65//!     });
66//!     error.into_internal_response() // Preserve all details
67//! }
68//! // Returns: HTTP 500 with full error details including sensitive information
69//! ```
70
71use std::{error::Error as StdError, fmt};
72
73use serde::{Deserialize, Serialize};
74
75/// Data every public-facing error variant must expose.
76pub trait AlienErrorData {
77    /// Short machine-readable identifier ("NOT_FOUND", "TIMEOUT", …).
78    fn code(&self) -> &'static str;
79    /// Whether the failing operation can be retried.
80    fn retryable(&self) -> bool;
81    /// Whether the error is internal (should not be shown to end users).
82    fn internal(&self) -> bool;
83    /// Human-readable message (defaults to `Display`).
84    fn message(&self) -> String;
85    /// HTTP status code for this error (defaults to 500).
86    fn http_status_code(&self) -> u16 {
87        500
88    }
89    /// Optional diagnostic payload built from struct/enum fields.
90    fn context(&self) -> Option<serde_json::Value> {
91        None
92    }
93
94    /// Whether to inherit the retryable flag from the source error.
95    /// Returns None if this error should inherit from source, Some(value) for explicit value.
96    fn retryable_inherit(&self) -> Option<bool> {
97        Some(self.retryable())
98    }
99
100    /// Whether to inherit the internal flag from the source error.
101    /// Returns None if this error should inherit from source, Some(value) for explicit value.
102    fn internal_inherit(&self) -> Option<bool> {
103        Some(self.internal())
104    }
105
106    /// Whether to inherit the HTTP status code from the source error.
107    /// Returns None if this error should inherit from source, Some(value) for explicit value.
108    fn http_status_code_inherit(&self) -> Option<u16> {
109        Some(self.http_status_code())
110    }
111}
112
113/// A special marker type for generic/standard errors that don't have specific metadata
114#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Default)]
115#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
116pub struct GenericError {
117    pub message: String,
118}
119
120impl std::fmt::Display for GenericError {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "{}", self.message)
123    }
124}
125
126impl StdError for GenericError {}
127
128impl AlienErrorData for GenericError {
129    fn code(&self) -> &'static str {
130        "GENERIC_ERROR"
131    }
132
133    fn retryable(&self) -> bool {
134        false
135    }
136
137    fn internal(&self) -> bool {
138        false
139    }
140
141    fn message(&self) -> String {
142        self.message.clone()
143    }
144
145    fn http_status_code(&self) -> u16 {
146        500
147    }
148}
149
150/// Canonical error container that provides a structured way to represent errors
151/// with rich metadata including error codes, human-readable messages, context,
152/// and chaining capabilities for error propagation.
153///
154/// This struct is designed to be both machine-readable and user-friendly,
155/// supporting serialization for API responses and detailed error reporting
156/// in distributed systems.
157#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
158#[serde(rename_all = "camelCase")]
159#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
160pub struct AlienError<T = GenericError>
161where
162    T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
163{
164    /// A unique identifier for the type of error.
165    ///
166    /// This should be a short, machine-readable string that can be used
167    /// by clients to programmatically handle different error types.
168    /// Examples: "NOT_FOUND", "VALIDATION_ERROR", "TIMEOUT"
169    #[cfg_attr(feature = "openapi", schema(example = "NOT_FOUND", max_length = 128))]
170    pub code: String,
171
172    /// Human-readable error message.
173    ///
174    /// This message should be clear and actionable for developers or end-users,
175    /// providing context about what went wrong and potentially how to fix it.
176    #[cfg_attr(
177        feature = "openapi",
178        schema(example = "Item not found.", max_length = 16384)
179    )]
180    pub message: String,
181
182    /// Additional diagnostic information about the error context.
183    ///
184    /// This optional field can contain structured data providing more details
185    /// about the error, such as validation errors, request parameters that
186    /// caused the issue, or other relevant context information.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    #[cfg_attr(feature = "openapi", schema(nullable = true))]
189    pub context: Option<serde_json::Value>,
190
191    /// Indicates whether the operation that caused the error should be retried.
192    ///
193    /// When `true`, the error is transient and the operation might succeed
194    /// if attempted again. When `false`, retrying the same operation is
195    /// unlikely to succeed without changes.
196    #[cfg_attr(feature = "openapi", schema(default = false))]
197    pub retryable: bool,
198
199    /// Indicates if this is an internal error that should not be exposed to users.
200    ///
201    /// When `true`, this error contains sensitive information or implementation
202    /// details that should not be shown to end-users. Such errors should be
203    /// logged for debugging but replaced with generic error messages in responses.
204    pub internal: bool,
205
206    /// HTTP status code for this error.
207    ///
208    /// Used when converting the error to an HTTP response. If None, falls back to
209    /// the error type's default status code or 500.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    #[cfg_attr(feature = "openapi", schema(minimum = 100, maximum = 599))]
212    pub http_status_code: Option<u16>,
213
214    /// The underlying error that caused this error, creating an error chain.
215    ///
216    /// This allows for proper error propagation and debugging by maintaining
217    /// the full context of how an error occurred through multiple layers
218    /// of an application.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    #[cfg_attr(feature = "openapi", schema(value_type = Option<serde_json::Value>))]
221    pub source: Option<Box<AlienError<GenericError>>>,
222
223    /// The original error for pattern matching
224    #[serde(
225        rename = "_error_for_pattern_matching",
226        skip_serializing_if = "Option::is_none"
227    )]
228    #[cfg_attr(feature = "openapi", schema(ignore))]
229    pub error: Option<T>,
230}
231
232impl<T> AlienError<T>
233where
234    T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
235{
236    /// Create an AlienError from an AlienErrorData implementor
237    pub fn new(meta: T) -> Self {
238        AlienError {
239            code: meta.code().to_string(),
240            message: meta.message(),
241            context: meta.context(),
242            retryable: meta.retryable(),
243            internal: meta.internal(),
244            http_status_code: Some(meta.http_status_code()),
245            source: None,
246            error: Some(meta),
247        }
248    }
249}
250
251impl AlienError<GenericError> {
252    /// Create an AlienError from a standard error
253    pub fn from_std(err: &(dyn StdError + 'static)) -> Self {
254        let generic = GenericError {
255            message: err.to_string(),
256        };
257
258        // Recursively build the source chain
259        let source = err.source().map(|src| Box::new(Self::from_std(src)));
260
261        AlienError {
262            code: generic.code().to_string(),
263            message: generic.message(),
264            context: generic.context(),
265            retryable: generic.retryable(),
266            internal: generic.internal(),
267            http_status_code: Some(generic.http_status_code()),
268            source,
269            error: Some(generic),
270        }
271    }
272}
273
274impl<T> fmt::Display for AlienError<T>
275where
276    T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
277{
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        write!(f, "{}: {}", self.code, self.message)?;
280        fn recurse(
281            e: &AlienError<GenericError>,
282            indent: &str,
283            f: &mut fmt::Formatter<'_>,
284        ) -> fmt::Result {
285            writeln!(f, "{}├─▶ {}: {}", indent, e.code, e.message)?;
286            if let Some(ref src) = e.source {
287                recurse(src, &format!("{}│   ", indent), f)?;
288            }
289            Ok(())
290        }
291        if let Some(ref src) = self.source {
292            writeln!(f)?;
293            recurse(src, "", f)?;
294        }
295        Ok(())
296    }
297}
298
299impl<T> StdError for AlienError<T>
300where
301    T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
302{
303    fn source(&self) -> Option<&(dyn StdError + 'static)> {
304        self.source
305            .as_ref()
306            .map(|e| e.as_ref() as &(dyn StdError + 'static))
307    }
308}
309
310/// Extension trait for adding context to AlienError Results
311pub trait Context<T, E> {
312    /// Add context to an AlienError result, wrapping it with a new error
313    fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
314        self,
315        meta: M,
316    ) -> std::result::Result<T, AlienError<M>>;
317}
318
319// Implementation for AlienError results
320impl<T, E> Context<T, E> for std::result::Result<T, AlienError<E>>
321where
322    E: AlienErrorData + Clone + std::fmt::Debug + Serialize + 'static + Send + Sync,
323{
324    fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
325        self,
326        meta: M,
327    ) -> std::result::Result<T, AlienError<M>> {
328        self.map_err(|err| {
329            let mut new_err = AlienError::new(meta.clone());
330
331            // Check for inheritance and apply source error properties
332            // SAFETY: err.retryable, err.internal, and err.http_status_code are always valid
333            // as they are primitive types (bool, Option<u16>) that cannot be in an invalid state
334            if meta.retryable_inherit().is_none() {
335                new_err.retryable = err.retryable;
336            }
337            if meta.internal_inherit().is_none() {
338                new_err.internal = err.internal;
339            }
340            if meta.http_status_code_inherit().is_none() {
341                new_err.http_status_code = err.http_status_code;
342            }
343
344            // Convert the original typed error to a generic error to maintain the chain
345            let generic_err = AlienError {
346                code: err.code.clone(),
347                message: err.message.clone(),
348                context: err.context.clone(),
349                retryable: err.retryable,
350                internal: err.internal,
351                source: err.source,
352                error: None,
353                http_status_code: err.http_status_code,
354            };
355            new_err.source = Some(Box::new(generic_err));
356            new_err
357        })
358    }
359}
360
361/// Extension trait for adding context directly to AlienError instances
362pub trait ContextError<E> {
363    /// Add context to an AlienError, wrapping it with a new error
364    fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
365        self,
366        meta: M,
367    ) -> AlienError<M>;
368}
369
370// Implementation for AlienError instances
371impl<E> ContextError<E> for AlienError<E>
372where
373    E: AlienErrorData + Clone + std::fmt::Debug + Serialize + 'static + Send + Sync,
374{
375    fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
376        self,
377        meta: M,
378    ) -> AlienError<M> {
379        let mut new_err = AlienError::new(meta.clone());
380
381        // Check for inheritance and apply source error properties
382        // SAFETY: self.retryable, self.internal, and self.http_status_code are always valid
383        // as they are primitive types (bool, Option<u16>) that cannot be in an invalid state
384        if meta.retryable_inherit().is_none() {
385            new_err.retryable = self.retryable;
386        }
387        if meta.internal_inherit().is_none() {
388            new_err.internal = self.internal;
389        }
390        if meta.http_status_code_inherit().is_none() {
391            new_err.http_status_code = self.http_status_code;
392        }
393
394        // Convert the original typed error to a generic error to maintain the chain
395        let generic_err = AlienError {
396            code: self.code.clone(),
397            message: self.message.clone(),
398            context: self.context.clone(),
399            retryable: self.retryable,
400            internal: self.internal,
401            source: self.source,
402            error: None,
403            http_status_code: self.http_status_code,
404        };
405        new_err.source = Some(Box::new(generic_err));
406        new_err
407    }
408}
409
410/// Extension trait for converting standard errors to AlienError
411pub trait IntoAlienError<T> {
412    /// Convert a standard error result into an AlienError result
413    fn into_alien_error(self) -> std::result::Result<T, AlienError<GenericError>>;
414}
415
416impl<T, E> IntoAlienError<T> for std::result::Result<T, E>
417where
418    E: StdError + 'static,
419{
420    fn into_alien_error(self) -> std::result::Result<T, AlienError<GenericError>> {
421        self.map_err(|err| AlienError::from_std(&err as &dyn StdError))
422    }
423}
424
425/// Extension trait for converting standard errors directly to AlienError
426pub trait IntoAlienErrorDirect {
427    /// Convert a standard error into an AlienError
428    fn into_alien_error(self) -> AlienError<GenericError>;
429}
430
431impl<E> IntoAlienErrorDirect for E
432where
433    E: StdError + 'static,
434{
435    fn into_alien_error(self) -> AlienError<GenericError> {
436        AlienError::from_std(&self as &dyn StdError)
437    }
438}
439
440/// Alias for the common `Result` type used throughout an application.
441/// This is now generic over the error type for better type safety.
442pub type Result<T, E = GenericError> = std::result::Result<T, AlienError<E>>;
443
444impl<T> AlienError<T>
445where
446    T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
447{
448    /// Convert this AlienError<T> to AlienError<GenericError> without losing data
449    pub fn into_generic(self) -> AlienError<GenericError> {
450        AlienError {
451            code: self.code,
452            message: self.message,
453            context: self.context,
454            retryable: self.retryable,
455            internal: self.internal,
456            source: self.source,
457            error: None,
458            http_status_code: self.http_status_code,
459        }
460    }
461}
462
463// Re-export the derive macro so users only depend on this crate.
464pub use alien_error_derive::AlienErrorData;
465
466// Conversions for anyhow interoperability
467#[cfg(feature = "anyhow")]
468impl From<anyhow::Error> for AlienError<GenericError> {
469    fn from(err: anyhow::Error) -> AlienError<GenericError> {
470        AlienError::new(GenericError {
471            message: err.to_string(),
472        })
473    }
474}
475
476#[cfg(feature = "anyhow")]
477pub trait IntoAnyhow<T> {
478    /// Convert an AlienError result into an anyhow result
479    fn into_anyhow(self) -> anyhow::Result<T>;
480}
481
482#[cfg(feature = "anyhow")]
483impl<T, E> IntoAnyhow<T> for std::result::Result<T, AlienError<E>>
484where
485    E: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
486{
487    fn into_anyhow(self) -> anyhow::Result<T> {
488        self.map_err(|err| anyhow::Error::new(err))
489    }
490}
491
492// Axum IntoResponse implementation
493#[cfg(feature = "axum")]
494impl<T> axum::response::IntoResponse for AlienError<T>
495where
496    T: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
497{
498    fn into_response(self) -> axum::response::Response {
499        // Default behavior: external response (sanitizes internal errors)
500        self.into_external_response()
501    }
502}
503
504#[cfg(feature = "axum")]
505impl<T> AlienError<T>
506where
507    T: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
508{
509    /// Convert to an Axum response suitable for internal microservice communication.
510    /// Preserves all error details including sensitive information from internal errors.
511    pub fn into_internal_response(self) -> axum::response::Response {
512        use axum::http::StatusCode;
513        use axum::response::{IntoResponse, Json};
514
515        // For internal responses, preserve all error details regardless of internal flag
516        let response_error = self.into_generic();
517
518        // Convert HTTP status code to StatusCode
519        let status_code = response_error
520            .http_status_code
521            .and_then(|code| StatusCode::from_u16(code).ok())
522            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
523
524        // Return JSON response with the error
525        (status_code, Json(response_error)).into_response()
526    }
527
528    /// Convert to an Axum response suitable for external API responses.
529    /// Sanitizes internal errors to prevent information leakage.
530    pub fn into_external_response(self) -> axum::response::Response {
531        use axum::http::StatusCode;
532        use axum::response::{IntoResponse, Json};
533
534        // For external responses, sanitize internal errors
535        let response_error = if self.internal {
536            // For internal errors, return a generic error message with 500 status code
537            AlienError::new(GenericError {
538                message: "Internal server error".to_string(),
539            })
540        } else {
541            self.into_generic()
542        };
543
544        // Convert HTTP status code to StatusCode
545        let status_code = response_error
546            .http_status_code
547            .and_then(|code| StatusCode::from_u16(code).ok())
548            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
549
550        // Return JSON response with the error
551        (status_code, Json(response_error)).into_response()
552    }
553}