1use std::{error::Error as StdError, fmt};
72
73use serde::{Deserialize, Serialize};
74
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76pub enum HumanLayerPresentation {
77 #[default]
78 Normal,
79 Transparent,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct HumanErrorCause {
84 pub code: String,
85 pub message: String,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct HumanErrorReport {
90 pub code: String,
91 pub message: String,
92 pub hint: Option<String>,
93 pub causes: Vec<HumanErrorCause>,
94}
95
96pub trait AlienErrorData {
98 fn code(&self) -> &'static str;
100 fn retryable(&self) -> bool;
102 fn internal(&self) -> bool;
104 fn message(&self) -> String;
106 fn http_status_code(&self) -> u16 {
108 500
109 }
110 fn context(&self) -> Option<serde_json::Value> {
112 None
113 }
114
115 fn retryable_inherit(&self) -> Option<bool> {
118 Some(self.retryable())
119 }
120
121 fn internal_inherit(&self) -> Option<bool> {
124 Some(self.internal())
125 }
126
127 fn http_status_code_inherit(&self) -> Option<u16> {
130 Some(self.http_status_code())
131 }
132
133 fn human_layer_presentation(&self) -> HumanLayerPresentation {
135 HumanLayerPresentation::Normal
136 }
137
138 fn hint(&self) -> Option<String> {
140 None
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Default)]
146#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
147pub struct GenericError {
148 pub message: String,
149}
150
151impl std::fmt::Display for GenericError {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 write!(f, "{}", self.message)
154 }
155}
156
157impl StdError for GenericError {}
158
159impl AlienErrorData for GenericError {
160 fn code(&self) -> &'static str {
161 "GENERIC_ERROR"
162 }
163
164 fn retryable(&self) -> bool {
165 false
166 }
167
168 fn internal(&self) -> bool {
169 false
170 }
171
172 fn message(&self) -> String {
173 self.message.clone()
174 }
175
176 fn http_status_code(&self) -> u16 {
177 500
178 }
179}
180
181#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
189#[serde(rename_all = "camelCase")]
190#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
191pub struct AlienError<T = GenericError>
192where
193 T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
194{
195 #[cfg_attr(feature = "openapi", schema(example = "NOT_FOUND", max_length = 128))]
201 pub code: String,
202
203 #[cfg_attr(
208 feature = "openapi",
209 schema(example = "Item not found.", max_length = 16384)
210 )]
211 pub message: String,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
219 #[cfg_attr(feature = "openapi", schema(nullable = true))]
220 pub context: Option<serde_json::Value>,
221
222 #[serde(skip_serializing_if = "Option::is_none")]
224 #[cfg_attr(feature = "openapi", schema(nullable = true))]
225 pub hint: Option<String>,
226
227 #[cfg_attr(feature = "openapi", schema(default = false))]
233 pub retryable: bool,
234
235 pub internal: bool,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
247 #[cfg_attr(feature = "openapi", schema(minimum = 100, maximum = 599))]
248 pub http_status_code: Option<u16>,
249
250 #[serde(skip_serializing_if = "Option::is_none")]
256 #[cfg_attr(feature = "openapi", schema(value_type = Option<serde_json::Value>))]
257 pub source: Option<Box<AlienError<GenericError>>>,
258
259 #[serde(skip, default)]
260 #[cfg_attr(feature = "openapi", schema(ignore))]
261 pub human_layer_presentation: HumanLayerPresentation,
262
263 #[serde(
265 rename = "_error_for_pattern_matching",
266 skip_serializing_if = "Option::is_none"
267 )]
268 #[cfg_attr(feature = "openapi", schema(ignore))]
269 pub error: Option<T>,
270}
271
272impl<T> AlienError<T>
273where
274 T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
275{
276 pub fn new(meta: T) -> Self {
278 AlienError {
279 code: meta.code().to_string(),
280 message: meta.message(),
281 context: meta.context(),
282 hint: meta.hint(),
283 retryable: meta.retryable(),
284 internal: meta.internal(),
285 http_status_code: Some(meta.http_status_code()),
286 source: None,
287 human_layer_presentation: meta.human_layer_presentation(),
288 error: Some(meta),
289 }
290 }
291}
292
293impl AlienError<GenericError> {
294 pub fn from_std(err: &(dyn StdError + 'static)) -> Self {
296 let generic = GenericError {
297 message: err.to_string(),
298 };
299
300 let source = err.source().map(|src| Box::new(Self::from_std(src)));
302
303 AlienError {
304 code: generic.code().to_string(),
305 message: generic.message(),
306 context: generic.context(),
307 hint: generic.hint(),
308 retryable: generic.retryable(),
309 internal: generic.internal(),
310 http_status_code: Some(generic.http_status_code()),
311 source,
312 human_layer_presentation: HumanLayerPresentation::Normal,
313 error: Some(generic),
314 }
315 }
316}
317
318impl<T> fmt::Display for AlienError<T>
319where
320 T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
321{
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 write!(f, "{}: {}", self.code, self.message)?;
324 fn recurse(
325 e: &AlienError<GenericError>,
326 indent: &str,
327 f: &mut fmt::Formatter<'_>,
328 ) -> fmt::Result {
329 writeln!(f, "{}├─▶ {}: {}", indent, e.code, e.message)?;
330 if let Some(ref src) = e.source {
331 recurse(src, &format!("{}│ ", indent), f)?;
332 }
333 Ok(())
334 }
335 if let Some(ref src) = self.source {
336 writeln!(f)?;
337 recurse(src, "", f)?;
338 }
339 Ok(())
340 }
341}
342
343impl<T> StdError for AlienError<T>
344where
345 T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
346{
347 fn source(&self) -> Option<&(dyn StdError + 'static)> {
348 self.source
349 .as_ref()
350 .map(|e| e.as_ref() as &(dyn StdError + 'static))
351 }
352}
353
354pub trait Context<T, E> {
356 fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
358 self,
359 meta: M,
360 ) -> std::result::Result<T, AlienError<M>>;
361}
362
363impl<T, E> Context<T, E> for std::result::Result<T, AlienError<E>>
365where
366 E: AlienErrorData + Clone + std::fmt::Debug + Serialize + 'static + Send + Sync,
367{
368 fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
369 self,
370 meta: M,
371 ) -> std::result::Result<T, AlienError<M>> {
372 self.map_err(|err| {
373 let mut new_err = AlienError::new(meta.clone());
374
375 if meta.retryable_inherit().is_none() {
379 new_err.retryable = err.retryable;
380 }
381 if meta.internal_inherit().is_none() {
382 new_err.internal = err.internal;
383 }
384 if meta.http_status_code_inherit().is_none() {
385 new_err.http_status_code = err.http_status_code;
386 }
387
388 let generic_err = AlienError {
390 code: err.code.clone(),
391 message: err.message.clone(),
392 context: err.context.clone(),
393 hint: err.hint.clone(),
394 retryable: err.retryable,
395 internal: err.internal,
396 source: err.source,
397 human_layer_presentation: err.human_layer_presentation,
398 error: None,
399 http_status_code: err.http_status_code,
400 };
401 new_err.source = Some(Box::new(generic_err));
402 new_err
403 })
404 }
405}
406
407pub trait ContextError<E> {
409 fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
411 self,
412 meta: M,
413 ) -> AlienError<M>;
414}
415
416impl<E> ContextError<E> for AlienError<E>
418where
419 E: AlienErrorData + Clone + std::fmt::Debug + Serialize + 'static + Send + Sync,
420{
421 fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
422 self,
423 meta: M,
424 ) -> AlienError<M> {
425 let mut new_err = AlienError::new(meta.clone());
426
427 if meta.retryable_inherit().is_none() {
431 new_err.retryable = self.retryable;
432 }
433 if meta.internal_inherit().is_none() {
434 new_err.internal = self.internal;
435 }
436 if meta.http_status_code_inherit().is_none() {
437 new_err.http_status_code = self.http_status_code;
438 }
439
440 let generic_err = AlienError {
442 code: self.code.clone(),
443 message: self.message.clone(),
444 context: self.context.clone(),
445 hint: self.hint.clone(),
446 retryable: self.retryable,
447 internal: self.internal,
448 source: self.source,
449 human_layer_presentation: self.human_layer_presentation,
450 error: None,
451 http_status_code: self.http_status_code,
452 };
453 new_err.source = Some(Box::new(generic_err));
454 new_err
455 }
456}
457
458pub trait IntoAlienError<T> {
460 fn into_alien_error(self) -> std::result::Result<T, AlienError<GenericError>>;
462}
463
464impl<T, E> IntoAlienError<T> for std::result::Result<T, E>
465where
466 E: StdError + 'static,
467{
468 fn into_alien_error(self) -> std::result::Result<T, AlienError<GenericError>> {
469 self.map_err(|err| AlienError::from_std(&err as &dyn StdError))
470 }
471}
472
473pub trait IntoAlienErrorDirect {
475 fn into_alien_error(self) -> AlienError<GenericError>;
477}
478
479impl<E> IntoAlienErrorDirect for E
480where
481 E: StdError + 'static,
482{
483 fn into_alien_error(self) -> AlienError<GenericError> {
484 AlienError::from_std(&self as &dyn StdError)
485 }
486}
487
488pub type Result<T, E = GenericError> = std::result::Result<T, AlienError<E>>;
491
492impl<T> AlienError<T>
493where
494 T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
495{
496 pub fn into_generic(self) -> AlienError<GenericError> {
498 AlienError {
499 code: self.code,
500 message: self.message,
501 context: self.context,
502 hint: self.hint,
503 retryable: self.retryable,
504 internal: self.internal,
505 source: self.source,
506 human_layer_presentation: self.human_layer_presentation,
507 error: None,
508 http_status_code: self.http_status_code,
509 }
510 }
511
512 pub fn human_report(&self) -> HumanErrorReport {
513 let mut layers = Vec::new();
514 collect_human_layers(
515 &self.code,
516 &self.message,
517 self.hint.as_deref(),
518 self.human_layer_presentation,
519 self.source.as_deref(),
520 &mut layers,
521 );
522
523 let headline_index = layers
524 .iter()
525 .position(|layer| layer.presentation == HumanLayerPresentation::Normal)
526 .unwrap_or(0);
527 let headline = &layers[headline_index];
528
529 let mut causes = Vec::new();
530 for (index, layer) in layers.iter().enumerate() {
531 if index == headline_index || layer.presentation == HumanLayerPresentation::Transparent
532 {
533 continue;
534 }
535
536 if causes.iter().any(|cause: &HumanErrorCause| {
537 cause.code == layer.code && cause.message == layer.message
538 }) {
539 continue;
540 }
541
542 causes.push(HumanErrorCause {
543 code: layer.code.clone(),
544 message: layer.message.clone(),
545 });
546 }
547
548 HumanErrorReport {
549 code: headline.code.clone(),
550 message: headline.message.clone(),
551 hint: headline.hint.clone().or_else(|| {
552 layers
553 .iter()
554 .enumerate()
555 .find(|(index, layer)| {
556 *index != headline_index
557 && layer.presentation == HumanLayerPresentation::Normal
558 && layer.hint.is_some()
559 })
560 .and_then(|(_, layer)| layer.hint.clone())
561 }),
562 causes,
563 }
564 }
565}
566
567#[derive(Debug, Clone)]
568struct HumanLayer {
569 code: String,
570 message: String,
571 hint: Option<String>,
572 presentation: HumanLayerPresentation,
573}
574
575fn collect_human_layers(
576 code: &str,
577 message: &str,
578 hint: Option<&str>,
579 presentation: HumanLayerPresentation,
580 source: Option<&AlienError<GenericError>>,
581 layers: &mut Vec<HumanLayer>,
582) {
583 layers.push(HumanLayer {
584 code: code.to_string(),
585 message: message.to_string(),
586 hint: hint.map(ToOwned::to_owned),
587 presentation,
588 });
589
590 if let Some(source) = source {
591 collect_human_layers(
592 &source.code,
593 &source.message,
594 source.hint.as_deref(),
595 source.human_layer_presentation,
596 source.source.as_deref(),
597 layers,
598 );
599 }
600}
601
602pub use alien_error_derive::AlienErrorData;
604
605#[cfg(feature = "anyhow")]
607impl From<anyhow::Error> for AlienError<GenericError> {
608 fn from(err: anyhow::Error) -> AlienError<GenericError> {
609 AlienError::new(GenericError {
610 message: err.to_string(),
611 })
612 }
613}
614
615#[cfg(feature = "anyhow")]
616pub trait IntoAnyhow<T> {
617 fn into_anyhow(self) -> anyhow::Result<T>;
619}
620
621#[cfg(feature = "anyhow")]
622impl<T, E> IntoAnyhow<T> for std::result::Result<T, AlienError<E>>
623where
624 E: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
625{
626 fn into_anyhow(self) -> anyhow::Result<T> {
627 self.map_err(|err| anyhow::Error::new(err))
628 }
629}
630
631#[cfg(feature = "axum")]
633impl<T> axum::response::IntoResponse for AlienError<T>
634where
635 T: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
636{
637 fn into_response(self) -> axum::response::Response {
638 self.into_external_response()
640 }
641}
642
643#[cfg(feature = "axum")]
644impl<T> AlienError<T>
645where
646 T: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
647{
648 pub fn into_internal_response(self) -> axum::response::Response {
651 use axum::http::StatusCode;
652 use axum::response::{IntoResponse, Json};
653
654 let response_error = self.into_generic();
656
657 let status_code = response_error
659 .http_status_code
660 .and_then(|code| StatusCode::from_u16(code).ok())
661 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
662
663 (status_code, Json(response_error)).into_response()
665 }
666
667 pub fn into_external_response(self) -> axum::response::Response {
670 use axum::http::StatusCode;
671 use axum::response::{IntoResponse, Json};
672
673 let response_error = if self.internal {
675 AlienError::new(GenericError {
677 message: "Internal server error".to_string(),
678 })
679 } else {
680 self.into_generic()
681 };
682
683 let status_code = response_error
685 .http_status_code
686 .and_then(|code| StatusCode::from_u16(code).ok())
687 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
688
689 (status_code, Json(response_error)).into_response()
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697
698 #[derive(Debug, Clone, Serialize)]
699 enum TestError {
700 Normal,
701 Transparent,
702 Hint,
703 }
704
705 impl AlienErrorData for TestError {
706 fn code(&self) -> &'static str {
707 match self {
708 Self::Normal => "NORMAL",
709 Self::Transparent => "WRAPPER",
710 Self::Hint => "HINT",
711 }
712 }
713
714 fn retryable(&self) -> bool {
715 false
716 }
717
718 fn internal(&self) -> bool {
719 false
720 }
721
722 fn message(&self) -> String {
723 match self {
724 Self::Normal => "Inner failure".to_string(),
725 Self::Transparent => "Wrapper failure".to_string(),
726 Self::Hint => "Action required".to_string(),
727 }
728 }
729
730 fn human_layer_presentation(&self) -> HumanLayerPresentation {
731 match self {
732 Self::Normal => HumanLayerPresentation::Normal,
733 Self::Transparent => HumanLayerPresentation::Transparent,
734 Self::Hint => HumanLayerPresentation::Normal,
735 }
736 }
737
738 fn hint(&self) -> Option<String> {
739 match self {
740 Self::Hint => Some("Run the setup command first.".to_string()),
741 _ => None,
742 }
743 }
744 }
745
746 #[test]
747 fn human_report_skips_transparent_wrappers() {
748 let err = Err::<(), _>(AlienError::new(TestError::Normal))
749 .context(TestError::Transparent)
750 .unwrap_err();
751
752 let report = err.human_report();
753 assert_eq!(report.code, "NORMAL");
754 assert_eq!(report.message, "Inner failure");
755 assert!(report.causes.is_empty());
756 }
757
758 #[test]
759 fn human_report_keeps_distinct_non_transparent_causes() {
760 let source = AlienError::new(GenericError {
761 message: "Socket closed".to_string(),
762 });
763 let err = Err::<(), _>(source).context(TestError::Normal).unwrap_err();
764
765 let report = err.human_report();
766 assert_eq!(report.code, "NORMAL");
767 assert_eq!(report.message, "Inner failure");
768 assert_eq!(report.causes.len(), 1);
769 assert_eq!(report.causes[0].code, "GENERIC_ERROR");
770 assert_eq!(report.causes[0].message, "Socket closed");
771 }
772
773 #[test]
774 fn human_report_uses_headline_hint_when_present() {
775 let err = AlienError::new(TestError::Hint);
776 let report = err.human_report();
777
778 assert_eq!(report.code, "HINT");
779 assert_eq!(report.message, "Action required");
780 assert_eq!(report.hint.as_deref(), Some("Run the setup command first."));
781 }
782
783 #[test]
784 fn human_report_uses_first_visible_hint_from_causes() {
785 let err = Err::<(), _>(AlienError::new(TestError::Hint))
786 .context(TestError::Transparent)
787 .unwrap_err();
788 let report = err.human_report();
789
790 assert_eq!(report.code, "HINT");
791 assert_eq!(report.hint.as_deref(), Some("Run the setup command first."));
792 }
793}