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 into_external(self) -> AlienError<GenericError> {
517 if self.internal {
518 AlienError::new(GenericError {
519 message: "Internal server error".to_string(),
520 })
521 } else {
522 self.into_generic()
523 }
524 }
525
526 pub fn human_report(&self) -> HumanErrorReport {
527 let mut layers = Vec::new();
528 collect_human_layers(
529 &self.code,
530 &self.message,
531 self.hint.as_deref(),
532 self.human_layer_presentation,
533 self.source.as_deref(),
534 &mut layers,
535 );
536
537 let headline_index = layers
538 .iter()
539 .position(|layer| layer.presentation == HumanLayerPresentation::Normal)
540 .unwrap_or(0);
541 let headline = &layers[headline_index];
542
543 let mut causes = Vec::new();
544 for (index, layer) in layers.iter().enumerate() {
545 if index == headline_index || layer.presentation == HumanLayerPresentation::Transparent
546 {
547 continue;
548 }
549
550 if causes.iter().any(|cause: &HumanErrorCause| {
551 cause.code == layer.code && cause.message == layer.message
552 }) {
553 continue;
554 }
555
556 causes.push(HumanErrorCause {
557 code: layer.code.clone(),
558 message: layer.message.clone(),
559 });
560 }
561
562 HumanErrorReport {
563 code: headline.code.clone(),
564 message: headline.message.clone(),
565 hint: headline.hint.clone().or_else(|| {
566 layers
567 .iter()
568 .enumerate()
569 .find(|(index, layer)| {
570 *index != headline_index
571 && layer.presentation == HumanLayerPresentation::Normal
572 && layer.hint.is_some()
573 })
574 .and_then(|(_, layer)| layer.hint.clone())
575 }),
576 causes,
577 }
578 }
579}
580
581#[derive(Debug, Clone)]
582struct HumanLayer {
583 code: String,
584 message: String,
585 hint: Option<String>,
586 presentation: HumanLayerPresentation,
587}
588
589fn collect_human_layers(
590 code: &str,
591 message: &str,
592 hint: Option<&str>,
593 presentation: HumanLayerPresentation,
594 source: Option<&AlienError<GenericError>>,
595 layers: &mut Vec<HumanLayer>,
596) {
597 layers.push(HumanLayer {
598 code: code.to_string(),
599 message: message.to_string(),
600 hint: hint.map(ToOwned::to_owned),
601 presentation,
602 });
603
604 if let Some(source) = source {
605 collect_human_layers(
606 &source.code,
607 &source.message,
608 source.hint.as_deref(),
609 source.human_layer_presentation,
610 source.source.as_deref(),
611 layers,
612 );
613 }
614}
615
616pub use alien_error_derive::AlienErrorData;
618
619#[cfg(feature = "anyhow")]
621impl From<anyhow::Error> for AlienError<GenericError> {
622 fn from(err: anyhow::Error) -> AlienError<GenericError> {
623 AlienError::new(GenericError {
624 message: err.to_string(),
625 })
626 }
627}
628
629#[cfg(feature = "anyhow")]
630pub trait IntoAnyhow<T> {
631 fn into_anyhow(self) -> anyhow::Result<T>;
633}
634
635#[cfg(feature = "anyhow")]
636impl<T, E> IntoAnyhow<T> for std::result::Result<T, AlienError<E>>
637where
638 E: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
639{
640 fn into_anyhow(self) -> anyhow::Result<T> {
641 self.map_err(|err| anyhow::Error::new(err))
642 }
643}
644
645#[cfg(feature = "axum")]
647impl<T> axum::response::IntoResponse for AlienError<T>
648where
649 T: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
650{
651 fn into_response(self) -> axum::response::Response {
652 self.into_external_response()
654 }
655}
656
657#[cfg(feature = "axum")]
658impl<T> AlienError<T>
659where
660 T: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
661{
662 pub fn into_internal_response(self) -> axum::response::Response {
665 use axum::http::StatusCode;
666 use axum::response::{IntoResponse, Json};
667
668 let response_error = self.into_generic();
670
671 let status_code = response_error
673 .http_status_code
674 .and_then(|code| StatusCode::from_u16(code).ok())
675 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
676
677 (status_code, Json(response_error)).into_response()
679 }
680
681 pub fn into_external_response(self) -> axum::response::Response {
684 use axum::http::StatusCode;
685 use axum::response::{IntoResponse, Json};
686
687 let response_error = self.into_external();
688
689 let status_code = response_error
691 .http_status_code
692 .and_then(|code| StatusCode::from_u16(code).ok())
693 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
694
695 (status_code, Json(response_error)).into_response()
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703
704 #[derive(Debug, Clone, Serialize)]
705 enum TestError {
706 Normal,
707 Transparent,
708 Hint,
709 }
710
711 impl AlienErrorData for TestError {
712 fn code(&self) -> &'static str {
713 match self {
714 Self::Normal => "NORMAL",
715 Self::Transparent => "WRAPPER",
716 Self::Hint => "HINT",
717 }
718 }
719
720 fn retryable(&self) -> bool {
721 false
722 }
723
724 fn internal(&self) -> bool {
725 false
726 }
727
728 fn message(&self) -> String {
729 match self {
730 Self::Normal => "Inner failure".to_string(),
731 Self::Transparent => "Wrapper failure".to_string(),
732 Self::Hint => "Action required".to_string(),
733 }
734 }
735
736 fn human_layer_presentation(&self) -> HumanLayerPresentation {
737 match self {
738 Self::Normal => HumanLayerPresentation::Normal,
739 Self::Transparent => HumanLayerPresentation::Transparent,
740 Self::Hint => HumanLayerPresentation::Normal,
741 }
742 }
743
744 fn hint(&self) -> Option<String> {
745 match self {
746 Self::Hint => Some("Run the setup command first.".to_string()),
747 _ => None,
748 }
749 }
750 }
751
752 #[test]
753 fn human_report_skips_transparent_wrappers() {
754 let err = Err::<(), _>(AlienError::new(TestError::Normal))
755 .context(TestError::Transparent)
756 .unwrap_err();
757
758 let report = err.human_report();
759 assert_eq!(report.code, "NORMAL");
760 assert_eq!(report.message, "Inner failure");
761 assert!(report.causes.is_empty());
762 }
763
764 #[test]
765 fn human_report_keeps_distinct_non_transparent_causes() {
766 let source = AlienError::new(GenericError {
767 message: "Socket closed".to_string(),
768 });
769 let err = Err::<(), _>(source).context(TestError::Normal).unwrap_err();
770
771 let report = err.human_report();
772 assert_eq!(report.code, "NORMAL");
773 assert_eq!(report.message, "Inner failure");
774 assert_eq!(report.causes.len(), 1);
775 assert_eq!(report.causes[0].code, "GENERIC_ERROR");
776 assert_eq!(report.causes[0].message, "Socket closed");
777 }
778
779 #[test]
780 fn human_report_uses_headline_hint_when_present() {
781 let err = AlienError::new(TestError::Hint);
782 let report = err.human_report();
783
784 assert_eq!(report.code, "HINT");
785 assert_eq!(report.message, "Action required");
786 assert_eq!(report.hint.as_deref(), Some("Run the setup command first."));
787 }
788
789 #[test]
790 fn human_report_uses_first_visible_hint_from_causes() {
791 let err = Err::<(), _>(AlienError::new(TestError::Hint))
792 .context(TestError::Transparent)
793 .unwrap_err();
794 let report = err.human_report();
795
796 assert_eq!(report.code, "HINT");
797 assert_eq!(report.hint.as_deref(), Some("Run the setup command first."));
798 }
799
800 #[test]
801 fn into_external_preserves_safe_error_data() {
802 let source = AlienError::new(GenericError {
803 message: "Socket closed".to_string(),
804 });
805 let error = Err::<(), _>(source)
806 .context(TestError::Hint)
807 .unwrap_err()
808 .into_external();
809
810 assert_eq!(error.code, "HINT");
811 assert_eq!(error.hint.as_deref(), Some("Run the setup command first."));
812 assert_eq!(
813 error
814 .source
815 .as_deref()
816 .map(|source| source.message.as_str()),
817 Some("Socket closed")
818 );
819 }
820
821 #[test]
822 fn into_external_sanitizes_internal_error_data() {
823 let mut error = AlienError::new(TestError::Normal);
824 error.internal = true;
825 error.context = Some(serde_json::json!({ "secret": "do-not-expose" }));
826
827 let external = error.into_external();
828
829 assert_eq!(external.code, "GENERIC_ERROR");
830 assert_eq!(external.message, "Internal server error");
831 assert!(external.context.is_none());
832 assert!(external.source.is_none());
833 }
834}