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 }
182 }
183}
184
185impl From<&ForkError> for ResultCode {
186 fn from(error: &ForkError) -> Self {
187 match error {
188 ForkError::Unsupported(_) => ResultCode::Unsupported,
189 ForkError::NotFound(_) => ResultCode::NotFound,
190 ForkError::InvalidFork(_) => ResultCode::InvalidArgument,
191 ForkError::Conflict(_) => ResultCode::Conflict,
192 ForkError::Backend(_) => ResultCode::Backend,
193 ForkError::Version { .. } => ResultCode::VersionSkew,
194 }
195 }
196}
197
198impl From<&AgentError> for ResultCode {
199 fn from(error: &AgentError) -> Self {
200 match error {
201 AgentError::Unsupported(_) => ResultCode::Unsupported,
202 AgentError::NotFound(_) => ResultCode::NotFound,
203 AgentError::Invalid(_) => ResultCode::InvalidArgument,
204 AgentError::Backend(_) => ResultCode::Backend,
205 AgentError::Version { .. } => ResultCode::VersionSkew,
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn given_result_codes_when_mapped_then_should_round_trip_through_the_numeric_value() {
216 for code in [
217 ResultCode::Ok,
218 ResultCode::Unsupported,
219 ResultCode::NotFound,
220 ResultCode::InvalidArgument,
221 ResultCode::TooLarge,
222 ResultCode::Conflict,
223 ResultCode::Stale,
224 ResultCode::VersionSkew,
225 ResultCode::Unauthenticated,
226 ResultCode::Backend,
227 ResultCode::Forbidden,
228 ResultCode::StepUpRequired,
229 ] {
230 assert_eq!(ResultCode::from_code(code.code()), code);
231 }
232 assert_eq!(ResultCode::from_code(900), ResultCode::Unrecognized(900));
234 assert_eq!(ResultCode::Unrecognized(900).code(), 900);
235 }
236
237 #[test]
238 fn given_surface_errors_when_classified_then_should_map_to_the_shared_code() {
239 assert_eq!(
240 ResultCode::from(&QueryError::IndexNotFound("orders".to_owned())),
241 ResultCode::NotFound
242 );
243 assert_eq!(
244 ResultCode::from(&QueryError::Stale {
245 what: "orders".to_owned(),
246 applied: 4,
247 required: 9,
248 }),
249 ResultCode::Stale
250 );
251 assert_eq!(
252 ResultCode::from(&KvError::VersionConflict { current: Some(3) }),
253 ResultCode::Conflict
254 );
255 assert_eq!(
256 ResultCode::from(&ForkError::Conflict("open".to_owned())),
257 ResultCode::Conflict
258 );
259 }
260
261 #[test]
262 fn given_result_codes_when_mapped_to_http_then_should_match_the_binding_table() {
263 assert_eq!(ResultCode::NotFound.http_status(), 404);
264 assert_eq!(ResultCode::Unsupported.http_status(), 501);
265 assert_eq!(ResultCode::TooLarge.http_status(), 413);
266 assert_eq!(ResultCode::Conflict.http_status(), 409);
267 assert_eq!(ResultCode::Stale.http_status(), 503);
268 assert_eq!(ResultCode::Unauthenticated.http_status(), 401);
269 assert_eq!(ResultCode::Forbidden.http_status(), 403);
270 assert_eq!(ResultCode::StepUpRequired.http_status(), 403);
271 assert_eq!(ResultCode::Backend.http_status(), 502);
272 assert_eq!(ResultCode::Unrecognized(777).http_status(), 500);
275 assert_eq!(ResultCode::Unrecognized(777).code(), 777);
276 }
277
278 #[cfg(feature = "cbor")]
279 #[test]
280 fn given_a_result_code_when_round_tripped_through_cbor_then_should_preserve_the_variant() {
281 use crate::framing::{decode_named, encode_named};
282 for code in [
283 ResultCode::Ok,
284 ResultCode::Conflict,
285 ResultCode::Stale,
286 ResultCode::Unrecognized(4242),
287 ] {
288 let bytes = encode_named(&code).expect("serializes");
289 let back: ResultCode = decode_named(&bytes).expect("deserializes");
290 assert_eq!(back, code);
291 }
292 }
293
294 #[cfg(feature = "cbor")]
295 #[test]
296 fn given_a_command_error_when_round_tripped_then_should_preserve_code_and_message() {
297 use crate::framing::{decode_named, encode_named};
298 let error = CommandError::unsupported("AGDX_KV_CAS not served on this build");
299 assert_eq!(error.code, ResultCode::Unsupported);
300 let bytes = encode_named(&error).expect("serializes");
301 let back: CommandError = decode_named(&bytes).expect("deserializes");
302 assert_eq!(back, error);
303 }
304}