1use std::fmt;
27
28use serde::{Deserialize, Serialize};
29
30#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
38#[serde(rename_all = "snake_case")]
39pub enum WireErrorCode {
40 NotFound,
42 NamespaceDenied,
44 SequenceConflict,
46 UnknownQuery,
48 QueryTimeout,
50 NotRunning,
52 Lagged,
54 InvalidInput,
56 Backend,
58 QueryFailed,
60 DeployDenied,
62 VersionPinned,
65}
66
67impl WireErrorCode {
68 #[must_use]
70 pub const fn as_str(self) -> &'static str {
71 match self {
72 Self::NotFound => "not_found",
73 Self::NamespaceDenied => "namespace_denied",
74 Self::SequenceConflict => "sequence_conflict",
75 Self::UnknownQuery => "unknown_query",
76 Self::QueryTimeout => "query_timeout",
77 Self::NotRunning => "not_running",
78 Self::Lagged => "lagged",
79 Self::InvalidInput => "invalid_input",
80 Self::Backend => "backend",
81 Self::QueryFailed => "query_failed",
82 Self::DeployDenied => "deploy_denied",
83 Self::VersionPinned => "version_pinned",
84 }
85 }
86}
87
88impl fmt::Display for WireErrorCode {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 formatter.write_str(self.as_str())
91 }
92}
93
94#[derive(thiserror::Error, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
96#[error("{code}: {message}")]
97pub struct WireError {
98 pub code: WireErrorCode,
100 pub message: String,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub error_type: Option<String>,
105}
106
107impl WireError {
108 #[must_use]
110 pub fn new(code: WireErrorCode, message: impl Into<String>) -> Self {
111 Self {
112 code,
113 message: message.into(),
114 error_type: None,
115 }
116 }
117
118 #[must_use]
120 pub fn with_error_type(mut self, error_type: impl Into<String>) -> Self {
121 self.error_type = Some(error_type.into());
122 self
123 }
124
125 #[must_use]
127 pub fn with_optional_error_type(mut self, error_type: Option<String>) -> Self {
128 self.error_type = error_type;
129 self
130 }
131
132 #[must_use]
134 pub fn new_with_type(
135 code: WireErrorCode,
136 error_type: impl Into<String>,
137 message: impl Into<String>,
138 ) -> Self {
139 Self::new(code, message).with_error_type(error_type)
140 }
141
142 #[must_use]
144 pub fn not_found(message: impl Into<String>) -> Self {
145 Self::new(WireErrorCode::NotFound, message)
146 }
147
148 #[must_use]
150 pub fn namespace_denied(message: impl Into<String>) -> Self {
151 Self::new(WireErrorCode::NamespaceDenied, message)
152 }
153
154 #[must_use]
156 pub fn sequence_conflict(message: impl Into<String>) -> Self {
157 Self::new(WireErrorCode::SequenceConflict, message)
158 }
159
160 #[must_use]
162 pub fn unknown_query(message: impl Into<String>) -> Self {
163 Self::new(WireErrorCode::UnknownQuery, message)
164 }
165
166 #[must_use]
168 pub fn query_timeout(message: impl Into<String>) -> Self {
169 Self::new(WireErrorCode::QueryTimeout, message)
170 }
171
172 #[must_use]
174 pub fn not_running(message: impl Into<String>) -> Self {
175 Self::new(WireErrorCode::NotRunning, message)
176 }
177
178 #[must_use]
180 pub fn lagged(message: impl Into<String>) -> Self {
181 Self::new(WireErrorCode::Lagged, message)
182 }
183
184 #[must_use]
186 pub fn invalid_input(message: impl Into<String>) -> Self {
187 Self::new(WireErrorCode::InvalidInput, message)
188 }
189
190 #[must_use]
192 pub fn backend(message: impl Into<String>) -> Self {
193 Self::new(WireErrorCode::Backend, message)
194 }
195
196 #[must_use]
198 pub fn query_failed(message: impl Into<String>) -> Self {
199 Self::new(WireErrorCode::QueryFailed, message)
200 }
201
202 #[must_use]
204 pub fn deploy_denied(message: impl Into<String>) -> Self {
205 Self::new(WireErrorCode::DeployDenied, message)
206 }
207
208 #[must_use]
210 pub fn version_pinned(message: impl Into<String>) -> Self {
211 Self::new(WireErrorCode::VersionPinned, message)
212 }
213
214 #[must_use]
216 pub fn not_found_with_type(error_type: impl Into<String>, message: impl Into<String>) -> Self {
217 Self::new_with_type(WireErrorCode::NotFound, error_type, message)
218 }
219
220 #[must_use]
222 pub fn not_running_with_type(
223 error_type: impl Into<String>,
224 message: impl Into<String>,
225 ) -> Self {
226 Self::new_with_type(WireErrorCode::NotRunning, error_type, message)
227 }
228
229 #[must_use]
231 pub fn backend_with_type(error_type: impl Into<String>, message: impl Into<String>) -> Self {
232 Self::new_with_type(WireErrorCode::Backend, error_type, message)
233 }
234}
235
236#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, prost::Enumeration)]
238#[repr(i32)]
239pub enum ProtoWireErrorCode {
240 Unspecified = 0,
242 NotFound = 1,
244 NamespaceDenied = 2,
246 SequenceConflict = 3,
248 UnknownQuery = 4,
250 QueryTimeout = 5,
252 NotRunning = 6,
254 Lagged = 7,
256 InvalidInput = 8,
258 Backend = 9,
260 QueryFailed = 10,
262 DeployDenied = 11,
264 VersionPinned = 12,
266}
267
268#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, prost::Message)]
270pub struct ProtoWireError {
271 #[prost(enumeration = "ProtoWireErrorCode", tag = "1")]
273 pub code: i32,
274 #[prost(string, tag = "2")]
276 pub message: String,
277 #[prost(string, optional, tag = "3")]
279 pub error_type: Option<String>,
280}
281
282impl From<WireErrorCode> for ProtoWireErrorCode {
283 fn from(value: WireErrorCode) -> Self {
284 match value {
285 WireErrorCode::NotFound => Self::NotFound,
286 WireErrorCode::NamespaceDenied => Self::NamespaceDenied,
287 WireErrorCode::SequenceConflict => Self::SequenceConflict,
288 WireErrorCode::UnknownQuery => Self::UnknownQuery,
289 WireErrorCode::QueryTimeout => Self::QueryTimeout,
290 WireErrorCode::NotRunning => Self::NotRunning,
291 WireErrorCode::Lagged => Self::Lagged,
292 WireErrorCode::InvalidInput => Self::InvalidInput,
293 WireErrorCode::Backend => Self::Backend,
294 WireErrorCode::QueryFailed => Self::QueryFailed,
295 WireErrorCode::DeployDenied => Self::DeployDenied,
296 WireErrorCode::VersionPinned => Self::VersionPinned,
297 }
298 }
299}
300
301impl TryFrom<ProtoWireErrorCode> for WireErrorCode {
302 type Error = WireError;
303
304 fn try_from(value: ProtoWireErrorCode) -> Result<Self, Self::Error> {
305 match value {
306 ProtoWireErrorCode::Unspecified => {
307 Err(WireError::backend("wire error code is missing"))
308 }
309 ProtoWireErrorCode::NotFound => Ok(Self::NotFound),
310 ProtoWireErrorCode::NamespaceDenied => Ok(Self::NamespaceDenied),
311 ProtoWireErrorCode::SequenceConflict => Ok(Self::SequenceConflict),
312 ProtoWireErrorCode::UnknownQuery => Ok(Self::UnknownQuery),
313 ProtoWireErrorCode::QueryTimeout => Ok(Self::QueryTimeout),
314 ProtoWireErrorCode::NotRunning => Ok(Self::NotRunning),
315 ProtoWireErrorCode::Lagged => Ok(Self::Lagged),
316 ProtoWireErrorCode::InvalidInput => Ok(Self::InvalidInput),
317 ProtoWireErrorCode::Backend => Ok(Self::Backend),
318 ProtoWireErrorCode::QueryFailed => Ok(Self::QueryFailed),
319 ProtoWireErrorCode::DeployDenied => Ok(Self::DeployDenied),
320 ProtoWireErrorCode::VersionPinned => Ok(Self::VersionPinned),
321 }
322 }
323}
324
325impl From<WireError> for ProtoWireError {
326 fn from(value: WireError) -> Self {
327 let code = ProtoWireErrorCode::from(value.code) as i32;
328 Self {
329 code,
330 message: value.message,
331 error_type: value.error_type,
332 }
333 }
334}
335
336impl TryFrom<ProtoWireError> for WireError {
337 type Error = WireError;
338
339 fn try_from(value: ProtoWireError) -> Result<Self, Self::Error> {
340 let code = ProtoWireErrorCode::try_from(value.code)
341 .map_err(|_| WireError::backend("wire error code is unknown"))?;
342 Ok(Self::new(WireErrorCode::try_from(code)?, value.message)
343 .with_optional_error_type(value.error_type))
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::{ProtoWireError, ProtoWireErrorCode, WireError, WireErrorCode};
350
351 fn assert_send_sync<T: Send + Sync>() {}
352
353 const fn next_code(code: WireErrorCode) -> Option<WireErrorCode> {
359 match code {
360 WireErrorCode::NotFound => Some(WireErrorCode::NamespaceDenied),
361 WireErrorCode::NamespaceDenied => Some(WireErrorCode::SequenceConflict),
362 WireErrorCode::SequenceConflict => Some(WireErrorCode::UnknownQuery),
363 WireErrorCode::UnknownQuery => Some(WireErrorCode::QueryTimeout),
364 WireErrorCode::QueryTimeout => Some(WireErrorCode::NotRunning),
365 WireErrorCode::NotRunning => Some(WireErrorCode::Lagged),
366 WireErrorCode::Lagged => Some(WireErrorCode::InvalidInput),
367 WireErrorCode::InvalidInput => Some(WireErrorCode::Backend),
368 WireErrorCode::Backend => Some(WireErrorCode::QueryFailed),
369 WireErrorCode::QueryFailed => Some(WireErrorCode::DeployDenied),
370 WireErrorCode::DeployDenied => Some(WireErrorCode::VersionPinned),
371 WireErrorCode::VersionPinned => None,
372 }
373 }
374
375 fn all_codes() -> Vec<WireErrorCode> {
377 let mut codes = vec![WireErrorCode::NotFound];
378 while let Some(&last) = codes.last() {
379 match next_code(last) {
380 Some(next) => codes.push(next),
381 None => break,
382 }
383 }
384 codes
385 }
386
387 #[test]
388 fn wire_error_is_send_sync() {
389 assert_send_sync::<WireError>();
390 }
391
392 #[test]
396 fn proto_numeric_values_are_pinned() {
397 let expected: &[(WireErrorCode, i32)] = &[
398 (WireErrorCode::NotFound, 1),
399 (WireErrorCode::NamespaceDenied, 2),
400 (WireErrorCode::SequenceConflict, 3),
401 (WireErrorCode::UnknownQuery, 4),
402 (WireErrorCode::QueryTimeout, 5),
403 (WireErrorCode::NotRunning, 6),
404 (WireErrorCode::Lagged, 7),
405 (WireErrorCode::InvalidInput, 8),
406 (WireErrorCode::Backend, 9),
407 (WireErrorCode::QueryFailed, 10),
408 (WireErrorCode::DeployDenied, 11),
409 (WireErrorCode::VersionPinned, 12),
410 ];
411 assert_eq!(
412 expected.len(),
413 all_codes().len(),
414 "every WireErrorCode variant must have a pinned numeric value"
415 );
416 for &(code, number) in expected {
417 assert_eq!(
418 ProtoWireErrorCode::from(code) as i32,
419 number,
420 "{code:?} must keep proto enum value {number}",
421 );
422 }
423 }
424
425 #[test]
428 fn string_codes_are_pinned() {
429 let expected: &[(WireErrorCode, &str)] = &[
430 (WireErrorCode::NotFound, "not_found"),
431 (WireErrorCode::NamespaceDenied, "namespace_denied"),
432 (WireErrorCode::SequenceConflict, "sequence_conflict"),
433 (WireErrorCode::UnknownQuery, "unknown_query"),
434 (WireErrorCode::QueryTimeout, "query_timeout"),
435 (WireErrorCode::NotRunning, "not_running"),
436 (WireErrorCode::Lagged, "lagged"),
437 (WireErrorCode::InvalidInput, "invalid_input"),
438 (WireErrorCode::Backend, "backend"),
439 (WireErrorCode::QueryFailed, "query_failed"),
440 (WireErrorCode::DeployDenied, "deploy_denied"),
441 (WireErrorCode::VersionPinned, "version_pinned"),
442 ];
443 assert_eq!(
444 expected.len(),
445 all_codes().len(),
446 "every WireErrorCode variant must have a pinned string code"
447 );
448 for &(code, string) in expected {
449 assert_eq!(code.as_str(), string, "{code:?} must keep code {string}");
450 }
451 }
452
453 #[test]
454 fn json_codes_match_as_str_and_round_trip() -> Result<(), serde_json::Error> {
455 for code in all_codes() {
456 let serialized = serde_json::to_value(code)?;
457 assert_eq!(
458 serialized,
459 serde_json::Value::String(code.as_str().to_owned()),
460 "JSON serialization of {code:?} must equal as_str()",
461 );
462 let deserialized: WireErrorCode =
463 serde_json::from_value(serde_json::Value::String(code.as_str().to_owned()))?;
464 assert_eq!(deserialized, code, "{code:?} must round-trip through JSON");
465
466 let error = WireError::new(code, format!("message for {}", code.as_str()));
467 let body = serde_json::to_value(&error)?;
468 assert_eq!(
469 body.get("code"),
470 Some(&serde_json::Value::String(code.as_str().to_owned())),
471 "WireError JSON body must carry the snake_case code for {code:?}",
472 );
473 let decoded: WireError = serde_json::from_value(body)?;
474 assert_eq!(decoded, error);
475 }
476 Ok(())
477 }
478
479 #[test]
480 fn proto_round_trips_every_code() -> Result<(), WireError> {
481 for code in all_codes() {
482 let error = WireError::new_with_type(
483 code,
484 format!("{}Variant", code.as_str()),
485 format!("message for {}", code.as_str()),
486 );
487 let proto = ProtoWireError::from(error.clone());
488 let decoded = WireError::try_from(proto)?;
489 assert_eq!(decoded, error);
490 }
491
492 Ok(())
493 }
494
495 #[test]
496 fn rejects_unspecified_proto_code() {
497 let proto = ProtoWireError {
498 code: 0,
499 message: String::from("missing"),
500 error_type: None,
501 };
502
503 let result = WireError::try_from(proto);
504 assert_eq!(
505 result,
506 Err(WireError::backend("wire error code is missing"))
507 );
508 }
509
510 #[test]
511 fn representative_documented_mappings_use_stable_codes() {
512 let engine_unknown_workflow = WireError::not_found("workflow was not found");
513 let store_sequence_conflict = WireError::sequence_conflict("event sequence conflicted");
514
515 assert_eq!(engine_unknown_workflow.code, WireErrorCode::NotFound);
516 assert_eq!(
517 store_sequence_conflict.code,
518 WireErrorCode::SequenceConflict
519 );
520 assert_eq!(
521 WireError::namespace_denied("denied").code,
522 WireErrorCode::NamespaceDenied
523 );
524 assert_eq!(
525 WireError::query_timeout("timeout").code,
526 WireErrorCode::QueryTimeout
527 );
528 assert_eq!(
529 WireError::unknown_query("unknown").code,
530 WireErrorCode::UnknownQuery
531 );
532 assert_eq!(
533 WireError::not_running("terminal").code,
534 WireErrorCode::NotRunning
535 );
536 assert_eq!(
537 WireError::invalid_input("malformed").code,
538 WireErrorCode::InvalidInput
539 );
540 assert_eq!(
541 WireError::query_failed("handler raised").code,
542 WireErrorCode::QueryFailed
543 );
544 assert_eq!(
545 WireError::deploy_denied("no deploy grant").code,
546 WireErrorCode::DeployDenied
547 );
548 assert_eq!(
549 WireError::version_pinned("pinned by live run").code,
550 WireErrorCode::VersionPinned
551 );
552 }
553}