1use crate::agent_workflow::AgentError;
11use crate::fork::ForkError;
12use crate::kv::KvError;
13use crate::query::QueryError;
14use serde::{Deserialize, Serialize};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21#[non_exhaustive]
22pub enum ResultCode {
23 Ok,
25 Unsupported,
27 NotFound,
29 InvalidArgument,
31 TooLarge,
33 Conflict,
36 Stale,
39 VersionSkew,
41 Unauthenticated,
43 Backend,
45 Forbidden,
47 StepUpRequired,
50 Unrecognized(u16),
56}
57
58impl ResultCode {
59 pub const fn code(self) -> u16 {
61 match self {
62 ResultCode::Ok => 0,
63 ResultCode::Unsupported => 1,
64 ResultCode::NotFound => 2,
65 ResultCode::InvalidArgument => 3,
66 ResultCode::TooLarge => 4,
67 ResultCode::Conflict => 5,
68 ResultCode::Stale => 6,
69 ResultCode::VersionSkew => 7,
70 ResultCode::Unauthenticated => 8,
71 ResultCode::Backend => 9,
72 ResultCode::Forbidden => 10,
73 ResultCode::StepUpRequired => 11,
74 ResultCode::Unrecognized(code) => code,
75 }
76 }
77
78 pub const fn from_code(code: u16) -> Self {
81 match code {
82 0 => ResultCode::Ok,
83 1 => ResultCode::Unsupported,
84 2 => ResultCode::NotFound,
85 3 => ResultCode::InvalidArgument,
86 4 => ResultCode::TooLarge,
87 5 => ResultCode::Conflict,
88 6 => ResultCode::Stale,
89 7 => ResultCode::VersionSkew,
90 8 => ResultCode::Unauthenticated,
91 9 => ResultCode::Backend,
92 10 => ResultCode::Forbidden,
93 11 => ResultCode::StepUpRequired,
94 other => ResultCode::Unrecognized(other),
95 }
96 }
97
98 pub const fn http_status(self) -> u16 {
101 match self {
102 ResultCode::Ok => 200,
103 ResultCode::Unsupported => 501,
104 ResultCode::NotFound => 404,
105 ResultCode::InvalidArgument => 400,
106 ResultCode::TooLarge => 413,
107 ResultCode::Conflict => 409,
108 ResultCode::Stale => 503,
109 ResultCode::VersionSkew => 400,
110 ResultCode::Unauthenticated => 401,
111 ResultCode::Backend => 502,
112 ResultCode::Forbidden => 403,
116 ResultCode::StepUpRequired => 403,
117 ResultCode::Unrecognized(_) => 500,
118 }
119 }
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
133pub struct CommandError {
134 pub code: ResultCode,
135 pub message: String,
136}
137
138impl CommandError {
139 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
141 Self {
142 code,
143 message: message.into(),
144 }
145 }
146
147 pub fn unsupported(message: impl Into<String>) -> Self {
149 Self::new(ResultCode::Unsupported, message)
150 }
151}
152
153impl From<&QueryError> for ResultCode {
154 fn from(error: &QueryError) -> Self {
155 match error {
156 QueryError::Unsupported(_) => ResultCode::Unsupported,
157 QueryError::Unauthorized(_) => ResultCode::Forbidden,
160 QueryError::IndexNotFound(_) | QueryError::ForkNotFound(_) => ResultCode::NotFound,
161 QueryError::Backend(_) => ResultCode::Backend,
162 QueryError::TooLarge { .. } => ResultCode::TooLarge,
163 QueryError::Version { .. } => ResultCode::VersionSkew,
164 QueryError::Stale { .. } => ResultCode::Stale,
165 }
166 }
167}
168
169impl From<&KvError> for ResultCode {
170 fn from(error: &KvError) -> Self {
171 match error {
172 KvError::Unsupported(_) => ResultCode::Unsupported,
173 KvError::InvalidKey(_) => ResultCode::InvalidArgument,
174 KvError::InvalidNamespace(_) => ResultCode::InvalidArgument,
175 KvError::TooLarge { .. } => ResultCode::TooLarge,
176 KvError::Backend(_) => ResultCode::Backend,
177 KvError::Version { .. } => ResultCode::VersionSkew,
178 KvError::VersionConflict { .. } => ResultCode::Conflict,
179 KvError::LeaseLost => ResultCode::Conflict,
180 KvError::NotFound => ResultCode::NotFound,
181 KvError::NotLeader => ResultCode::Backend,
182 }
183 }
184}
185
186impl From<&ForkError> for ResultCode {
187 fn from(error: &ForkError) -> Self {
188 match error {
189 ForkError::Unsupported(_) => ResultCode::Unsupported,
190 ForkError::NotFound(_) => ResultCode::NotFound,
191 ForkError::InvalidFork(_) => ResultCode::InvalidArgument,
192 ForkError::Conflict(_) => ResultCode::Conflict,
193 ForkError::Backend(_) => ResultCode::Backend,
194 ForkError::Version { .. } => ResultCode::VersionSkew,
195 ForkError::NotLeader => ResultCode::Backend,
196 }
197 }
198}
199
200impl From<&AgentError> for ResultCode {
201 fn from(error: &AgentError) -> Self {
202 match error {
203 AgentError::Unsupported(_) => ResultCode::Unsupported,
204 AgentError::NotFound(_) => ResultCode::NotFound,
205 AgentError::Invalid(_) => ResultCode::InvalidArgument,
206 AgentError::Backend(_) => ResultCode::Backend,
207 AgentError::Version { .. } => ResultCode::VersionSkew,
208 AgentError::NotLeader => ResultCode::Backend,
209 }
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn given_result_codes_when_mapped_then_should_round_trip_through_the_numeric_value() {
219 for code in [
220 ResultCode::Ok,
221 ResultCode::Unsupported,
222 ResultCode::NotFound,
223 ResultCode::InvalidArgument,
224 ResultCode::TooLarge,
225 ResultCode::Conflict,
226 ResultCode::Stale,
227 ResultCode::VersionSkew,
228 ResultCode::Unauthenticated,
229 ResultCode::Backend,
230 ResultCode::Forbidden,
231 ResultCode::StepUpRequired,
232 ] {
233 assert_eq!(ResultCode::from_code(code.code()), code);
234 }
235 assert_eq!(ResultCode::from_code(900), ResultCode::Unrecognized(900));
237 assert_eq!(ResultCode::Unrecognized(900).code(), 900);
238 }
239
240 #[test]
241 fn given_surface_errors_when_classified_then_should_map_to_the_shared_code() {
242 assert_eq!(
243 ResultCode::from(&QueryError::IndexNotFound("orders".to_owned())),
244 ResultCode::NotFound
245 );
246 assert_eq!(
247 ResultCode::from(&QueryError::Stale {
248 what: "orders".to_owned(),
249 applied: 4,
250 required: 9,
251 }),
252 ResultCode::Stale
253 );
254 assert_eq!(
255 ResultCode::from(&KvError::VersionConflict { current: Some(3) }),
256 ResultCode::Conflict
257 );
258 assert_eq!(
259 ResultCode::from(&ForkError::Conflict("open".to_owned())),
260 ResultCode::Conflict
261 );
262 assert_eq!(ResultCode::from(&KvError::NotLeader), ResultCode::Backend);
263 assert_eq!(ResultCode::from(&ForkError::NotLeader), ResultCode::Backend);
264 assert_eq!(
265 ResultCode::from(&AgentError::NotLeader),
266 ResultCode::Backend
267 );
268 }
269
270 #[test]
271 fn given_result_codes_when_mapped_to_http_then_should_match_the_binding_table() {
272 assert_eq!(ResultCode::NotFound.http_status(), 404);
273 assert_eq!(ResultCode::Unsupported.http_status(), 501);
274 assert_eq!(ResultCode::TooLarge.http_status(), 413);
275 assert_eq!(ResultCode::Conflict.http_status(), 409);
276 assert_eq!(ResultCode::Stale.http_status(), 503);
277 assert_eq!(ResultCode::Unauthenticated.http_status(), 401);
278 assert_eq!(ResultCode::Forbidden.http_status(), 403);
279 assert_eq!(ResultCode::StepUpRequired.http_status(), 403);
280 assert_eq!(ResultCode::Backend.http_status(), 502);
281 assert_eq!(ResultCode::Unrecognized(777).http_status(), 500);
284 assert_eq!(ResultCode::Unrecognized(777).code(), 777);
285 }
286
287 #[cfg(feature = "cbor")]
288 #[test]
289 fn given_a_result_code_when_round_tripped_through_cbor_then_should_preserve_the_variant() {
290 use crate::framing::{decode_named, encode_named};
291 for code in [
292 ResultCode::Ok,
293 ResultCode::Conflict,
294 ResultCode::Stale,
295 ResultCode::Unrecognized(4242),
296 ] {
297 let bytes = encode_named(&code).expect("serializes");
298 let back: ResultCode = decode_named(&bytes).expect("deserializes");
299 assert_eq!(back, code);
300 }
301 }
302
303 #[cfg(feature = "cbor")]
304 #[test]
305 fn given_a_command_error_when_round_tripped_then_should_preserve_code_and_message() {
306 use crate::framing::{decode_named, encode_named};
307 let error = CommandError::unsupported("AGDX_KV_CAS not served on this build");
308 assert_eq!(error.code, ResultCode::Unsupported);
309 let bytes = encode_named(&error).expect("serializes");
310 let back: CommandError = decode_named(&bytes).expect("deserializes");
311 assert_eq!(back, error);
312 }
313}