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}