Skip to main content

platform_core/
error.rs

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    /// Provider-supplied retry delay hint preserved for the Host retry rail.
45    pub retry_after_ms: Option<u64>,
46    /// Opaque Provider trace reference suitable for operational correlation.
47    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}