1use std::fmt;
29
30use serde::{Deserialize, Serialize};
31
32#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
40#[serde(rename_all = "snake_case")]
41pub enum WireErrorCode {
42 NotFound,
44 NamespaceDenied,
46 SequenceConflict,
48 UnknownQuery,
50 QueryTimeout,
52 NotRunning,
54 Lagged,
56 InvalidInput,
58 Backend,
60 QueryFailed,
62 DeployDenied,
64 GrantDenied,
70 VersionPinned,
73 NotOwner,
77 InvalidState,
82}
83
84impl WireErrorCode {
85 #[must_use]
87 pub const fn as_str(self) -> &'static str {
88 match self {
89 Self::NotFound => "not_found",
90 Self::NamespaceDenied => "namespace_denied",
91 Self::SequenceConflict => "sequence_conflict",
92 Self::UnknownQuery => "unknown_query",
93 Self::QueryTimeout => "query_timeout",
94 Self::NotRunning => "not_running",
95 Self::Lagged => "lagged",
96 Self::InvalidInput => "invalid_input",
97 Self::Backend => "backend",
98 Self::QueryFailed => "query_failed",
99 Self::DeployDenied => "deploy_denied",
100 Self::GrantDenied => "grant_denied",
101 Self::VersionPinned => "version_pinned",
102 Self::NotOwner => "not_owner",
103 Self::InvalidState => "invalid_state",
104 }
105 }
106}
107
108impl fmt::Display for WireErrorCode {
109 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110 formatter.write_str(self.as_str())
111 }
112}
113
114#[derive(thiserror::Error, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
116#[error("{code}: {message}")]
117pub struct WireError {
118 pub code: WireErrorCode,
120 pub message: String,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub error_type: Option<String>,
125}
126
127impl WireError {
128 #[must_use]
130 pub fn new(code: WireErrorCode, message: impl Into<String>) -> Self {
131 Self {
132 code,
133 message: message.into(),
134 error_type: None,
135 }
136 }
137
138 #[must_use]
140 pub fn with_error_type(mut self, error_type: impl Into<String>) -> Self {
141 self.error_type = Some(error_type.into());
142 self
143 }
144
145 #[must_use]
147 pub fn with_optional_error_type(mut self, error_type: Option<String>) -> Self {
148 self.error_type = error_type;
149 self
150 }
151
152 #[must_use]
154 pub fn new_with_type(
155 code: WireErrorCode,
156 error_type: impl Into<String>,
157 message: impl Into<String>,
158 ) -> Self {
159 Self::new(code, message).with_error_type(error_type)
160 }
161
162 #[must_use]
164 pub fn not_found(message: impl Into<String>) -> Self {
165 Self::new(WireErrorCode::NotFound, message)
166 }
167
168 #[must_use]
170 pub fn namespace_denied(message: impl Into<String>) -> Self {
171 Self::new(WireErrorCode::NamespaceDenied, message)
172 }
173
174 #[must_use]
176 pub fn sequence_conflict(message: impl Into<String>) -> Self {
177 Self::new(WireErrorCode::SequenceConflict, message)
178 }
179
180 #[must_use]
182 pub fn unknown_query(message: impl Into<String>) -> Self {
183 Self::new(WireErrorCode::UnknownQuery, message)
184 }
185
186 #[must_use]
188 pub fn query_timeout(message: impl Into<String>) -> Self {
189 Self::new(WireErrorCode::QueryTimeout, message)
190 }
191
192 #[must_use]
194 pub fn not_running(message: impl Into<String>) -> Self {
195 Self::new(WireErrorCode::NotRunning, message)
196 }
197
198 #[must_use]
200 pub fn lagged(message: impl Into<String>) -> Self {
201 Self::new(WireErrorCode::Lagged, message)
202 }
203
204 #[must_use]
206 pub fn invalid_input(message: impl Into<String>) -> Self {
207 Self::new(WireErrorCode::InvalidInput, message)
208 }
209
210 #[must_use]
212 pub fn backend(message: impl Into<String>) -> Self {
213 Self::new(WireErrorCode::Backend, message)
214 }
215
216 #[must_use]
218 pub fn query_failed(message: impl Into<String>) -> Self {
219 Self::new(WireErrorCode::QueryFailed, message)
220 }
221
222 #[must_use]
224 pub fn deploy_denied(message: impl Into<String>) -> Self {
225 Self::new(WireErrorCode::DeployDenied, message)
226 }
227
228 #[must_use]
231 pub fn grant_denied(message: impl Into<String>) -> Self {
232 Self::new(WireErrorCode::GrantDenied, message)
233 }
234
235 #[must_use]
237 pub fn version_pinned(message: impl Into<String>) -> Self {
238 Self::new(WireErrorCode::VersionPinned, message)
239 }
240
241 #[must_use]
243 pub fn not_owner(message: impl Into<String>) -> Self {
244 Self::new(WireErrorCode::NotOwner, message)
245 }
246
247 #[must_use]
249 pub fn invalid_state(message: impl Into<String>) -> Self {
250 Self::new(WireErrorCode::InvalidState, message)
251 }
252
253 #[must_use]
255 pub fn invalid_state_with_type(
256 error_type: impl Into<String>,
257 message: impl Into<String>,
258 ) -> Self {
259 Self::new_with_type(WireErrorCode::InvalidState, error_type, message)
260 }
261
262 #[must_use]
264 pub fn not_found_with_type(error_type: impl Into<String>, message: impl Into<String>) -> Self {
265 Self::new_with_type(WireErrorCode::NotFound, error_type, message)
266 }
267
268 #[must_use]
270 pub fn not_running_with_type(
271 error_type: impl Into<String>,
272 message: impl Into<String>,
273 ) -> Self {
274 Self::new_with_type(WireErrorCode::NotRunning, error_type, message)
275 }
276
277 #[must_use]
279 pub fn backend_with_type(error_type: impl Into<String>, message: impl Into<String>) -> Self {
280 Self::new_with_type(WireErrorCode::Backend, error_type, message)
281 }
282}
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, prost::Enumeration)]
286#[repr(i32)]
287pub enum ProtoWireErrorCode {
288 Unspecified = 0,
290 NotFound = 1,
292 NamespaceDenied = 2,
294 SequenceConflict = 3,
296 UnknownQuery = 4,
298 QueryTimeout = 5,
300 NotRunning = 6,
302 Lagged = 7,
304 InvalidInput = 8,
306 Backend = 9,
308 QueryFailed = 10,
310 DeployDenied = 11,
312 VersionPinned = 12,
314 NotOwner = 13,
316 InvalidState = 14,
318 GrantDenied = 15,
320}
321
322#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, prost::Message)]
324pub struct ProtoWireError {
325 #[prost(enumeration = "ProtoWireErrorCode", tag = "1")]
327 pub code: i32,
328 #[prost(string, tag = "2")]
330 pub message: String,
331 #[prost(string, optional, tag = "3")]
333 pub error_type: Option<String>,
334}
335
336impl From<WireErrorCode> for ProtoWireErrorCode {
337 fn from(value: WireErrorCode) -> Self {
338 match value {
339 WireErrorCode::NotFound => Self::NotFound,
340 WireErrorCode::NamespaceDenied => Self::NamespaceDenied,
341 WireErrorCode::SequenceConflict => Self::SequenceConflict,
342 WireErrorCode::UnknownQuery => Self::UnknownQuery,
343 WireErrorCode::QueryTimeout => Self::QueryTimeout,
344 WireErrorCode::NotRunning => Self::NotRunning,
345 WireErrorCode::Lagged => Self::Lagged,
346 WireErrorCode::InvalidInput => Self::InvalidInput,
347 WireErrorCode::Backend => Self::Backend,
348 WireErrorCode::QueryFailed => Self::QueryFailed,
349 WireErrorCode::DeployDenied => Self::DeployDenied,
350 WireErrorCode::GrantDenied => Self::GrantDenied,
351 WireErrorCode::VersionPinned => Self::VersionPinned,
352 WireErrorCode::NotOwner => Self::NotOwner,
353 WireErrorCode::InvalidState => Self::InvalidState,
354 }
355 }
356}
357
358impl TryFrom<ProtoWireErrorCode> for WireErrorCode {
359 type Error = WireError;
360
361 fn try_from(value: ProtoWireErrorCode) -> Result<Self, Self::Error> {
362 match value {
363 ProtoWireErrorCode::Unspecified => {
364 Err(WireError::backend("wire error code is missing"))
365 }
366 ProtoWireErrorCode::NotFound => Ok(Self::NotFound),
367 ProtoWireErrorCode::NamespaceDenied => Ok(Self::NamespaceDenied),
368 ProtoWireErrorCode::SequenceConflict => Ok(Self::SequenceConflict),
369 ProtoWireErrorCode::UnknownQuery => Ok(Self::UnknownQuery),
370 ProtoWireErrorCode::QueryTimeout => Ok(Self::QueryTimeout),
371 ProtoWireErrorCode::NotRunning => Ok(Self::NotRunning),
372 ProtoWireErrorCode::Lagged => Ok(Self::Lagged),
373 ProtoWireErrorCode::InvalidInput => Ok(Self::InvalidInput),
374 ProtoWireErrorCode::Backend => Ok(Self::Backend),
375 ProtoWireErrorCode::QueryFailed => Ok(Self::QueryFailed),
376 ProtoWireErrorCode::DeployDenied => Ok(Self::DeployDenied),
377 ProtoWireErrorCode::GrantDenied => Ok(Self::GrantDenied),
378 ProtoWireErrorCode::VersionPinned => Ok(Self::VersionPinned),
379 ProtoWireErrorCode::NotOwner => Ok(Self::NotOwner),
380 ProtoWireErrorCode::InvalidState => Ok(Self::InvalidState),
381 }
382 }
383}
384
385impl From<WireError> for ProtoWireError {
386 fn from(value: WireError) -> Self {
387 let code = ProtoWireErrorCode::from(value.code) as i32;
388 Self {
389 code,
390 message: value.message,
391 error_type: value.error_type,
392 }
393 }
394}
395
396impl TryFrom<ProtoWireError> for WireError {
397 type Error = WireError;
398
399 fn try_from(value: ProtoWireError) -> Result<Self, Self::Error> {
400 let code = ProtoWireErrorCode::try_from(value.code)
401 .map_err(|_| WireError::backend("wire error code is unknown"))?;
402 Ok(Self::new(WireErrorCode::try_from(code)?, value.message)
403 .with_optional_error_type(value.error_type))
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::{ProtoWireError, ProtoWireErrorCode, WireError, WireErrorCode};
410
411 fn assert_send_sync<T: Send + Sync>() {}
412
413 const fn next_code(code: WireErrorCode) -> Option<WireErrorCode> {
419 match code {
420 WireErrorCode::NotFound => Some(WireErrorCode::NamespaceDenied),
421 WireErrorCode::NamespaceDenied => Some(WireErrorCode::SequenceConflict),
422 WireErrorCode::SequenceConflict => Some(WireErrorCode::UnknownQuery),
423 WireErrorCode::UnknownQuery => Some(WireErrorCode::QueryTimeout),
424 WireErrorCode::QueryTimeout => Some(WireErrorCode::NotRunning),
425 WireErrorCode::NotRunning => Some(WireErrorCode::Lagged),
426 WireErrorCode::Lagged => Some(WireErrorCode::InvalidInput),
427 WireErrorCode::InvalidInput => Some(WireErrorCode::Backend),
428 WireErrorCode::Backend => Some(WireErrorCode::QueryFailed),
429 WireErrorCode::QueryFailed => Some(WireErrorCode::DeployDenied),
430 WireErrorCode::DeployDenied => Some(WireErrorCode::GrantDenied),
431 WireErrorCode::GrantDenied => Some(WireErrorCode::VersionPinned),
432 WireErrorCode::VersionPinned => Some(WireErrorCode::NotOwner),
433 WireErrorCode::NotOwner => Some(WireErrorCode::InvalidState),
434 WireErrorCode::InvalidState => None,
435 }
436 }
437
438 fn all_codes() -> Vec<WireErrorCode> {
440 let mut codes = vec![WireErrorCode::NotFound];
441 while let Some(&last) = codes.last() {
442 match next_code(last) {
443 Some(next) => codes.push(next),
444 None => break,
445 }
446 }
447 codes
448 }
449
450 #[test]
451 fn wire_error_is_send_sync() {
452 assert_send_sync::<WireError>();
453 }
454
455 #[test]
459 fn proto_numeric_values_are_pinned() {
460 let expected: &[(WireErrorCode, i32)] = &[
461 (WireErrorCode::NotFound, 1),
462 (WireErrorCode::NamespaceDenied, 2),
463 (WireErrorCode::SequenceConflict, 3),
464 (WireErrorCode::UnknownQuery, 4),
465 (WireErrorCode::QueryTimeout, 5),
466 (WireErrorCode::NotRunning, 6),
467 (WireErrorCode::Lagged, 7),
468 (WireErrorCode::InvalidInput, 8),
469 (WireErrorCode::Backend, 9),
470 (WireErrorCode::QueryFailed, 10),
471 (WireErrorCode::DeployDenied, 11),
472 (WireErrorCode::GrantDenied, 15),
473 (WireErrorCode::VersionPinned, 12),
474 (WireErrorCode::NotOwner, 13),
475 (WireErrorCode::InvalidState, 14),
476 ];
477 assert_eq!(
478 expected.len(),
479 all_codes().len(),
480 "every WireErrorCode variant must have a pinned numeric value"
481 );
482 for &(code, number) in expected {
483 assert_eq!(
484 ProtoWireErrorCode::from(code) as i32,
485 number,
486 "{code:?} must keep proto enum value {number}",
487 );
488 }
489 }
490
491 #[test]
494 fn string_codes_are_pinned() {
495 let expected: &[(WireErrorCode, &str)] = &[
496 (WireErrorCode::NotFound, "not_found"),
497 (WireErrorCode::NamespaceDenied, "namespace_denied"),
498 (WireErrorCode::SequenceConflict, "sequence_conflict"),
499 (WireErrorCode::UnknownQuery, "unknown_query"),
500 (WireErrorCode::QueryTimeout, "query_timeout"),
501 (WireErrorCode::NotRunning, "not_running"),
502 (WireErrorCode::Lagged, "lagged"),
503 (WireErrorCode::InvalidInput, "invalid_input"),
504 (WireErrorCode::Backend, "backend"),
505 (WireErrorCode::QueryFailed, "query_failed"),
506 (WireErrorCode::DeployDenied, "deploy_denied"),
507 (WireErrorCode::GrantDenied, "grant_denied"),
508 (WireErrorCode::VersionPinned, "version_pinned"),
509 (WireErrorCode::NotOwner, "not_owner"),
510 (WireErrorCode::InvalidState, "invalid_state"),
511 ];
512 assert_eq!(
513 expected.len(),
514 all_codes().len(),
515 "every WireErrorCode variant must have a pinned string code"
516 );
517 for &(code, string) in expected {
518 assert_eq!(code.as_str(), string, "{code:?} must keep code {string}");
519 }
520 }
521
522 #[test]
523 fn json_codes_match_as_str_and_round_trip() -> Result<(), serde_json::Error> {
524 for code in all_codes() {
525 let serialized = serde_json::to_value(code)?;
526 assert_eq!(
527 serialized,
528 serde_json::Value::String(code.as_str().to_owned()),
529 "JSON serialization of {code:?} must equal as_str()",
530 );
531 let deserialized: WireErrorCode =
532 serde_json::from_value(serde_json::Value::String(code.as_str().to_owned()))?;
533 assert_eq!(deserialized, code, "{code:?} must round-trip through JSON");
534
535 let error = WireError::new(code, format!("message for {}", code.as_str()));
536 let body = serde_json::to_value(&error)?;
537 assert_eq!(
538 body.get("code"),
539 Some(&serde_json::Value::String(code.as_str().to_owned())),
540 "WireError JSON body must carry the snake_case code for {code:?}",
541 );
542 let decoded: WireError = serde_json::from_value(body)?;
543 assert_eq!(decoded, error);
544 }
545 Ok(())
546 }
547
548 #[test]
549 fn proto_round_trips_every_code() -> Result<(), WireError> {
550 for code in all_codes() {
551 let error = WireError::new_with_type(
552 code,
553 format!("{}Variant", code.as_str()),
554 format!("message for {}", code.as_str()),
555 );
556 let proto = ProtoWireError::from(error.clone());
557 let decoded = WireError::try_from(proto)?;
558 assert_eq!(decoded, error);
559 }
560
561 Ok(())
562 }
563
564 #[test]
570 fn a_grant_refusal_is_not_a_deploy_refusal() {
571 assert_ne!(WireErrorCode::GrantDenied, WireErrorCode::DeployDenied);
572 assert_ne!(
573 WireErrorCode::GrantDenied.as_str(),
574 WireErrorCode::DeployDenied.as_str()
575 );
576 assert_ne!(
577 ProtoWireErrorCode::from(WireErrorCode::GrantDenied) as i32,
578 ProtoWireErrorCode::from(WireErrorCode::DeployDenied) as i32
579 );
580 }
581
582 #[test]
585 fn every_code_has_a_distinct_string() {
586 let mut strings: Vec<&str> = all_codes().iter().map(|code| code.as_str()).collect();
587 let total = strings.len();
588 strings.sort_unstable();
589 strings.dedup();
590 assert_eq!(strings.len(), total, "two wire codes share one string code");
591 }
592
593 #[test]
594 fn rejects_unspecified_proto_code() {
595 let proto = ProtoWireError {
596 code: 0,
597 message: String::from("missing"),
598 error_type: None,
599 };
600
601 let result = WireError::try_from(proto);
602 assert_eq!(
603 result,
604 Err(WireError::backend("wire error code is missing"))
605 );
606 }
607
608 #[test]
609 fn representative_documented_mappings_use_stable_codes() {
610 let engine_unknown_workflow = WireError::not_found("workflow was not found");
611 let store_sequence_conflict = WireError::sequence_conflict("event sequence conflicted");
612
613 assert_eq!(engine_unknown_workflow.code, WireErrorCode::NotFound);
614 assert_eq!(
615 store_sequence_conflict.code,
616 WireErrorCode::SequenceConflict
617 );
618 assert_eq!(
619 WireError::namespace_denied("denied").code,
620 WireErrorCode::NamespaceDenied
621 );
622 assert_eq!(
623 WireError::query_timeout("timeout").code,
624 WireErrorCode::QueryTimeout
625 );
626 assert_eq!(
627 WireError::unknown_query("unknown").code,
628 WireErrorCode::UnknownQuery
629 );
630 assert_eq!(
631 WireError::not_running("terminal").code,
632 WireErrorCode::NotRunning
633 );
634 assert_eq!(
635 WireError::invalid_input("malformed").code,
636 WireErrorCode::InvalidInput
637 );
638 assert_eq!(
639 WireError::query_failed("handler raised").code,
640 WireErrorCode::QueryFailed
641 );
642 assert_eq!(
643 WireError::deploy_denied("no deploy grant").code,
644 WireErrorCode::DeployDenied
645 );
646 assert_eq!(
647 WireError::version_pinned("pinned by live run").code,
648 WireErrorCode::VersionPinned
649 );
650 assert_eq!(
651 WireError::grant_denied("no assistant.sessions grant").code,
652 WireErrorCode::GrantDenied
653 );
654 assert_eq!(
655 WireError::not_owner("wrong shard owner").code,
656 WireErrorCode::NotOwner
657 );
658 assert_eq!(
659 WireError::invalid_state("run is not reopenable").code,
660 WireErrorCode::InvalidState
661 );
662 }
663}