1use crate::agent_workflow::AgentError;
11use crate::fork::ForkError;
12use crate::graph::GraphError;
13use crate::kv::KvError;
14use crate::query::QueryError;
15use serde::{Deserialize, Serialize};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22#[non_exhaustive]
23pub enum ResultCode {
24 Ok,
26 Unsupported,
28 NotFound,
30 InvalidArgument,
32 TooLarge,
34 Conflict,
37 Stale,
40 VersionSkew,
42 Unauthenticated,
44 Backend,
46 Forbidden,
48 StepUpRequired,
51 Unavailable,
59 Unrecognized(u16),
65}
66
67impl ResultCode {
68 pub const fn code(self) -> u16 {
70 match self {
71 ResultCode::Ok => 0,
72 ResultCode::Unsupported => 1,
73 ResultCode::NotFound => 2,
74 ResultCode::InvalidArgument => 3,
75 ResultCode::TooLarge => 4,
76 ResultCode::Conflict => 5,
77 ResultCode::Stale => 6,
78 ResultCode::VersionSkew => 7,
79 ResultCode::Unauthenticated => 8,
80 ResultCode::Backend => 9,
81 ResultCode::Forbidden => 10,
82 ResultCode::StepUpRequired => 11,
83 ResultCode::Unavailable => 12,
84 ResultCode::Unrecognized(code) => code,
85 }
86 }
87
88 pub const fn from_code(code: u16) -> Self {
91 match code {
92 0 => ResultCode::Ok,
93 1 => ResultCode::Unsupported,
94 2 => ResultCode::NotFound,
95 3 => ResultCode::InvalidArgument,
96 4 => ResultCode::TooLarge,
97 5 => ResultCode::Conflict,
98 6 => ResultCode::Stale,
99 7 => ResultCode::VersionSkew,
100 8 => ResultCode::Unauthenticated,
101 9 => ResultCode::Backend,
102 10 => ResultCode::Forbidden,
103 11 => ResultCode::StepUpRequired,
104 12 => ResultCode::Unavailable,
105 other => ResultCode::Unrecognized(other),
106 }
107 }
108
109 pub const fn http_status(self) -> u16 {
112 match self {
113 ResultCode::Ok => 200,
114 ResultCode::Unsupported => 501,
115 ResultCode::NotFound => 404,
116 ResultCode::InvalidArgument => 400,
117 ResultCode::TooLarge => 413,
118 ResultCode::Conflict => 409,
119 ResultCode::Stale => 503,
120 ResultCode::VersionSkew => 400,
121 ResultCode::Unauthenticated => 401,
122 ResultCode::Backend => 502,
123 ResultCode::Forbidden => 403,
127 ResultCode::StepUpRequired => 403,
128 ResultCode::Unavailable => 503,
130 ResultCode::Unrecognized(_) => 500,
131 }
132 }
133
134 pub const fn is_retryable(self) -> bool {
140 matches!(self, ResultCode::Unavailable | ResultCode::Stale)
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
155pub struct CommandError {
156 pub code: ResultCode,
157 pub message: String,
158}
159
160impl CommandError {
161 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
163 Self {
164 code,
165 message: message.into(),
166 }
167 }
168
169 pub fn unsupported(message: impl Into<String>) -> Self {
171 Self::new(ResultCode::Unsupported, message)
172 }
173}
174
175impl From<&QueryError> for ResultCode {
176 fn from(error: &QueryError) -> Self {
177 match error {
178 QueryError::Unsupported(_) => ResultCode::Unsupported,
179 QueryError::Unauthorized(_) => ResultCode::Forbidden,
182 QueryError::IndexNotFound(_) | QueryError::ForkNotFound(_) => ResultCode::NotFound,
183 QueryError::Backend(_) => ResultCode::Backend,
184 QueryError::Unavailable(_) => ResultCode::Unavailable,
185 QueryError::TooLarge { .. } => ResultCode::TooLarge,
186 QueryError::Version { .. } => ResultCode::VersionSkew,
187 QueryError::Stale { .. } => ResultCode::Stale,
188 }
189 }
190}
191
192impl From<&KvError> for ResultCode {
193 fn from(error: &KvError) -> Self {
194 match error {
195 KvError::Unsupported(_) => ResultCode::Unsupported,
196 KvError::InvalidKey(_) => ResultCode::InvalidArgument,
197 KvError::InvalidNamespace(_) => ResultCode::InvalidArgument,
198 KvError::TooLarge { .. } => ResultCode::TooLarge,
199 KvError::Backend(_) => ResultCode::Backend,
200 KvError::Unavailable(_) => ResultCode::Unavailable,
201 KvError::Version { .. } => ResultCode::VersionSkew,
202 KvError::VersionConflict { .. } => ResultCode::Conflict,
203 KvError::LeaseLost => ResultCode::Conflict,
204 KvError::NotFound => ResultCode::NotFound,
205 KvError::NotLeader => ResultCode::Unavailable,
206 }
207 }
208}
209
210impl From<&ForkError> for ResultCode {
211 fn from(error: &ForkError) -> Self {
212 match error {
213 ForkError::Unsupported(_) => ResultCode::Unsupported,
214 ForkError::NotFound(_) => ResultCode::NotFound,
215 ForkError::InvalidFork(_) => ResultCode::InvalidArgument,
216 ForkError::Conflict(_) => ResultCode::Conflict,
217 ForkError::Backend(_) => ResultCode::Backend,
218 ForkError::Unavailable(_) => ResultCode::Unavailable,
219 ForkError::Version { .. } => ResultCode::VersionSkew,
220 ForkError::NotLeader => ResultCode::Unavailable,
221 }
222 }
223}
224
225impl From<&GraphError> for ResultCode {
226 fn from(error: &GraphError) -> Self {
227 match error {
228 GraphError::Unsupported(_) => ResultCode::Unsupported,
229 GraphError::Unauthorized(_) => ResultCode::Forbidden,
230 GraphError::InvalidName(_) => ResultCode::InvalidArgument,
231 GraphError::NotFound(_) => ResultCode::NotFound,
232 GraphError::TooLarge { .. } => ResultCode::TooLarge,
233 GraphError::Backend(_) => ResultCode::Backend,
234 GraphError::Unavailable(_) => ResultCode::Unavailable,
235 GraphError::Version { .. } => ResultCode::VersionSkew,
236 }
237 }
238}
239
240impl From<&AgentError> for ResultCode {
241 fn from(error: &AgentError) -> Self {
242 match error {
243 AgentError::Unsupported(_) => ResultCode::Unsupported,
244 AgentError::NotFound(_) => ResultCode::NotFound,
245 AgentError::Invalid(_) => ResultCode::InvalidArgument,
246 AgentError::Backend(_) => ResultCode::Backend,
247 AgentError::Unavailable(_) => ResultCode::Unavailable,
248 AgentError::Version { .. } => ResultCode::VersionSkew,
249 AgentError::NotLeader => ResultCode::Unavailable,
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn given_result_codes_when_mapped_then_should_round_trip_through_the_numeric_value() {
260 for code in [
261 ResultCode::Ok,
262 ResultCode::Unsupported,
263 ResultCode::NotFound,
264 ResultCode::InvalidArgument,
265 ResultCode::TooLarge,
266 ResultCode::Conflict,
267 ResultCode::Stale,
268 ResultCode::VersionSkew,
269 ResultCode::Unauthenticated,
270 ResultCode::Backend,
271 ResultCode::Forbidden,
272 ResultCode::StepUpRequired,
273 ResultCode::Unavailable,
274 ] {
275 assert_eq!(ResultCode::from_code(code.code()), code);
276 }
277 assert_eq!(ResultCode::from_code(900), ResultCode::Unrecognized(900));
279 assert_eq!(ResultCode::Unrecognized(900).code(), 900);
280 }
281
282 #[test]
283 fn given_surface_errors_when_classified_then_should_map_to_the_shared_code() {
284 assert_eq!(
285 ResultCode::from(&QueryError::IndexNotFound("orders".to_owned())),
286 ResultCode::NotFound
287 );
288 assert_eq!(
289 ResultCode::from(&QueryError::Stale {
290 what: "orders".to_owned(),
291 applied: 4,
292 required: 9,
293 }),
294 ResultCode::Stale
295 );
296 assert_eq!(
297 ResultCode::from(&KvError::VersionConflict { current: Some(3) }),
298 ResultCode::Conflict
299 );
300 assert_eq!(
301 ResultCode::from(&ForkError::Conflict("open".to_owned())),
302 ResultCode::Conflict
303 );
304 assert_eq!(
305 ResultCode::from(&KvError::NotLeader),
306 ResultCode::Unavailable
307 );
308 assert_eq!(
309 ResultCode::from(&ForkError::NotLeader),
310 ResultCode::Unavailable
311 );
312 assert_eq!(
313 ResultCode::from(&AgentError::NotLeader),
314 ResultCode::Unavailable
315 );
316 }
317
318 #[test]
321 fn given_transient_outcomes_when_classified_then_should_be_the_only_retryable_codes() {
322 for surface in [
323 ResultCode::from(&QueryError::Unavailable("dropped".to_owned())),
324 ResultCode::from(&KvError::Unavailable("dropped".to_owned())),
325 ResultCode::from(&ForkError::Unavailable("dropped".to_owned())),
326 ResultCode::from(&AgentError::Unavailable("dropped".to_owned())),
327 ] {
328 assert_eq!(surface, ResultCode::Unavailable);
329 }
330 assert_eq!(ResultCode::Unavailable.http_status(), 503);
331 assert!(ResultCode::Unavailable.is_retryable());
332 assert!(ResultCode::Stale.is_retryable());
333 for code in [
334 ResultCode::Ok,
335 ResultCode::Unsupported,
336 ResultCode::NotFound,
337 ResultCode::InvalidArgument,
338 ResultCode::TooLarge,
339 ResultCode::Conflict,
340 ResultCode::VersionSkew,
341 ResultCode::Unauthenticated,
342 ResultCode::Backend,
343 ResultCode::Forbidden,
344 ResultCode::StepUpRequired,
345 ResultCode::Unrecognized(900),
346 ] {
347 assert!(
348 !code.is_retryable(),
349 "{code:?} must not invite a blind retry"
350 );
351 }
352 }
353
354 #[test]
355 fn given_result_codes_when_mapped_to_http_then_should_match_the_binding_table() {
356 assert_eq!(ResultCode::NotFound.http_status(), 404);
357 assert_eq!(ResultCode::Unsupported.http_status(), 501);
358 assert_eq!(ResultCode::TooLarge.http_status(), 413);
359 assert_eq!(ResultCode::Conflict.http_status(), 409);
360 assert_eq!(ResultCode::Stale.http_status(), 503);
361 assert_eq!(ResultCode::Unauthenticated.http_status(), 401);
362 assert_eq!(ResultCode::Forbidden.http_status(), 403);
363 assert_eq!(ResultCode::StepUpRequired.http_status(), 403);
364 assert_eq!(ResultCode::Backend.http_status(), 502);
365 assert_eq!(ResultCode::Unrecognized(777).http_status(), 500);
368 assert_eq!(ResultCode::Unrecognized(777).code(), 777);
369 }
370
371 #[cfg(feature = "cbor")]
372 #[test]
373 fn given_a_result_code_when_round_tripped_through_cbor_then_should_preserve_the_variant() {
374 use crate::framing::{decode_named, encode_named};
375 for code in [
376 ResultCode::Ok,
377 ResultCode::Conflict,
378 ResultCode::Stale,
379 ResultCode::Unrecognized(4242),
380 ] {
381 let bytes = encode_named(&code).expect("serializes");
382 let back: ResultCode = decode_named(&bytes).expect("deserializes");
383 assert_eq!(back, code);
384 }
385 }
386
387 #[cfg(feature = "cbor")]
388 #[test]
389 fn given_a_command_error_when_round_tripped_then_should_preserve_code_and_message() {
390 use crate::framing::{decode_named, encode_named};
391 let error = CommandError::unsupported("AGDX_KV_CAS not served on this build");
392 assert_eq!(error.code, ResultCode::Unsupported);
393 let bytes = encode_named(&error).expect("serializes");
394 let back: CommandError = decode_named(&bytes).expect("deserializes");
395 assert_eq!(back, error);
396 }
397}