1use serde::{Deserialize, Serialize};
2use std::error::Error;
3use std::fmt::{Debug, Display};
4use thiserror::Error;
5
6const MAX_RETRY_AFTER_MS: u64 = 24 * 60 * 60 * 1_000;
7const MAX_PROVIDER_TRACE_REFERENCE_BYTES: usize = 512;
8
9pub type AppResult<T> = Result<T, AppError>;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum ErrorCode {
14 Validation,
15 Unauthorized,
16 Forbidden,
17 NotFound,
18 Conflict,
19 RateLimited,
20 ExternalDependency,
21 Internal,
22}
23
24impl ErrorCode {
25 pub fn as_str(self) -> &'static str {
26 match self {
27 Self::Validation => "validation_failed",
28 Self::Unauthorized => "unauthorized",
29 Self::Forbidden => "forbidden",
30 Self::NotFound => "not_found",
31 Self::Conflict => "conflict",
32 Self::RateLimited => "rate_limited",
33 Self::ExternalDependency => "external_dependency_failure",
34 Self::Internal => "internal_error",
35 }
36 }
37}
38
39#[derive(Error)]
40pub struct AppError {
41 pub code: ErrorCode,
42 pub public_message: String,
43 pub retryable: bool,
44 pub retry_after_ms: Option<u64>,
46 pub provider_trace_reference: Option<String>,
48 pub details: Vec<ErrorDetail>,
49 #[source]
50 source: Option<Box<dyn Error + Send + Sync>>,
51}
52
53impl Debug for AppError {
54 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 formatter
56 .debug_struct("AppError")
57 .field("code", &self.code)
58 .field("public_message", &self.public_message)
59 .field("retryable", &self.retryable)
60 .field("retry_after_ms", &self.retry_after_ms)
61 .field("provider_trace_reference", &self.provider_trace_reference)
62 .field("details", &self.details)
63 .finish_non_exhaustive()
64 }
65}
66
67impl Display for AppError {
68 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(formatter, "{}: {}", self.code.as_str(), self.public_message)
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
74pub struct ErrorDetail {
75 pub field: Option<String>,
76 pub reason: String,
77}
78
79impl AppError {
80 pub fn new(code: ErrorCode, public_message: impl Into<String>) -> Self {
81 Self {
82 code,
83 public_message: public_message.into(),
84 retryable: false,
85 retry_after_ms: None,
86 provider_trace_reference: None,
87 details: Vec::new(),
88 source: None,
89 }
90 }
91
92 pub fn validation(public_message: impl Into<String>, details: Vec<ErrorDetail>) -> Self {
93 Self {
94 code: ErrorCode::Validation,
95 public_message: public_message.into(),
96 retryable: false,
97 retry_after_ms: None,
98 provider_trace_reference: None,
99 details,
100 source: None,
101 }
102 }
103
104 pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
105 self.source = Some(Box::new(source));
106 self
107 }
108
109 pub fn retryable(mut self) -> Self {
110 self.retryable = true;
111 self
112 }
113
114 #[must_use]
115 pub fn with_retry_after_ms(mut self, retry_after_ms: Option<u64>) -> Self {
116 self.retry_after_ms = retry_after_ms.map(|value| value.min(MAX_RETRY_AFTER_MS));
117 self
118 }
119
120 #[must_use]
121 pub fn with_provider_trace_reference(
122 mut self,
123 provider_trace_reference: Option<String>,
124 ) -> Self {
125 self.provider_trace_reference = provider_trace_reference
126 .filter(|value| !value.is_empty())
127 .map(|value| sanitize_provider_trace_reference(&value));
128 self
129 }
130}
131
132fn sanitize_provider_trace_reference(value: &str) -> String {
133 let mut sanitized = String::with_capacity(value.len().min(MAX_PROVIDER_TRACE_REFERENCE_BYTES));
134 for character in value.chars() {
135 let sanitized_character = if character.is_control() {
136 '�'
137 } else {
138 character
139 };
140 if sanitized.len() + sanitized_character.len_utf8() > MAX_PROVIDER_TRACE_REFERENCE_BYTES {
141 break;
142 }
143 sanitized.push(sanitized_character);
144 }
145 sanitized
146}