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 NotOwner,
69 InvalidState,
74}
75
76impl WireErrorCode {
77 #[must_use]
79 pub const fn as_str(self) -> &'static str {
80 match self {
81 Self::NotFound => "not_found",
82 Self::NamespaceDenied => "namespace_denied",
83 Self::SequenceConflict => "sequence_conflict",
84 Self::UnknownQuery => "unknown_query",
85 Self::QueryTimeout => "query_timeout",
86 Self::NotRunning => "not_running",
87 Self::Lagged => "lagged",
88 Self::InvalidInput => "invalid_input",
89 Self::Backend => "backend",
90 Self::QueryFailed => "query_failed",
91 Self::DeployDenied => "deploy_denied",
92 Self::VersionPinned => "version_pinned",
93 Self::NotOwner => "not_owner",
94 Self::InvalidState => "invalid_state",
95 }
96 }
97}
98
99impl fmt::Display for WireErrorCode {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 formatter.write_str(self.as_str())
102 }
103}
104
105#[derive(thiserror::Error, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
107#[error("{code}: {message}")]
108pub struct WireError {
109 pub code: WireErrorCode,
111 pub message: String,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub error_type: Option<String>,
116}
117
118impl WireError {
119 #[must_use]
121 pub fn new(code: WireErrorCode, message: impl Into<String>) -> Self {
122 Self {
123 code,
124 message: message.into(),
125 error_type: None,
126 }
127 }
128
129 #[must_use]
131 pub fn with_error_type(mut self, error_type: impl Into<String>) -> Self {
132 self.error_type = Some(error_type.into());
133 self
134 }
135
136 #[must_use]
138 pub fn with_optional_error_type(mut self, error_type: Option<String>) -> Self {
139 self.error_type = error_type;
140 self
141 }
142
143 #[must_use]
145 pub fn new_with_type(
146 code: WireErrorCode,
147 error_type: impl Into<String>,
148 message: impl Into<String>,
149 ) -> Self {
150 Self::new(code, message).with_error_type(error_type)
151 }
152
153 #[must_use]
155 pub fn not_found(message: impl Into<String>) -> Self {
156 Self::new(WireErrorCode::NotFound, message)
157 }
158
159 #[must_use]
161 pub fn namespace_denied(message: impl Into<String>) -> Self {
162 Self::new(WireErrorCode::NamespaceDenied, message)
163 }
164
165 #[must_use]
167 pub fn sequence_conflict(message: impl Into<String>) -> Self {
168 Self::new(WireErrorCode::SequenceConflict, message)
169 }
170
171 #[must_use]
173 pub fn unknown_query(message: impl Into<String>) -> Self {
174 Self::new(WireErrorCode::UnknownQuery, message)
175 }
176
177 #[must_use]
179 pub fn query_timeout(message: impl Into<String>) -> Self {
180 Self::new(WireErrorCode::QueryTimeout, message)
181 }
182
183 #[must_use]
185 pub fn not_running(message: impl Into<String>) -> Self {
186 Self::new(WireErrorCode::NotRunning, message)
187 }
188
189 #[must_use]
191 pub fn lagged(message: impl Into<String>) -> Self {
192 Self::new(WireErrorCode::Lagged, message)
193 }
194
195 #[must_use]
197 pub fn invalid_input(message: impl Into<String>) -> Self {
198 Self::new(WireErrorCode::InvalidInput, message)
199 }
200
201 #[must_use]
203 pub fn backend(message: impl Into<String>) -> Self {
204 Self::new(WireErrorCode::Backend, message)
205 }
206
207 #[must_use]
209 pub fn query_failed(message: impl Into<String>) -> Self {
210 Self::new(WireErrorCode::QueryFailed, message)
211 }
212
213 #[must_use]
215 pub fn deploy_denied(message: impl Into<String>) -> Self {
216 Self::new(WireErrorCode::DeployDenied, message)
217 }
218
219 #[must_use]
221 pub fn version_pinned(message: impl Into<String>) -> Self {
222 Self::new(WireErrorCode::VersionPinned, message)
223 }
224
225 #[must_use]
227 pub fn not_owner(message: impl Into<String>) -> Self {
228 Self::new(WireErrorCode::NotOwner, message)
229 }
230
231 #[must_use]
233 pub fn invalid_state(message: impl Into<String>) -> Self {
234 Self::new(WireErrorCode::InvalidState, message)
235 }
236
237 #[must_use]
239 pub fn invalid_state_with_type(
240 error_type: impl Into<String>,
241 message: impl Into<String>,
242 ) -> Self {
243 Self::new_with_type(WireErrorCode::InvalidState, error_type, message)
244 }
245
246 #[must_use]
248 pub fn not_found_with_type(error_type: impl Into<String>, message: impl Into<String>) -> Self {
249 Self::new_with_type(WireErrorCode::NotFound, error_type, message)
250 }
251
252 #[must_use]
254 pub fn not_running_with_type(
255 error_type: impl Into<String>,
256 message: impl Into<String>,
257 ) -> Self {
258 Self::new_with_type(WireErrorCode::NotRunning, error_type, message)
259 }
260
261 #[must_use]
263 pub fn backend_with_type(error_type: impl Into<String>, message: impl Into<String>) -> Self {
264 Self::new_with_type(WireErrorCode::Backend, error_type, message)
265 }
266}
267
268#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, prost::Enumeration)]
270#[repr(i32)]
271pub enum ProtoWireErrorCode {
272 Unspecified = 0,
274 NotFound = 1,
276 NamespaceDenied = 2,
278 SequenceConflict = 3,
280 UnknownQuery = 4,
282 QueryTimeout = 5,
284 NotRunning = 6,
286 Lagged = 7,
288 InvalidInput = 8,
290 Backend = 9,
292 QueryFailed = 10,
294 DeployDenied = 11,
296 VersionPinned = 12,
298 NotOwner = 13,
300 InvalidState = 14,
302}
303
304#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, prost::Message)]
306pub struct ProtoWireError {
307 #[prost(enumeration = "ProtoWireErrorCode", tag = "1")]
309 pub code: i32,
310 #[prost(string, tag = "2")]
312 pub message: String,
313 #[prost(string, optional, tag = "3")]
315 pub error_type: Option<String>,
316}
317
318impl From<WireErrorCode> for ProtoWireErrorCode {
319 fn from(value: WireErrorCode) -> Self {
320 match value {
321 WireErrorCode::NotFound => Self::NotFound,
322 WireErrorCode::NamespaceDenied => Self::NamespaceDenied,
323 WireErrorCode::SequenceConflict => Self::SequenceConflict,
324 WireErrorCode::UnknownQuery => Self::UnknownQuery,
325 WireErrorCode::QueryTimeout => Self::QueryTimeout,
326 WireErrorCode::NotRunning => Self::NotRunning,
327 WireErrorCode::Lagged => Self::Lagged,
328 WireErrorCode::InvalidInput => Self::InvalidInput,
329 WireErrorCode::Backend => Self::Backend,
330 WireErrorCode::QueryFailed => Self::QueryFailed,
331 WireErrorCode::DeployDenied => Self::DeployDenied,
332 WireErrorCode::VersionPinned => Self::VersionPinned,
333 WireErrorCode::NotOwner => Self::NotOwner,
334 WireErrorCode::InvalidState => Self::InvalidState,
335 }
336 }
337}
338
339impl TryFrom<ProtoWireErrorCode> for WireErrorCode {
340 type Error = WireError;
341
342 fn try_from(value: ProtoWireErrorCode) -> Result<Self, Self::Error> {
343 match value {
344 ProtoWireErrorCode::Unspecified => {
345 Err(WireError::backend("wire error code is missing"))
346 }
347 ProtoWireErrorCode::NotFound => Ok(Self::NotFound),
348 ProtoWireErrorCode::NamespaceDenied => Ok(Self::NamespaceDenied),
349 ProtoWireErrorCode::SequenceConflict => Ok(Self::SequenceConflict),
350 ProtoWireErrorCode::UnknownQuery => Ok(Self::UnknownQuery),
351 ProtoWireErrorCode::QueryTimeout => Ok(Self::QueryTimeout),
352 ProtoWireErrorCode::NotRunning => Ok(Self::NotRunning),
353 ProtoWireErrorCode::Lagged => Ok(Self::Lagged),
354 ProtoWireErrorCode::InvalidInput => Ok(Self::InvalidInput),
355 ProtoWireErrorCode::Backend => Ok(Self::Backend),
356 ProtoWireErrorCode::QueryFailed => Ok(Self::QueryFailed),
357 ProtoWireErrorCode::DeployDenied => Ok(Self::DeployDenied),
358 ProtoWireErrorCode::VersionPinned => Ok(Self::VersionPinned),
359 ProtoWireErrorCode::NotOwner => Ok(Self::NotOwner),
360 ProtoWireErrorCode::InvalidState => Ok(Self::InvalidState),
361 }
362 }
363}
364
365impl From<WireError> for ProtoWireError {
366 fn from(value: WireError) -> Self {
367 let code = ProtoWireErrorCode::from(value.code) as i32;
368 Self {
369 code,
370 message: value.message,
371 error_type: value.error_type,
372 }
373 }
374}
375
376impl TryFrom<ProtoWireError> for WireError {
377 type Error = WireError;
378
379 fn try_from(value: ProtoWireError) -> Result<Self, Self::Error> {
380 let code = ProtoWireErrorCode::try_from(value.code)
381 .map_err(|_| WireError::backend("wire error code is unknown"))?;
382 Ok(Self::new(WireErrorCode::try_from(code)?, value.message)
383 .with_optional_error_type(value.error_type))
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::{ProtoWireError, ProtoWireErrorCode, WireError, WireErrorCode};
390
391 fn assert_send_sync<T: Send + Sync>() {}
392
393 const fn next_code(code: WireErrorCode) -> Option<WireErrorCode> {
399 match code {
400 WireErrorCode::NotFound => Some(WireErrorCode::NamespaceDenied),
401 WireErrorCode::NamespaceDenied => Some(WireErrorCode::SequenceConflict),
402 WireErrorCode::SequenceConflict => Some(WireErrorCode::UnknownQuery),
403 WireErrorCode::UnknownQuery => Some(WireErrorCode::QueryTimeout),
404 WireErrorCode::QueryTimeout => Some(WireErrorCode::NotRunning),
405 WireErrorCode::NotRunning => Some(WireErrorCode::Lagged),
406 WireErrorCode::Lagged => Some(WireErrorCode::InvalidInput),
407 WireErrorCode::InvalidInput => Some(WireErrorCode::Backend),
408 WireErrorCode::Backend => Some(WireErrorCode::QueryFailed),
409 WireErrorCode::QueryFailed => Some(WireErrorCode::DeployDenied),
410 WireErrorCode::DeployDenied => Some(WireErrorCode::VersionPinned),
411 WireErrorCode::VersionPinned => Some(WireErrorCode::NotOwner),
412 WireErrorCode::NotOwner => Some(WireErrorCode::InvalidState),
413 WireErrorCode::InvalidState => None,
414 }
415 }
416
417 fn all_codes() -> Vec<WireErrorCode> {
419 let mut codes = vec![WireErrorCode::NotFound];
420 while let Some(&last) = codes.last() {
421 match next_code(last) {
422 Some(next) => codes.push(next),
423 None => break,
424 }
425 }
426 codes
427 }
428
429 #[test]
430 fn wire_error_is_send_sync() {
431 assert_send_sync::<WireError>();
432 }
433
434 #[test]
438 fn proto_numeric_values_are_pinned() {
439 let expected: &[(WireErrorCode, i32)] = &[
440 (WireErrorCode::NotFound, 1),
441 (WireErrorCode::NamespaceDenied, 2),
442 (WireErrorCode::SequenceConflict, 3),
443 (WireErrorCode::UnknownQuery, 4),
444 (WireErrorCode::QueryTimeout, 5),
445 (WireErrorCode::NotRunning, 6),
446 (WireErrorCode::Lagged, 7),
447 (WireErrorCode::InvalidInput, 8),
448 (WireErrorCode::Backend, 9),
449 (WireErrorCode::QueryFailed, 10),
450 (WireErrorCode::DeployDenied, 11),
451 (WireErrorCode::VersionPinned, 12),
452 (WireErrorCode::NotOwner, 13),
453 (WireErrorCode::InvalidState, 14),
454 ];
455 assert_eq!(
456 expected.len(),
457 all_codes().len(),
458 "every WireErrorCode variant must have a pinned numeric value"
459 );
460 for &(code, number) in expected {
461 assert_eq!(
462 ProtoWireErrorCode::from(code) as i32,
463 number,
464 "{code:?} must keep proto enum value {number}",
465 );
466 }
467 }
468
469 #[test]
472 fn string_codes_are_pinned() {
473 let expected: &[(WireErrorCode, &str)] = &[
474 (WireErrorCode::NotFound, "not_found"),
475 (WireErrorCode::NamespaceDenied, "namespace_denied"),
476 (WireErrorCode::SequenceConflict, "sequence_conflict"),
477 (WireErrorCode::UnknownQuery, "unknown_query"),
478 (WireErrorCode::QueryTimeout, "query_timeout"),
479 (WireErrorCode::NotRunning, "not_running"),
480 (WireErrorCode::Lagged, "lagged"),
481 (WireErrorCode::InvalidInput, "invalid_input"),
482 (WireErrorCode::Backend, "backend"),
483 (WireErrorCode::QueryFailed, "query_failed"),
484 (WireErrorCode::DeployDenied, "deploy_denied"),
485 (WireErrorCode::VersionPinned, "version_pinned"),
486 (WireErrorCode::NotOwner, "not_owner"),
487 (WireErrorCode::InvalidState, "invalid_state"),
488 ];
489 assert_eq!(
490 expected.len(),
491 all_codes().len(),
492 "every WireErrorCode variant must have a pinned string code"
493 );
494 for &(code, string) in expected {
495 assert_eq!(code.as_str(), string, "{code:?} must keep code {string}");
496 }
497 }
498
499 #[test]
500 fn json_codes_match_as_str_and_round_trip() -> Result<(), serde_json::Error> {
501 for code in all_codes() {
502 let serialized = serde_json::to_value(code)?;
503 assert_eq!(
504 serialized,
505 serde_json::Value::String(code.as_str().to_owned()),
506 "JSON serialization of {code:?} must equal as_str()",
507 );
508 let deserialized: WireErrorCode =
509 serde_json::from_value(serde_json::Value::String(code.as_str().to_owned()))?;
510 assert_eq!(deserialized, code, "{code:?} must round-trip through JSON");
511
512 let error = WireError::new(code, format!("message for {}", code.as_str()));
513 let body = serde_json::to_value(&error)?;
514 assert_eq!(
515 body.get("code"),
516 Some(&serde_json::Value::String(code.as_str().to_owned())),
517 "WireError JSON body must carry the snake_case code for {code:?}",
518 );
519 let decoded: WireError = serde_json::from_value(body)?;
520 assert_eq!(decoded, error);
521 }
522 Ok(())
523 }
524
525 #[test]
526 fn proto_round_trips_every_code() -> Result<(), WireError> {
527 for code in all_codes() {
528 let error = WireError::new_with_type(
529 code,
530 format!("{}Variant", code.as_str()),
531 format!("message for {}", code.as_str()),
532 );
533 let proto = ProtoWireError::from(error.clone());
534 let decoded = WireError::try_from(proto)?;
535 assert_eq!(decoded, error);
536 }
537
538 Ok(())
539 }
540
541 #[test]
542 fn rejects_unspecified_proto_code() {
543 let proto = ProtoWireError {
544 code: 0,
545 message: String::from("missing"),
546 error_type: None,
547 };
548
549 let result = WireError::try_from(proto);
550 assert_eq!(
551 result,
552 Err(WireError::backend("wire error code is missing"))
553 );
554 }
555
556 #[test]
557 fn representative_documented_mappings_use_stable_codes() {
558 let engine_unknown_workflow = WireError::not_found("workflow was not found");
559 let store_sequence_conflict = WireError::sequence_conflict("event sequence conflicted");
560
561 assert_eq!(engine_unknown_workflow.code, WireErrorCode::NotFound);
562 assert_eq!(
563 store_sequence_conflict.code,
564 WireErrorCode::SequenceConflict
565 );
566 assert_eq!(
567 WireError::namespace_denied("denied").code,
568 WireErrorCode::NamespaceDenied
569 );
570 assert_eq!(
571 WireError::query_timeout("timeout").code,
572 WireErrorCode::QueryTimeout
573 );
574 assert_eq!(
575 WireError::unknown_query("unknown").code,
576 WireErrorCode::UnknownQuery
577 );
578 assert_eq!(
579 WireError::not_running("terminal").code,
580 WireErrorCode::NotRunning
581 );
582 assert_eq!(
583 WireError::invalid_input("malformed").code,
584 WireErrorCode::InvalidInput
585 );
586 assert_eq!(
587 WireError::query_failed("handler raised").code,
588 WireErrorCode::QueryFailed
589 );
590 assert_eq!(
591 WireError::deploy_denied("no deploy grant").code,
592 WireErrorCode::DeployDenied
593 );
594 assert_eq!(
595 WireError::version_pinned("pinned by live run").code,
596 WireErrorCode::VersionPinned
597 );
598 assert_eq!(
599 WireError::not_owner("wrong shard owner").code,
600 WireErrorCode::NotOwner
601 );
602 assert_eq!(
603 WireError::invalid_state("run is not reopenable").code,
604 WireErrorCode::InvalidState
605 );
606 }
607}