1use crate::engine::utils::{compute_path_parts, strip_hash_prefix};
2use chrono::Utc;
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5use thiserror::Error;
6
7#[derive(Debug, Error, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub enum DataflowError {
15 #[error("Validation error: {0}")]
17 Validation(String),
18
19 #[error("Function execution error: {context}")]
21 FunctionExecution {
22 context: String,
23 #[source]
24 #[serde(skip)]
25 source: Option<Box<Self>>,
26 },
27
28 #[error("Workflow error: {0}")]
30 Workflow(String),
31
32 #[error("Task error: {0}")]
34 Task(String),
35
36 #[error("Function not found: {0}")]
38 FunctionNotFound(String),
39
40 #[error("Deserialization error: {0}")]
42 Deserialization(String),
43
44 #[error("IO error: {0}")]
46 Io(String),
47
48 #[error("Logic evaluation error: {0}")]
50 LogicEvaluation(String),
51
52 #[error("HTTP error: {status} - {message}")]
54 Http { status: u16, message: String },
55
56 #[error("Timeout error: {0}")]
58 Timeout(String),
59
60 #[error("Unknown error: {0}")]
62 Unknown(String),
63
64 #[error("{message}")]
77 Service {
78 kind: String,
80 message: String,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
85 detail: Option<String>,
86 retryable: bool,
89 },
90}
91
92impl DataflowError {
93 pub fn function_execution<S: Into<String>>(context: S, source: Option<Self>) -> Self {
95 Self::FunctionExecution {
96 context: context.into(),
97 source: source.map(Box::new),
98 }
99 }
100
101 pub fn http<S: Into<String>>(status: u16, message: S) -> Self {
103 Self::Http {
104 status,
105 message: message.into(),
106 }
107 }
108
109 pub fn from_io(err: std::io::Error) -> Self {
111 Self::Io(err.to_string())
112 }
113
114 pub fn from_serde(err: serde_json::Error) -> Self {
116 Self::Deserialization(err.to_string())
117 }
118
119 pub fn retryable(&self) -> bool {
125 match self {
126 Self::Http { status, .. } => {
128 *status >= 500 || *status == 429 || *status == 408 || *status == 0
130 }
132 Self::Timeout(_) => true,
133 Self::Io(_) => true,
134 Self::FunctionExecution { source, .. } => {
135 source.as_ref().map(|e| e.retryable()).unwrap_or(false)
137 }
138
139 Self::Validation(_) => false,
141 Self::LogicEvaluation(_) => false,
142 Self::Deserialization(_) => false,
143 Self::Workflow(_) => false,
144 Self::Task(_) => false,
145 Self::FunctionNotFound(_) => false,
146 Self::Unknown(_) => false,
147
148 Self::Service { retryable, .. } => *retryable,
150 }
151 }
152
153 pub fn kind(&self) -> Option<&str> {
159 match self {
160 Self::Service { kind, .. } => Some(kind),
161 Self::FunctionExecution { source, .. } => source.as_deref().and_then(Self::kind),
164 _ => None,
165 }
166 }
167
168 pub fn detail(&self) -> Option<&str> {
174 match self {
175 Self::Service { detail, .. } => detail.as_deref(),
176 Self::FunctionExecution { source, .. } => source.as_deref().and_then(Self::detail),
177 _ => None,
178 }
179 }
180
181 pub fn service(kind: impl Into<String>, message: impl Into<String>) -> ServiceErrorBuilder {
201 ServiceErrorBuilder {
202 kind: kind.into(),
203 message: message.into(),
204 detail: None,
205 retryable: false,
206 }
207 }
208}
209
210#[must_use = "ServiceErrorBuilder must be `.build()` to produce a DataflowError"]
212pub struct ServiceErrorBuilder {
213 kind: String,
214 message: String,
215 detail: Option<String>,
216 retryable: bool,
217}
218
219impl ServiceErrorBuilder {
220 pub fn detail(mut self, detail: impl Into<String>) -> Self {
223 self.detail = Some(detail.into());
224 self
225 }
226
227 pub fn retryable(mut self, retryable: bool) -> Self {
231 self.retryable = retryable;
232 self
233 }
234
235 pub fn build(self) -> DataflowError {
236 DataflowError::Service {
237 kind: self.kind,
238 message: self.message,
239 detail: self.detail,
240 retryable: self.retryable,
241 }
242 }
243}
244
245pub type Result<T> = std::result::Result<T, DataflowError>;
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
255#[non_exhaustive]
256pub struct ErrorInfo {
257 pub code: String,
259
260 pub message: String,
262
263 pub path: Option<String>,
265
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub workflow_id: Option<String>,
269
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub task_id: Option<String>,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub timestamp: Option<String>,
277
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub retry_attempted: Option<bool>,
281
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub retry_count: Option<u32>,
285
286 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub detail: Option<String>,
294}
295
296pub(crate) const DEFAULT_ERROR_CONTEXT_LIMIT: usize = 32;
301
302#[derive(Debug, Clone)]
309pub(crate) struct ErrorContextConfig {
310 pub(crate) path: String,
312 pub(crate) path_parts: Arc<[Arc<str>]>,
314 pub(crate) limit: usize,
316}
317
318impl ErrorContextConfig {
319 pub(crate) fn new(path: String, limit: usize) -> Result<Self> {
328 let mut segments = path.split('.');
329 let first = segments.next().unwrap_or_default();
330 let root = strip_hash_prefix(first);
331 if !matches!(root, "data" | "metadata" | "temp_data") {
332 return Err(DataflowError::Workflow(format!(
333 "error context path must start with `data`, `metadata` or `temp_data`, got {path:?}"
334 )));
335 }
336 let rest: Vec<&str> = segments.collect();
337 if rest.is_empty() || rest.iter().any(|s| s.is_empty()) {
338 return Err(DataflowError::Workflow(format!(
339 "error context path must name a slot inside `{root}`, got {path:?}"
340 )));
341 }
342 if root == "metadata" && rest == ["progress"] {
343 return Err(DataflowError::Workflow(
344 "error context path may not be `metadata.progress` — the engine owns \
345 that slot for cross-workflow chaining"
346 .to_string(),
347 ));
348 }
349 if limit == 0 {
350 return Err(DataflowError::Workflow(
351 "error context limit must be at least 1".to_string(),
352 ));
353 }
354 let path_parts = compute_path_parts(first, &rest.join("."));
355 Ok(Self {
356 path,
357 path_parts,
358 limit,
359 })
360 }
361}
362
363fn variant_code(error: &DataflowError) -> &'static str {
370 match error {
371 DataflowError::Validation(_) => "VALIDATION_ERROR",
372 DataflowError::Workflow(_) => "WORKFLOW_ERROR",
373 DataflowError::Task(_) => "TASK_ERROR",
374 DataflowError::FunctionNotFound(_) => "FUNCTION_NOT_FOUND",
375 DataflowError::FunctionExecution { .. } => "FUNCTION_ERROR",
376 DataflowError::LogicEvaluation(_) => "LOGIC_ERROR",
377 DataflowError::Http { .. } => "HTTP_ERROR",
378 DataflowError::Timeout(_) => "TIMEOUT_ERROR",
379 DataflowError::Io(_) => "IO_ERROR",
380 DataflowError::Deserialization(_) => "DESERIALIZATION_ERROR",
381 DataflowError::Unknown(_) => "UNKNOWN_ERROR",
382 DataflowError::Service { .. } => "TASK_ERROR",
383 }
384}
385
386pub(crate) fn service_error_code(error: &DataflowError) -> String {
401 match error.kind() {
402 Some(kind) if !kind.is_empty() => kind.to_string(),
403 _ => variant_code(error).to_string(),
404 }
405}
406
407impl ErrorInfo {
408 pub fn new(workflow_id: Option<String>, task_id: Option<String>, error: DataflowError) -> Self {
410 Self {
411 code: service_error_code(&error),
414 detail: error.detail().map(str::to_string),
415 message: error.to_string(),
416 path: None,
417 workflow_id,
418 task_id,
419 timestamp: Some(Utc::now().to_rfc3339()),
420 retry_attempted: Some(false),
421 retry_count: Some(0),
422 }
423 }
424
425 pub fn simple(code: String, message: String, path: Option<String>) -> Self {
427 Self {
428 code,
429 message,
430 path,
431 workflow_id: None,
432 task_id: None,
433 timestamp: Some(Utc::now().to_rfc3339()),
434 retry_attempted: None,
435 retry_count: None,
436 detail: None,
437 }
438 }
439
440 pub fn simple_ref(code: &str, message: &str, path: Option<&str>) -> Self {
442 Self {
443 code: code.to_string(),
444 message: message.to_string(),
445 path: path.map(|s| s.to_string()),
446 workflow_id: None,
447 task_id: None,
448 timestamp: Some(Utc::now().to_rfc3339()),
449 retry_attempted: None,
450 retry_count: None,
451 detail: None,
452 }
453 }
454
455 pub fn with_retry(mut self) -> Self {
457 self.retry_attempted = Some(true);
458 self.retry_count = Some(self.retry_count.unwrap_or(0) + 1);
459 self
460 }
461
462 pub fn builder(code: impl Into<String>, message: impl Into<String>) -> ErrorInfoBuilder {
464 ErrorInfoBuilder::new(code, message)
465 }
466}
467
468#[must_use = "ErrorInfoBuilder must be `.build()` to produce an ErrorInfo"]
470pub struct ErrorInfoBuilder {
471 code: String,
472 message: String,
473 path: Option<String>,
474 workflow_id: Option<String>,
475 task_id: Option<String>,
476 timestamp: Option<String>,
477 retry_attempted: Option<bool>,
478 retry_count: Option<u32>,
479 detail: Option<String>,
480}
481
482impl ErrorInfoBuilder {
483 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
485 Self {
486 code: code.into(),
487 message: message.into(),
488 path: None,
489 workflow_id: None,
490 task_id: None,
491 timestamp: Some(Utc::now().to_rfc3339()),
492 retry_attempted: None,
493 retry_count: None,
494 detail: None,
495 }
496 }
497
498 pub fn path(mut self, path: impl Into<String>) -> Self {
500 self.path = Some(path.into());
501 self
502 }
503
504 pub fn workflow_id(mut self, id: impl Into<String>) -> Self {
506 self.workflow_id = Some(id.into());
507 self
508 }
509
510 pub fn task_id(mut self, id: impl Into<String>) -> Self {
512 self.task_id = Some(id.into());
513 self
514 }
515
516 pub fn timestamp(mut self, timestamp: impl Into<String>) -> Self {
518 self.timestamp = Some(timestamp.into());
519 self
520 }
521
522 pub fn retry_attempted(mut self, attempted: bool) -> Self {
524 self.retry_attempted = Some(attempted);
525 self
526 }
527
528 pub fn retry_count(mut self, count: u32) -> Self {
530 self.retry_count = Some(count);
531 self
532 }
533
534 pub fn detail(mut self, detail: impl Into<String>) -> Self {
537 self.detail = Some(detail.into());
538 self
539 }
540
541 pub fn build(self) -> ErrorInfo {
543 ErrorInfo {
544 code: self.code,
545 message: self.message,
546 path: self.path,
547 workflow_id: self.workflow_id,
548 task_id: self.task_id,
549 timestamp: self.timestamp,
550 retry_attempted: self.retry_attempted,
551 retry_count: self.retry_count,
552 detail: self.detail,
553 }
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn test_retryable_errors() {
563 assert!(
565 DataflowError::Http {
566 status: 500,
567 message: "Internal Server Error".to_string()
568 }
569 .retryable()
570 );
571 assert!(
572 DataflowError::Http {
573 status: 502,
574 message: "Bad Gateway".to_string()
575 }
576 .retryable()
577 );
578 assert!(
579 DataflowError::Http {
580 status: 503,
581 message: "Service Unavailable".to_string()
582 }
583 .retryable()
584 );
585 assert!(
586 DataflowError::Http {
587 status: 429,
588 message: "Too Many Requests".to_string()
589 }
590 .retryable()
591 );
592 assert!(
593 DataflowError::Http {
594 status: 408,
595 message: "Request Timeout".to_string()
596 }
597 .retryable()
598 );
599 assert!(
600 DataflowError::Http {
601 status: 0,
602 message: "Connection Error".to_string()
603 }
604 .retryable()
605 );
606 assert!(DataflowError::Timeout("Connection timeout".to_string()).retryable());
607 assert!(DataflowError::Io("Network error".to_string()).retryable());
608 }
609
610 #[test]
611 fn test_non_retryable_errors() {
612 assert!(
614 !DataflowError::Http {
615 status: 400,
616 message: "Bad Request".to_string()
617 }
618 .retryable()
619 );
620 assert!(
621 !DataflowError::Http {
622 status: 401,
623 message: "Unauthorized".to_string()
624 }
625 .retryable()
626 );
627 assert!(
628 !DataflowError::Http {
629 status: 403,
630 message: "Forbidden".to_string()
631 }
632 .retryable()
633 );
634 assert!(
635 !DataflowError::Http {
636 status: 404,
637 message: "Not Found".to_string()
638 }
639 .retryable()
640 );
641 assert!(!DataflowError::Validation("Invalid input".to_string()).retryable());
642 assert!(!DataflowError::LogicEvaluation("Invalid logic".to_string()).retryable());
643 assert!(!DataflowError::Deserialization("Invalid JSON".to_string()).retryable());
644 assert!(!DataflowError::Workflow("Invalid workflow".to_string()).retryable());
645 assert!(!DataflowError::Unknown("Unknown error".to_string()).retryable());
646 }
647
648 #[test]
649 fn test_function_execution_error_retryability() {
650 let retryable_source = DataflowError::Http {
652 status: 500,
653 message: "Server Error".to_string(),
654 };
655 let non_retryable_source = DataflowError::Validation("Invalid data".to_string());
656
657 let retryable_func_error =
658 DataflowError::function_execution("HTTP call failed", Some(retryable_source));
659 let non_retryable_func_error =
660 DataflowError::function_execution("Validation failed", Some(non_retryable_source));
661 let no_source_func_error = DataflowError::function_execution("Unknown failure", None);
662
663 assert!(retryable_func_error.retryable());
664 assert!(!non_retryable_func_error.retryable());
665 assert!(!no_source_func_error.retryable());
666 }
667
668 #[test]
669 fn test_error_info_builder() {
670 let error = ErrorInfo::builder("TEST_ERROR", "Test message").build();
672 assert_eq!(error.code, "TEST_ERROR");
673 assert_eq!(error.message, "Test message");
674 assert!(error.timestamp.is_some());
675 assert!(error.path.is_none());
676
677 let error = ErrorInfo::builder("VALIDATION_ERROR", "Field validation failed")
679 .path("data.email")
680 .workflow_id("workflow_1")
681 .task_id("validate_email")
682 .retry_attempted(true)
683 .retry_count(2)
684 .build();
685
686 assert_eq!(error.code, "VALIDATION_ERROR");
687 assert_eq!(error.message, "Field validation failed");
688 assert_eq!(error.path, Some("data.email".to_string()));
689 assert_eq!(error.workflow_id, Some("workflow_1".to_string()));
690 assert_eq!(error.task_id, Some("validate_email".to_string()));
691 assert_eq!(error.retry_attempted, Some(true));
692 assert_eq!(error.retry_count, Some(2));
693 }
694
695 #[test]
696 fn test_error_info_new_from_dataflow_error() {
697 let test_cases = vec![
699 (
700 DataflowError::Validation("test".to_string()),
701 "VALIDATION_ERROR",
702 ),
703 (
704 DataflowError::Workflow("test".to_string()),
705 "WORKFLOW_ERROR",
706 ),
707 (DataflowError::Task("test".to_string()), "TASK_ERROR"),
708 (
709 DataflowError::FunctionNotFound("test".to_string()),
710 "FUNCTION_NOT_FOUND",
711 ),
712 (
713 DataflowError::function_execution("test", None),
714 "FUNCTION_ERROR",
715 ),
716 (
717 DataflowError::LogicEvaluation("test".to_string()),
718 "LOGIC_ERROR",
719 ),
720 (DataflowError::http(404, "Not Found"), "HTTP_ERROR"),
721 (DataflowError::Timeout("test".to_string()), "TIMEOUT_ERROR"),
722 (DataflowError::Io("test".to_string()), "IO_ERROR"),
723 (
724 DataflowError::Deserialization("test".to_string()),
725 "DESERIALIZATION_ERROR",
726 ),
727 (DataflowError::Unknown("test".to_string()), "UNKNOWN_ERROR"),
728 ];
729
730 for (error, expected_code) in test_cases {
731 let info = ErrorInfo::new(
732 Some("workflow_1".to_string()),
733 Some("task_1".to_string()),
734 error,
735 );
736 assert_eq!(info.code, expected_code);
737 assert_eq!(info.workflow_id, Some("workflow_1".to_string()));
738 assert_eq!(info.task_id, Some("task_1".to_string()));
739 assert!(info.timestamp.is_some());
740 assert_eq!(info.retry_attempted, Some(false));
741 assert_eq!(info.retry_count, Some(0));
742 }
743 }
744
745 #[test]
746 fn test_error_info_simple_constructors() {
747 let error = ErrorInfo::simple(
749 "CUSTOM_ERROR".to_string(),
750 "Custom message".to_string(),
751 Some("data.field".to_string()),
752 );
753 assert_eq!(error.code, "CUSTOM_ERROR");
754 assert_eq!(error.message, "Custom message");
755 assert_eq!(error.path, Some("data.field".to_string()));
756 assert!(error.workflow_id.is_none());
757 assert!(error.task_id.is_none());
758 assert!(error.timestamp.is_some());
759
760 let error = ErrorInfo::simple_ref("REF_ERROR", "Ref message", Some("data.path"));
762 assert_eq!(error.code, "REF_ERROR");
763 assert_eq!(error.message, "Ref message");
764 assert_eq!(error.path, Some("data.path".to_string()));
765
766 let error = ErrorInfo::simple_ref("NO_PATH", "No path message", None);
768 assert!(error.path.is_none());
769 }
770
771 #[test]
772 fn test_error_info_with_retry() {
773 let error = ErrorInfo::simple_ref("TEST", "Test", None);
774 assert!(error.retry_attempted.is_none());
775 assert!(error.retry_count.is_none());
776
777 let error = error.with_retry();
778 assert_eq!(error.retry_attempted, Some(true));
779 assert_eq!(error.retry_count, Some(1));
780
781 let error = error.with_retry();
782 assert_eq!(error.retry_attempted, Some(true));
783 assert_eq!(error.retry_count, Some(2));
784 }
785
786 #[test]
787 fn test_error_display_messages() {
788 assert_eq!(
790 DataflowError::Validation("test".to_string()).to_string(),
791 "Validation error: test"
792 );
793 assert_eq!(
794 DataflowError::Workflow("test".to_string()).to_string(),
795 "Workflow error: test"
796 );
797 assert_eq!(
798 DataflowError::Task("test".to_string()).to_string(),
799 "Task error: test"
800 );
801 assert_eq!(
802 DataflowError::FunctionNotFound("test".to_string()).to_string(),
803 "Function not found: test"
804 );
805 assert_eq!(
806 DataflowError::http(404, "Not Found").to_string(),
807 "HTTP error: 404 - Not Found"
808 );
809 assert_eq!(
810 DataflowError::Timeout("test".to_string()).to_string(),
811 "Timeout error: test"
812 );
813 }
814
815 #[test]
816 fn test_error_conversions() {
817 let json_str = "invalid json";
819 let serde_result: std::result::Result<serde_json::Value, _> =
820 serde_json::from_str(json_str);
821 if let Err(e) = serde_result {
822 let dataflow_err = DataflowError::from_serde(e);
823 assert!(matches!(dataflow_err, DataflowError::Deserialization(_)));
824 }
825 }
826
827 #[test]
828 fn service_error_carries_kind_detail_and_declared_retryability() {
829 let e = DataflowError::service("circuit_open", "upstream unavailable")
830 .detail("connector 'billing' breaker open")
831 .retryable(true)
832 .build();
833
834 assert_eq!(e.kind(), Some("circuit_open"));
835 assert_eq!(e.detail(), Some("connector 'billing' breaker open"));
836 assert!(e.retryable());
837 }
838
839 #[test]
840 fn function_execution_inherits_kind_detail_and_retryable_from_a_wrapped_service_source() {
841 let inner = DataflowError::service("circuit_open", "upstream unavailable")
847 .detail("connector 'billing' breaker open since 12:04")
848 .retryable(true)
849 .build();
850 let wrapped = DataflowError::function_execution("calling billing connector", Some(inner));
851
852 assert_eq!(wrapped.kind(), Some("circuit_open"));
853 assert_eq!(
854 wrapped.detail(),
855 Some("connector 'billing' breaker open since 12:04")
856 );
857 assert!(wrapped.retryable());
858
859 assert_eq!(service_error_code(&wrapped), "circuit_open");
862 }
863
864 #[test]
865 fn service_display_hides_the_detail_but_debug_shows_it() {
866 let e = DataflowError::service("circuit_open", "upstream unavailable")
867 .detail("SECRET-TOPOLOGY")
868 .build();
869
870 assert_eq!(e.to_string(), "upstream unavailable");
872 assert!(!e.to_string().contains("SECRET-TOPOLOGY"));
873 assert!(format!("{e:?}").contains("SECRET-TOPOLOGY"));
875 }
876
877 #[test]
878 fn service_retryability_is_independent_of_every_other_field() {
879 let yes = DataflowError::service("k", "m").retryable(true).build();
880 let no = DataflowError::service("k", "m").retryable(false).build();
881 assert!(yes.retryable());
882 assert!(!no.retryable());
883 assert!(!DataflowError::service("k", "m").build().retryable());
885 }
886
887 #[test]
888 fn kind_and_detail_are_none_for_every_engine_owned_variant() {
889 let variants = [
890 DataflowError::Validation("v".into()),
891 DataflowError::FunctionExecution {
892 context: "c".into(),
893 source: None,
894 },
895 DataflowError::LogicEvaluation("l".into()),
896 DataflowError::Deserialization("d".into()),
897 DataflowError::Workflow("w".into()),
898 DataflowError::Task("t".into()),
899 DataflowError::FunctionNotFound("f".into()),
900 DataflowError::Http {
901 status: 500,
902 message: "h".into(),
903 },
904 DataflowError::Timeout("to".into()),
905 DataflowError::Io("io".into()),
906 DataflowError::Unknown("u".into()),
907 ];
908 assert_eq!(variants.len(), 11, "one assertion per engine-owned variant");
909 for v in &variants {
910 assert_eq!(v.kind(), None, "kind() for {v:?}");
911 assert_eq!(v.detail(), None, "detail() for {v:?}");
912 }
913 }
914
915 #[test]
916 fn service_error_code_passes_kind_through_verbatim() {
917 let e = DataflowError::service("circuit_open", "m").build();
920 let info = ErrorInfo::new(None, None, e);
921 assert_eq!(info.code, "circuit_open");
922 }
923
924 #[test]
925 fn an_empty_kind_falls_back_rather_than_recording_an_empty_code() {
926 let e = DataflowError::service("", "m").build();
927 let info = ErrorInfo::new(None, None, e);
928 assert_eq!(info.code, "TASK_ERROR");
929 assert!(!info.code.is_empty());
930 }
931
932 #[test]
933 fn a_non_ascii_kind_is_neither_panicked_on_nor_mangled() {
934 let e = DataflowError::service("limite_dépassé", "m").build();
935 let info = ErrorInfo::new(None, None, e);
936 assert_eq!(info.code, "limite_dépassé");
938 }
939
940 #[test]
941 fn error_info_lifts_the_detail_and_omits_it_when_absent() {
942 let with = ErrorInfo::new(
943 None,
944 None,
945 DataflowError::service("k", "m").detail("op only").build(),
946 );
947 assert_eq!(with.detail.as_deref(), Some("op only"));
948 assert!(serde_json::to_string(&with).unwrap().contains("detail"));
949
950 let without = ErrorInfo::new(None, None, DataflowError::service("k", "m").build());
952 assert_eq!(without.detail, None);
953 assert!(!serde_json::to_string(&without).unwrap().contains("detail"));
954
955 let engine_owned = ErrorInfo::new(None, None, DataflowError::Task("t".into()));
956 assert_eq!(engine_owned.detail, None);
957 assert!(
958 !serde_json::to_string(&engine_owned)
959 .unwrap()
960 .contains("detail"),
961 "the JSON shape is unchanged for every pre-existing error"
962 );
963 }
964
965 #[test]
966 fn a_service_error_round_trips_through_serde_with_and_without_detail() {
967 for e in [
968 DataflowError::service("k", "m")
969 .detail("d")
970 .retryable(true)
971 .build(),
972 DataflowError::service("k", "m").build(),
973 ] {
974 let json = serde_json::to_string(&e).unwrap();
975 let back: DataflowError = serde_json::from_str(&json).unwrap();
976 assert_eq!(back.kind(), e.kind());
977 assert_eq!(back.detail(), e.detail());
978 assert_eq!(back.retryable(), e.retryable());
979 }
980
981 let bare = DataflowError::service("k", "m").build();
983 assert!(!serde_json::to_string(&bare).unwrap().contains("detail"));
984 }
985}