Skip to main content

pharos_runtime_core/
error.rs

1//! Stable public error taxonomy shared by HTTP, CLI, jobs, migrations, and integrations.
2
3use serde_json::Value;
4/// Schema identifier used by the public Pharos error envelope.
5///
6/// # Examples
7///
8/// ```rust,ignore
9/// let _ = pharos_runtime_core::error::ERROR_SCHEMA;
10/// ```
11///
12/// # Related
13///
14/// - [`crate::error`] for the surrounding module surface.
15pub const ERROR_SCHEMA: &str = "pharos.error.v1";
16
17#[derive(Debug, Clone, Copy, Eq, PartialEq)]
18/// Stable failure taxonomy shared across HTTP, CLI, jobs, and integration surfaces.
19///
20/// # Variants
21///
22/// - `InvalidInput`: Supported `ErrorCode` variant.
23/// - `AuthenticationRequired`: Supported `ErrorCode` variant.
24/// - `AccessDenied`: Supported `ErrorCode` variant.
25/// - `NotFound`: Supported `ErrorCode` variant.
26/// - `Conflict`: Supported `ErrorCode` variant.
27/// - `RateLimited`: Supported `ErrorCode` variant.
28/// - `CapacityExceeded`: Supported `ErrorCode` variant.
29/// - `DeadlineExceeded`: Supported `ErrorCode` variant.
30/// - `DependencyUnavailable`: Supported `ErrorCode` variant.
31/// - `IntegrityFailure`: Supported `ErrorCode` variant.
32/// - `InternalFailure`: Supported `ErrorCode` variant.
33///
34/// # Examples
35///
36/// ```rust,ignore
37/// let _ = pharos_runtime_core::error::ErrorCode::InvalidInput;
38/// ```
39///
40/// # Related
41///
42/// - [`crate::error`] for constructors, helpers, and neighboring runtime types.
43pub enum ErrorCode {
44    InvalidInput,
45    AuthenticationRequired,
46    AccessDenied,
47    NotFound,
48    Conflict,
49    RateLimited,
50    CapacityExceeded,
51    DeadlineExceeded,
52    DependencyUnavailable,
53    IntegrityFailure,
54    InternalFailure,
55}
56
57impl ErrorCode {
58    /// Maps a CLI exit code into the public error taxonomy.
59    ///
60    /// # Examples
61    ///
62    /// ```rust,ignore
63    /// let _ = pharos_runtime_core::error::fn;
64    /// ```
65    ///
66    /// # Related
67    ///
68    /// - [`crate::error`] for the surrounding module surface.
69    pub const fn from_exit_code(exit_code: i32) -> Self {
70        match exit_code {
71            64 | 78 => Self::InvalidInput,
72            66 => Self::NotFound,
73            69 => Self::DependencyUnavailable,
74            70 => Self::InternalFailure,
75            74 => Self::IntegrityFailure,
76            75 => Self::Conflict,
77            77 => Self::AccessDenied,
78            _ => Self::InternalFailure,
79        }
80    }
81    /// Maps an HTTP status code into the public error taxonomy.
82    ///
83    /// # Examples
84    ///
85    /// ```rust,ignore
86    /// let _ = pharos_runtime_core::error::fn;
87    /// ```
88    ///
89    /// # Related
90    ///
91    /// - [`crate::error`] for the surrounding module surface.
92    pub const fn from_http_status(status: u16) -> Self {
93        match status {
94            400 | 405 | 413 | 422 => Self::InvalidInput,
95            401 => Self::AuthenticationRequired,
96            403 => Self::AccessDenied,
97            404 => Self::NotFound,
98            409 => Self::Conflict,
99            424 | 502 => Self::DependencyUnavailable,
100            429 => Self::RateLimited,
101            503 => Self::CapacityExceeded,
102            504 => Self::DeadlineExceeded,
103            _ => Self::InternalFailure,
104        }
105    }
106    /// Returns the stable machine-readable error code.
107    ///
108    /// # Examples
109    ///
110    /// ```rust,ignore
111    /// let _ = pharos_runtime_core::error::fn;
112    /// ```
113    ///
114    /// # Related
115    ///
116    /// - [`crate::error`] for the surrounding module surface.
117    pub const fn as_str(self) -> &'static str {
118        match self {
119            Self::InvalidInput => "invalid_input",
120            Self::AuthenticationRequired => "authentication_required",
121            Self::AccessDenied => "access_denied",
122            Self::NotFound => "not_found",
123            Self::Conflict => "conflict",
124            Self::RateLimited => "rate_limited",
125            Self::CapacityExceeded => "capacity_exceeded",
126            Self::DeadlineExceeded => "deadline_exceeded",
127            Self::DependencyUnavailable => "dependency_unavailable",
128            Self::IntegrityFailure => "integrity_failure",
129            Self::InternalFailure => "internal_failure",
130        }
131    }
132    /// Returns the fail-closed HTTP status for this error class.
133    ///
134    /// # Examples
135    ///
136    /// ```rust,ignore
137    /// let _ = pharos_runtime_core::error::fn;
138    /// ```
139    ///
140    /// # Related
141    ///
142    /// - [`crate::error`] for the surrounding module surface.
143    pub const fn http_status(self) -> u16 {
144        match self {
145            Self::InvalidInput => 400,
146            Self::AuthenticationRequired => 401,
147            Self::AccessDenied => 403,
148            Self::NotFound => 404,
149            Self::Conflict => 409,
150            Self::RateLimited => 429,
151            Self::CapacityExceeded => 503,
152            Self::DeadlineExceeded => 504,
153            Self::DependencyUnavailable => 503,
154            Self::IntegrityFailure | Self::InternalFailure => 500,
155        }
156    }
157    /// Returns whether a caller can retry without changing the request.
158    ///
159    /// # Examples
160    ///
161    /// ```rust,ignore
162    /// let _ = pharos_runtime_core::error::fn;
163    /// ```
164    ///
165    /// # Related
166    ///
167    /// - [`crate::error`] for the surrounding module surface.
168    pub const fn retryable(self) -> bool {
169        matches!(
170            self,
171            Self::RateLimited
172                | Self::CapacityExceeded
173                | Self::DeadlineExceeded
174                | Self::DependencyUnavailable
175        )
176    }
177    /// Returns the stable CLI exit code for this error class.
178    ///
179    /// # Examples
180    ///
181    /// ```rust,ignore
182    /// let _ = pharos_runtime_core::error::fn;
183    /// ```
184    ///
185    /// # Related
186    ///
187    /// - [`crate::error`] for the surrounding module surface.
188    pub const fn exit_code(self) -> i32 {
189        match self {
190            Self::InvalidInput => 64,
191            Self::AuthenticationRequired | Self::AccessDenied => 77,
192            Self::NotFound => 66,
193            Self::Conflict | Self::RateLimited | Self::CapacityExceeded => 75,
194            Self::DeadlineExceeded | Self::DependencyUnavailable => 69,
195            Self::IntegrityFailure => 74,
196            Self::InternalFailure => 70,
197        }
198    }
199}
200/// Builds the stable public error envelope returned by runtime and CLI surfaces.
201///
202/// # Parameters
203///
204/// - `code`: Input consumed by [`crate::error::envelope`].
205/// - `message`: Input consumed by [`crate::error::envelope`].
206/// - `correlation_id`: Input consumed by [`crate::error::envelope`].
207///
208/// # Returns
209///
210/// Returns the computed value produced by [`crate::error::envelope`].
211///
212/// # Examples
213///
214/// ```rust,ignore
215/// let _result = pharos_runtime_core::error::envelope(todo!(), "example", todo!());
216/// ```
217///
218/// # Related
219///
220/// - [`crate::error`] for adjacent helpers that share the same runtime surface.
221pub fn envelope(code: ErrorCode, message: &str, correlation_id: Option<&str>) -> Value {
222    serde_json::json!({
223        "schema": ERROR_SCHEMA,
224        "error": {
225            "code": code.as_str(),
226            "message": message,
227            "retryable": code.retryable(),
228            "correlation_id": correlation_id.unwrap_or_default(),
229        }
230    })
231}
232
233#[cfg(test)]
234mod tests {
235    use super::{ErrorCode, envelope};
236
237    #[test]
238    /// Performs stable error mapping never turns failure into success.
239    fn stable_error_mapping_never_turns_failure_into_success() {
240        for code in [
241            ErrorCode::InvalidInput,
242            ErrorCode::AuthenticationRequired,
243            ErrorCode::AccessDenied,
244            ErrorCode::NotFound,
245            ErrorCode::Conflict,
246            ErrorCode::RateLimited,
247            ErrorCode::CapacityExceeded,
248            ErrorCode::DeadlineExceeded,
249            ErrorCode::DependencyUnavailable,
250            ErrorCode::IntegrityFailure,
251            ErrorCode::InternalFailure,
252        ] {
253            assert!(code.http_status() >= 400);
254            assert_ne!(code.exit_code(), 0);
255            assert_eq!(envelope(code, "safe", None)["error"]["code"], code.as_str());
256        }
257    }
258}