1use crate::authz::AuthzError;
11use crate::checkpoint::CheckpointError;
12use crate::fork::ForkError;
13use crate::graph::GraphError;
14use crate::kv::KvError;
15use crate::query::QueryError;
16use crate::session::SessionError;
17use serde::{Deserialize, Serialize};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24#[non_exhaustive]
25pub enum ResultCode {
26 Ok,
28 Unsupported,
30 NotFound,
32 InvalidArgument,
34 TooLarge,
36 Conflict,
39 Stale,
42 VersionSkew,
44 Unauthenticated,
46 Backend,
48 Forbidden,
50 StepUpRequired,
53 Unavailable,
61 ResourceLimit,
63 Cancelled,
65 DeadlineExceeded,
67 ExpiredSnapshot,
69 StaleGeneration,
71 TargetUnavailable,
73 Pending,
78 BackendUnavailable,
82 Unrecognized(u16),
88}
89
90impl ResultCode {
91 pub const fn code(self) -> u16 {
93 match self {
94 ResultCode::Ok => 0,
95 ResultCode::Unsupported => 1,
96 ResultCode::NotFound => 2,
97 ResultCode::InvalidArgument => 3,
98 ResultCode::TooLarge => 4,
99 ResultCode::Conflict => 5,
100 ResultCode::Stale => 6,
101 ResultCode::VersionSkew => 7,
102 ResultCode::Unauthenticated => 8,
103 ResultCode::Backend => 9,
104 ResultCode::Forbidden => 10,
105 ResultCode::StepUpRequired => 11,
106 ResultCode::Unavailable => 12,
107 ResultCode::ResourceLimit => 13,
108 ResultCode::Cancelled => 14,
109 ResultCode::DeadlineExceeded => 15,
110 ResultCode::ExpiredSnapshot => 16,
111 ResultCode::StaleGeneration => 17,
112 ResultCode::TargetUnavailable => 18,
113 ResultCode::Pending => 19,
114 ResultCode::BackendUnavailable => 20,
115 ResultCode::Unrecognized(code) => code,
116 }
117 }
118
119 pub const fn from_code(code: u16) -> Self {
122 match code {
123 0 => ResultCode::Ok,
124 1 => ResultCode::Unsupported,
125 2 => ResultCode::NotFound,
126 3 => ResultCode::InvalidArgument,
127 4 => ResultCode::TooLarge,
128 5 => ResultCode::Conflict,
129 6 => ResultCode::Stale,
130 7 => ResultCode::VersionSkew,
131 8 => ResultCode::Unauthenticated,
132 9 => ResultCode::Backend,
133 10 => ResultCode::Forbidden,
134 11 => ResultCode::StepUpRequired,
135 12 => ResultCode::Unavailable,
136 13 => ResultCode::ResourceLimit,
137 14 => ResultCode::Cancelled,
138 15 => ResultCode::DeadlineExceeded,
139 16 => ResultCode::ExpiredSnapshot,
140 17 => ResultCode::StaleGeneration,
141 18 => ResultCode::TargetUnavailable,
142 19 => ResultCode::Pending,
143 20 => ResultCode::BackendUnavailable,
144 other => ResultCode::Unrecognized(other),
145 }
146 }
147
148 pub const fn http_status(self) -> u16 {
151 match self {
152 ResultCode::Ok => 200,
153 ResultCode::Unsupported => 501,
154 ResultCode::NotFound => 404,
155 ResultCode::InvalidArgument => 400,
156 ResultCode::TooLarge => 413,
157 ResultCode::Conflict => 409,
158 ResultCode::Stale => 503,
159 ResultCode::VersionSkew => 400,
160 ResultCode::Unauthenticated => 401,
161 ResultCode::Backend => 502,
162 ResultCode::Forbidden => 403,
166 ResultCode::StepUpRequired => 403,
167 ResultCode::Unavailable => 503,
169 ResultCode::ResourceLimit => 429,
170 ResultCode::Cancelled => 409,
171 ResultCode::DeadlineExceeded => 408,
172 ResultCode::ExpiredSnapshot => 410,
173 ResultCode::StaleGeneration => 409,
174 ResultCode::TargetUnavailable => 503,
175 ResultCode::Pending => 503,
178 ResultCode::BackendUnavailable => 503,
179 ResultCode::Unrecognized(_) => 500,
180 }
181 }
182
183 pub const fn is_retryable(self) -> bool {
191 matches!(
192 self,
193 ResultCode::Unavailable
194 | ResultCode::Stale
195 | ResultCode::TargetUnavailable
196 | ResultCode::BackendUnavailable
197 )
198 }
199
200 pub const fn is_pending(self) -> bool {
203 matches!(self, ResultCode::Pending)
204 }
205
206 pub const fn is_backend_unavailable(self) -> bool {
213 matches!(self, ResultCode::BackendUnavailable)
214 }
215}
216
217#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
228pub struct CommandError {
229 pub code: ResultCode,
230 pub message: String,
231}
232
233impl CommandError {
234 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
236 Self {
237 code,
238 message: message.into(),
239 }
240 }
241
242 pub fn unsupported(message: impl Into<String>) -> Self {
244 Self::new(ResultCode::Unsupported, message)
245 }
246}
247
248impl From<&QueryError> for ResultCode {
249 fn from(error: &QueryError) -> Self {
250 match error {
251 QueryError::Unsupported(_) => ResultCode::Unsupported,
252 QueryError::Unauthorized(_) => ResultCode::Forbidden,
255 QueryError::IndexNotFound(_) | QueryError::ForkNotFound(_) => ResultCode::NotFound,
256 QueryError::Backend(_) => ResultCode::Backend,
257 QueryError::Unavailable(_) => ResultCode::Unavailable,
258 QueryError::TooLarge { .. } => ResultCode::TooLarge,
259 QueryError::Version { .. } => ResultCode::VersionSkew,
260 QueryError::Stale { .. } => ResultCode::Stale,
261 QueryError::Cancelled { .. } => ResultCode::Cancelled,
262 QueryError::DeadlineExceeded { .. } => ResultCode::DeadlineExceeded,
263 QueryError::ExpiredSnapshot { .. } => ResultCode::ExpiredSnapshot,
264 QueryError::StaleGeneration { .. } => ResultCode::StaleGeneration,
265 QueryError::TargetUnavailable { .. } => ResultCode::TargetUnavailable,
266 QueryError::ResourceLimit { .. } => ResultCode::ResourceLimit,
267 }
268 }
269}
270
271impl From<&KvError> for ResultCode {
272 fn from(error: &KvError) -> Self {
273 match error {
274 KvError::Unsupported(_) => ResultCode::Unsupported,
275 KvError::InvalidKey(_) => ResultCode::InvalidArgument,
276 KvError::InvalidNamespace(_) => ResultCode::InvalidArgument,
277 KvError::TooLarge { .. } => ResultCode::TooLarge,
278 KvError::Backend(_) => ResultCode::Backend,
279 KvError::Unavailable(_) => ResultCode::Unavailable,
280 KvError::Version { .. } => ResultCode::VersionSkew,
281 KvError::VersionConflict { .. } => ResultCode::Conflict,
282 KvError::LeaseLost => ResultCode::Conflict,
283 KvError::NotFound => ResultCode::NotFound,
284 KvError::NotLeader => ResultCode::Unavailable,
285 KvError::Stale { .. } => ResultCode::Stale,
286 }
287 }
288}
289
290impl From<&ForkError> for ResultCode {
291 fn from(error: &ForkError) -> Self {
292 match error {
293 ForkError::Unsupported(_) => ResultCode::Unsupported,
294 ForkError::NotFound(_) => ResultCode::NotFound,
295 ForkError::InvalidFork(_) => ResultCode::InvalidArgument,
296 ForkError::Conflict(_) => ResultCode::Conflict,
297 ForkError::Backend(_) => ResultCode::Backend,
298 ForkError::Unavailable(_) => ResultCode::Unavailable,
299 ForkError::Version { .. } => ResultCode::VersionSkew,
300 ForkError::NotLeader => ResultCode::Unavailable,
301 }
302 }
303}
304
305impl From<&GraphError> for ResultCode {
306 fn from(error: &GraphError) -> Self {
307 match error {
308 GraphError::Unsupported(_) => ResultCode::Unsupported,
309 GraphError::Unauthorized(_) => ResultCode::Forbidden,
310 GraphError::InvalidName(_) => ResultCode::InvalidArgument,
311 GraphError::NotFound(_) => ResultCode::NotFound,
312 GraphError::TooLarge { .. } => ResultCode::TooLarge,
313 GraphError::Backend(_) => ResultCode::Backend,
314 GraphError::Unavailable(_) => ResultCode::Unavailable,
315 GraphError::Version { .. } => ResultCode::VersionSkew,
316 }
317 }
318}
319
320impl From<&AuthzError> for ResultCode {
321 fn from(error: &AuthzError) -> Self {
322 match error {
323 AuthzError::Unsupported(_) => ResultCode::Unsupported,
324 AuthzError::Unauthorized => ResultCode::Forbidden,
325 AuthzError::UnknownRole(_) => ResultCode::NotFound,
326 AuthzError::InvalidName(_) | AuthzError::TenancyViolation(_) => {
327 ResultCode::InvalidArgument
328 }
329 AuthzError::Conflict { .. } => ResultCode::Conflict,
330 AuthzError::Version { .. } => ResultCode::VersionSkew,
331 }
332 }
333}
334
335impl From<&SessionError> for ResultCode {
336 fn from(error: &SessionError) -> Self {
337 match error {
338 SessionError::Unsupported(_) => ResultCode::Unsupported,
339 SessionError::NotFound(_) | SessionError::NotRegistered(_) => ResultCode::NotFound,
340 SessionError::Invalid(_) => ResultCode::InvalidArgument,
341 SessionError::Unauthorized(_) => ResultCode::Forbidden,
342 SessionError::Stale(_) => ResultCode::StaleGeneration,
343 SessionError::Backend(_) => ResultCode::Backend,
344 SessionError::Unavailable(_) => ResultCode::Unavailable,
345 }
346 }
347}
348
349impl From<&CheckpointError> for ResultCode {
350 fn from(error: &CheckpointError) -> Self {
351 match error {
352 CheckpointError::Invalid(_) => ResultCode::InvalidArgument,
353 CheckpointError::NotFound => ResultCode::NotFound,
354 CheckpointError::Conflict { .. } | CheckpointError::LeaseLost => ResultCode::Conflict,
355 CheckpointError::Unauthorized => ResultCode::Forbidden,
356 CheckpointError::Unavailable(_) => ResultCode::Unavailable,
357 CheckpointError::Version { .. } => ResultCode::VersionSkew,
358 }
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn given_result_codes_when_mapped_then_should_round_trip_through_the_numeric_value() {
368 for code in [
369 ResultCode::Ok,
370 ResultCode::Unsupported,
371 ResultCode::NotFound,
372 ResultCode::InvalidArgument,
373 ResultCode::TooLarge,
374 ResultCode::Conflict,
375 ResultCode::Stale,
376 ResultCode::VersionSkew,
377 ResultCode::Unauthenticated,
378 ResultCode::Backend,
379 ResultCode::Forbidden,
380 ResultCode::StepUpRequired,
381 ResultCode::Unavailable,
382 ResultCode::TargetUnavailable,
383 ResultCode::Pending,
384 ResultCode::BackendUnavailable,
385 ] {
386 assert_eq!(ResultCode::from_code(code.code()), code);
387 }
388 assert_eq!(ResultCode::Pending.code(), 19);
389 assert_eq!(ResultCode::BackendUnavailable.code(), 20);
390 assert!(ResultCode::BackendUnavailable.is_retryable());
391 assert!(ResultCode::BackendUnavailable.is_backend_unavailable());
392 assert!(!ResultCode::Unavailable.is_backend_unavailable());
393 assert_eq!(ResultCode::from_code(21), ResultCode::Unrecognized(21));
394 assert_eq!(ResultCode::from_code(900), ResultCode::Unrecognized(900));
396 assert_eq!(ResultCode::Unrecognized(900).code(), 900);
397 }
398
399 #[test]
400 fn given_surface_errors_when_classified_then_should_map_to_the_shared_code() {
401 assert_eq!(
402 ResultCode::from(&QueryError::IndexNotFound("readings".to_owned())),
403 ResultCode::NotFound
404 );
405 assert_eq!(
406 ResultCode::from(&QueryError::Stale {
407 what: "readings".to_owned(),
408 applied: 4,
409 required: 9,
410 }),
411 ResultCode::Stale
412 );
413 assert_eq!(
414 ResultCode::from(&KvError::VersionConflict { current: Some(3) }),
415 ResultCode::Conflict
416 );
417 assert_eq!(
418 ResultCode::from(&ForkError::Conflict("open".to_owned())),
419 ResultCode::Conflict
420 );
421 assert_eq!(
422 ResultCode::from(&KvError::NotLeader),
423 ResultCode::Unavailable
424 );
425 assert_eq!(
426 ResultCode::from(&ForkError::NotLeader),
427 ResultCode::Unavailable
428 );
429 assert_eq!(
430 ResultCode::from(&AuthzError::TenancyViolation("prefix acme".to_owned())),
431 ResultCode::InvalidArgument
432 );
433 assert_eq!(
434 ResultCode::from(&AuthzError::Unauthorized),
435 ResultCode::Forbidden
436 );
437 }
438
439 #[test]
442 fn given_transient_outcomes_when_classified_then_should_be_the_only_retryable_codes() {
443 for surface in [
444 ResultCode::from(&QueryError::Unavailable("dropped".to_owned())),
445 ResultCode::from(&KvError::Unavailable("dropped".to_owned())),
446 ResultCode::from(&ForkError::Unavailable("dropped".to_owned())),
447 ] {
448 assert_eq!(surface, ResultCode::Unavailable);
449 }
450 assert_eq!(ResultCode::Unavailable.http_status(), 503);
451 assert!(ResultCode::Unavailable.is_retryable());
452 assert!(ResultCode::Stale.is_retryable());
453 for code in [
454 ResultCode::Ok,
455 ResultCode::Unsupported,
456 ResultCode::NotFound,
457 ResultCode::InvalidArgument,
458 ResultCode::TooLarge,
459 ResultCode::Conflict,
460 ResultCode::VersionSkew,
461 ResultCode::Unauthenticated,
462 ResultCode::Backend,
463 ResultCode::Forbidden,
464 ResultCode::StepUpRequired,
465 ResultCode::Pending,
466 ResultCode::Unrecognized(900),
467 ] {
468 assert!(
469 !code.is_retryable(),
470 "{code:?} must not invite a blind retry"
471 );
472 }
473 }
474
475 #[test]
476 fn given_result_codes_when_mapped_to_http_then_should_match_the_binding_table() {
477 assert_eq!(ResultCode::NotFound.http_status(), 404);
478 assert_eq!(ResultCode::Unsupported.http_status(), 501);
479 assert_eq!(ResultCode::TooLarge.http_status(), 413);
480 assert_eq!(ResultCode::Conflict.http_status(), 409);
481 assert_eq!(ResultCode::Stale.http_status(), 503);
482 assert_eq!(ResultCode::Unauthenticated.http_status(), 401);
483 assert_eq!(ResultCode::Forbidden.http_status(), 403);
484 assert_eq!(ResultCode::StepUpRequired.http_status(), 403);
485 assert_eq!(ResultCode::Backend.http_status(), 502);
486 assert_eq!(ResultCode::Unrecognized(777).http_status(), 500);
489 assert_eq!(ResultCode::Unrecognized(777).code(), 777);
490 }
491
492 #[cfg(feature = "cbor")]
493 #[test]
494 fn given_a_result_code_when_round_tripped_through_cbor_then_should_preserve_the_variant() {
495 use crate::framing::{decode_named, encode_named};
496 for code in [
497 ResultCode::Ok,
498 ResultCode::Conflict,
499 ResultCode::Stale,
500 ResultCode::Unrecognized(4242),
501 ] {
502 let bytes = encode_named(&code).expect("serializes");
503 let back: ResultCode = decode_named(&bytes).expect("deserializes");
504 assert_eq!(back, code);
505 }
506 }
507
508 #[test]
509 fn given_backend_unavailable_when_serialized_then_should_use_the_backend_unavailable_string() {
510 let json = serde_json::to_string(&ResultCode::BackendUnavailable).expect("serializes");
511 assert_eq!(json, "\"backend_unavailable\"");
512 let back: ResultCode =
513 serde_json::from_str("\"backend_unavailable\"").expect("deserializes");
514 assert_eq!(back, ResultCode::BackendUnavailable);
515 }
516
517 #[cfg(feature = "cbor")]
518 #[test]
519 fn given_a_command_error_when_round_tripped_then_should_preserve_code_and_message() {
520 use crate::framing::{decode_named, encode_named};
521 let error = CommandError::unsupported("AGDX_KV_CAS not served on this build");
522 assert_eq!(error.code, ResultCode::Unsupported);
523 let bytes = encode_named(&error).expect("serializes");
524 let back: CommandError = decode_named(&bytes).expect("deserializes");
525 assert_eq!(back, error);
526 }
527}