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}