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("{0}")]
72 BudgetExceeded(String),
73
74 #[error("HTTP error: {status} - {message}")]
76 Http { status: u16, message: String },
77
78 #[error("Timeout error: {0}")]
80 Timeout(String),
81
82 #[error("Unknown error: {0}")]
84 Unknown(String),
85
86 #[error("{message}")]
99 Service {
100 kind: String,
102 message: String,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
107 detail: Option<String>,
108 retryable: bool,
111 },
112}
113
114impl DataflowError {
115 pub fn function_execution<S: Into<String>>(context: S, source: Option<Self>) -> Self {
117 Self::FunctionExecution {
118 context: context.into(),
119 source: source.map(Box::new),
120 }
121 }
122
123 pub fn http<S: Into<String>>(status: u16, message: S) -> Self {
125 Self::Http {
126 status,
127 message: message.into(),
128 }
129 }
130
131 pub fn from_io(err: std::io::Error) -> Self {
133 Self::Io(err.to_string())
134 }
135
136 pub fn from_serde(err: serde_json::Error) -> Self {
138 Self::Deserialization(err.to_string())
139 }
140
141 pub fn retryable(&self) -> bool {
147 match self {
148 Self::Http { status, .. } => {
150 *status >= 500 || *status == 429 || *status == 408 || *status == 0
152 }
154 Self::Timeout(_) => true,
155 Self::Io(_) => true,
156 Self::FunctionExecution { source, .. } => {
157 source.as_ref().map(|e| e.retryable()).unwrap_or(false)
159 }
160
161 Self::Validation(_) => false,
163 Self::LogicEvaluation(_) => false,
164 Self::BudgetExceeded(_) => false,
167 Self::Deserialization(_) => false,
168 Self::Workflow(_) => false,
169 Self::Task(_) => false,
170 Self::FunctionNotFound(_) => false,
171 Self::Unknown(_) => false,
172
173 Self::Service { retryable, .. } => *retryable,
175 }
176 }
177
178 pub fn kind(&self) -> Option<&str> {
184 match self {
185 Self::Service { kind, .. } => Some(kind),
186 Self::FunctionExecution { source, .. } => source.as_deref().and_then(Self::kind),
189 _ => None,
190 }
191 }
192
193 pub fn detail(&self) -> Option<&str> {
199 match self {
200 Self::Service { detail, .. } => detail.as_deref(),
201 Self::FunctionExecution { source, .. } => source.as_deref().and_then(Self::detail),
202 _ => None,
203 }
204 }
205
206 pub fn service(kind: impl Into<String>, message: impl Into<String>) -> ServiceErrorBuilder {
226 ServiceErrorBuilder {
227 kind: kind.into(),
228 message: message.into(),
229 detail: None,
230 retryable: false,
231 }
232 }
233}
234
235#[must_use = "ServiceErrorBuilder must be `.build()` to produce a DataflowError"]
237pub struct ServiceErrorBuilder {
238 kind: String,
239 message: String,
240 detail: Option<String>,
241 retryable: bool,
242}
243
244impl ServiceErrorBuilder {
245 pub fn detail(mut self, detail: impl Into<String>) -> Self {
248 self.detail = Some(detail.into());
249 self
250 }
251
252 pub fn retryable(mut self, retryable: bool) -> Self {
256 self.retryable = retryable;
257 self
258 }
259
260 pub fn build(self) -> DataflowError {
261 DataflowError::Service {
262 kind: self.kind,
263 message: self.message,
264 detail: self.detail,
265 retryable: self.retryable,
266 }
267 }
268}
269
270pub type Result<T> = std::result::Result<T, DataflowError>;
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
280#[non_exhaustive]
281pub struct ErrorInfo {
282 pub code: String,
284
285 pub message: String,
287
288 pub path: Option<String>,
290
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub workflow_id: Option<String>,
294
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub task_id: Option<String>,
298
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub timestamp: Option<String>,
302
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub retry_attempted: Option<bool>,
306
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub retry_count: Option<u32>,
310
311 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub detail: Option<String>,
319}
320
321pub(crate) const DEFAULT_ERROR_CONTEXT_LIMIT: usize = 32;
326
327#[derive(Debug, Clone)]
334pub(crate) struct ErrorContextConfig {
335 pub(crate) path: String,
337 pub(crate) path_parts: Arc<[Arc<str>]>,
339 pub(crate) limit: usize,
341}
342
343impl ErrorContextConfig {
344 pub(crate) fn new(path: String, limit: usize) -> Result<Self> {
353 let mut segments = path.split('.');
354 let first = segments.next().unwrap_or_default();
355 let root = strip_hash_prefix(first);
356 if !matches!(root, "data" | "metadata" | "temp_data") {
357 return Err(DataflowError::Workflow(format!(
358 "error context path must start with `data`, `metadata` or `temp_data`, got {path:?}"
359 )));
360 }
361 let rest: Vec<&str> = segments.collect();
362 if rest.is_empty() || rest.iter().any(|s| s.is_empty()) {
363 return Err(DataflowError::Workflow(format!(
364 "error context path must name a slot inside `{root}`, got {path:?}"
365 )));
366 }
367 if root == "metadata" && rest == ["progress"] {
368 return Err(DataflowError::Workflow(
369 "error context path may not be `metadata.progress` — the engine owns \
370 that slot for cross-workflow chaining"
371 .to_string(),
372 ));
373 }
374 if limit == 0 {
375 return Err(DataflowError::Workflow(
376 "error context limit must be at least 1".to_string(),
377 ));
378 }
379 let path_parts = compute_path_parts(first, &rest.join("."));
380 Ok(Self {
381 path,
382 path_parts,
383 limit,
384 })
385 }
386}
387
388fn variant_code(error: &DataflowError) -> &'static str {
395 match error {
396 DataflowError::Validation(_) => "VALIDATION_ERROR",
397 DataflowError::Workflow(_) => "WORKFLOW_ERROR",
398 DataflowError::Task(_) => "TASK_ERROR",
399 DataflowError::FunctionNotFound(_) => "FUNCTION_NOT_FOUND",
400 DataflowError::FunctionExecution { .. } => "FUNCTION_ERROR",
401 DataflowError::LogicEvaluation(_) => "LOGIC_ERROR",
402 DataflowError::BudgetExceeded(_) => "BUDGET_EXCEEDED",
403 DataflowError::Http { .. } => "HTTP_ERROR",
404 DataflowError::Timeout(_) => "TIMEOUT_ERROR",
405 DataflowError::Io(_) => "IO_ERROR",
406 DataflowError::Deserialization(_) => "DESERIALIZATION_ERROR",
407 DataflowError::Unknown(_) => "UNKNOWN_ERROR",
408 DataflowError::Service { .. } => "TASK_ERROR",
409 }
410}
411
412pub(crate) fn service_error_code(error: &DataflowError) -> String {
427 match error.kind() {
428 Some(kind) if !kind.is_empty() => kind.to_string(),
429 _ => variant_code(error).to_string(),
430 }
431}
432
433pub(crate) fn from_datalogic_eval(error: &datalogic_rs::Error) -> DataflowError {
447 if error.tag() == "BudgetExceeded" {
448 return DataflowError::BudgetExceeded(error.to_string());
449 }
450 DataflowError::LogicEvaluation(error.to_string())
451}
452
453impl ErrorInfo {
454 pub fn new(workflow_id: Option<String>, task_id: Option<String>, error: DataflowError) -> Self {
456 Self {
457 code: service_error_code(&error),
460 detail: error.detail().map(str::to_string),
461 message: error.to_string(),
462 path: None,
463 workflow_id,
464 task_id,
465 timestamp: Some(Utc::now().to_rfc3339()),
466 retry_attempted: Some(false),
467 retry_count: Some(0),
468 }
469 }
470
471 pub fn simple(code: String, message: String, path: Option<String>) -> Self {
473 Self {
474 code,
475 message,
476 path,
477 workflow_id: None,
478 task_id: None,
479 timestamp: Some(Utc::now().to_rfc3339()),
480 retry_attempted: None,
481 retry_count: None,
482 detail: None,
483 }
484 }
485
486 pub fn simple_ref(code: &str, message: &str, path: Option<&str>) -> Self {
488 Self {
489 code: code.to_string(),
490 message: message.to_string(),
491 path: path.map(|s| s.to_string()),
492 workflow_id: None,
493 task_id: None,
494 timestamp: Some(Utc::now().to_rfc3339()),
495 retry_attempted: None,
496 retry_count: None,
497 detail: None,
498 }
499 }
500
501 pub fn with_retry(mut self) -> Self {
503 self.retry_attempted = Some(true);
504 self.retry_count = Some(self.retry_count.unwrap_or(0) + 1);
505 self
506 }
507
508 pub fn builder(code: impl Into<String>, message: impl Into<String>) -> ErrorInfoBuilder {
510 ErrorInfoBuilder::new(code, message)
511 }
512}
513
514#[must_use = "ErrorInfoBuilder must be `.build()` to produce an ErrorInfo"]
516pub struct ErrorInfoBuilder {
517 code: String,
518 message: String,
519 path: Option<String>,
520 workflow_id: Option<String>,
521 task_id: Option<String>,
522 timestamp: Option<String>,
523 retry_attempted: Option<bool>,
524 retry_count: Option<u32>,
525 detail: Option<String>,
526}
527
528impl ErrorInfoBuilder {
529 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
531 Self {
532 code: code.into(),
533 message: message.into(),
534 path: None,
535 workflow_id: None,
536 task_id: None,
537 timestamp: Some(Utc::now().to_rfc3339()),
538 retry_attempted: None,
539 retry_count: None,
540 detail: None,
541 }
542 }
543
544 pub fn path(mut self, path: impl Into<String>) -> Self {
546 self.path = Some(path.into());
547 self
548 }
549
550 pub fn workflow_id(mut self, id: impl Into<String>) -> Self {
552 self.workflow_id = Some(id.into());
553 self
554 }
555
556 pub fn task_id(mut self, id: impl Into<String>) -> Self {
558 self.task_id = Some(id.into());
559 self
560 }
561
562 pub fn timestamp(mut self, timestamp: impl Into<String>) -> Self {
564 self.timestamp = Some(timestamp.into());
565 self
566 }
567
568 pub fn retry_attempted(mut self, attempted: bool) -> Self {
570 self.retry_attempted = Some(attempted);
571 self
572 }
573
574 pub fn retry_count(mut self, count: u32) -> Self {
576 self.retry_count = Some(count);
577 self
578 }
579
580 pub fn detail(mut self, detail: impl Into<String>) -> Self {
583 self.detail = Some(detail.into());
584 self
585 }
586
587 pub fn build(self) -> ErrorInfo {
589 ErrorInfo {
590 code: self.code,
591 message: self.message,
592 path: self.path,
593 workflow_id: self.workflow_id,
594 task_id: self.task_id,
595 timestamp: self.timestamp,
596 retry_attempted: self.retry_attempted,
597 retry_count: self.retry_count,
598 detail: self.detail,
599 }
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 #[test]
608 fn test_retryable_errors() {
609 assert!(
611 DataflowError::Http {
612 status: 500,
613 message: "Internal Server Error".to_string()
614 }
615 .retryable()
616 );
617 assert!(
618 DataflowError::Http {
619 status: 502,
620 message: "Bad Gateway".to_string()
621 }
622 .retryable()
623 );
624 assert!(
625 DataflowError::Http {
626 status: 503,
627 message: "Service Unavailable".to_string()
628 }
629 .retryable()
630 );
631 assert!(
632 DataflowError::Http {
633 status: 429,
634 message: "Too Many Requests".to_string()
635 }
636 .retryable()
637 );
638 assert!(
639 DataflowError::Http {
640 status: 408,
641 message: "Request Timeout".to_string()
642 }
643 .retryable()
644 );
645 assert!(
646 DataflowError::Http {
647 status: 0,
648 message: "Connection Error".to_string()
649 }
650 .retryable()
651 );
652 assert!(DataflowError::Timeout("Connection timeout".to_string()).retryable());
653 assert!(DataflowError::Io("Network error".to_string()).retryable());
654 }
655
656 #[test]
657 fn test_non_retryable_errors() {
658 assert!(
660 !DataflowError::Http {
661 status: 400,
662 message: "Bad Request".to_string()
663 }
664 .retryable()
665 );
666 assert!(
667 !DataflowError::Http {
668 status: 401,
669 message: "Unauthorized".to_string()
670 }
671 .retryable()
672 );
673 assert!(
674 !DataflowError::Http {
675 status: 403,
676 message: "Forbidden".to_string()
677 }
678 .retryable()
679 );
680 assert!(
681 !DataflowError::Http {
682 status: 404,
683 message: "Not Found".to_string()
684 }
685 .retryable()
686 );
687 assert!(!DataflowError::Validation("Invalid input".to_string()).retryable());
688 assert!(!DataflowError::LogicEvaluation("Invalid logic".to_string()).retryable());
689 assert!(!DataflowError::Deserialization("Invalid JSON".to_string()).retryable());
690 assert!(!DataflowError::Workflow("Invalid workflow".to_string()).retryable());
691 assert!(!DataflowError::Unknown("Unknown error".to_string()).retryable());
692 }
693
694 #[test]
695 fn test_function_execution_error_retryability() {
696 let retryable_source = DataflowError::Http {
698 status: 500,
699 message: "Server Error".to_string(),
700 };
701 let non_retryable_source = DataflowError::Validation("Invalid data".to_string());
702
703 let retryable_func_error =
704 DataflowError::function_execution("HTTP call failed", Some(retryable_source));
705 let non_retryable_func_error =
706 DataflowError::function_execution("Validation failed", Some(non_retryable_source));
707 let no_source_func_error = DataflowError::function_execution("Unknown failure", None);
708
709 assert!(retryable_func_error.retryable());
710 assert!(!non_retryable_func_error.retryable());
711 assert!(!no_source_func_error.retryable());
712 }
713
714 #[test]
715 fn test_error_info_builder() {
716 let error = ErrorInfo::builder("TEST_ERROR", "Test message").build();
718 assert_eq!(error.code, "TEST_ERROR");
719 assert_eq!(error.message, "Test message");
720 assert!(error.timestamp.is_some());
721 assert!(error.path.is_none());
722
723 let error = ErrorInfo::builder("VALIDATION_ERROR", "Field validation failed")
725 .path("data.email")
726 .workflow_id("workflow_1")
727 .task_id("validate_email")
728 .retry_attempted(true)
729 .retry_count(2)
730 .build();
731
732 assert_eq!(error.code, "VALIDATION_ERROR");
733 assert_eq!(error.message, "Field validation failed");
734 assert_eq!(error.path, Some("data.email".to_string()));
735 assert_eq!(error.workflow_id, Some("workflow_1".to_string()));
736 assert_eq!(error.task_id, Some("validate_email".to_string()));
737 assert_eq!(error.retry_attempted, Some(true));
738 assert_eq!(error.retry_count, Some(2));
739 }
740
741 #[test]
742 fn test_error_info_new_from_dataflow_error() {
743 let test_cases = vec![
745 (
746 DataflowError::Validation("test".to_string()),
747 "VALIDATION_ERROR",
748 ),
749 (
750 DataflowError::Workflow("test".to_string()),
751 "WORKFLOW_ERROR",
752 ),
753 (DataflowError::Task("test".to_string()), "TASK_ERROR"),
754 (
755 DataflowError::FunctionNotFound("test".to_string()),
756 "FUNCTION_NOT_FOUND",
757 ),
758 (
759 DataflowError::function_execution("test", None),
760 "FUNCTION_ERROR",
761 ),
762 (
763 DataflowError::LogicEvaluation("test".to_string()),
764 "LOGIC_ERROR",
765 ),
766 (DataflowError::http(404, "Not Found"), "HTTP_ERROR"),
767 (DataflowError::Timeout("test".to_string()), "TIMEOUT_ERROR"),
768 (DataflowError::Io("test".to_string()), "IO_ERROR"),
769 (
770 DataflowError::Deserialization("test".to_string()),
771 "DESERIALIZATION_ERROR",
772 ),
773 (DataflowError::Unknown("test".to_string()), "UNKNOWN_ERROR"),
774 ];
775
776 for (error, expected_code) in test_cases {
777 let info = ErrorInfo::new(
778 Some("workflow_1".to_string()),
779 Some("task_1".to_string()),
780 error,
781 );
782 assert_eq!(info.code, expected_code);
783 assert_eq!(info.workflow_id, Some("workflow_1".to_string()));
784 assert_eq!(info.task_id, Some("task_1".to_string()));
785 assert!(info.timestamp.is_some());
786 assert_eq!(info.retry_attempted, Some(false));
787 assert_eq!(info.retry_count, Some(0));
788 }
789 }
790
791 #[test]
792 fn test_error_info_simple_constructors() {
793 let error = ErrorInfo::simple(
795 "CUSTOM_ERROR".to_string(),
796 "Custom message".to_string(),
797 Some("data.field".to_string()),
798 );
799 assert_eq!(error.code, "CUSTOM_ERROR");
800 assert_eq!(error.message, "Custom message");
801 assert_eq!(error.path, Some("data.field".to_string()));
802 assert!(error.workflow_id.is_none());
803 assert!(error.task_id.is_none());
804 assert!(error.timestamp.is_some());
805
806 let error = ErrorInfo::simple_ref("REF_ERROR", "Ref message", Some("data.path"));
808 assert_eq!(error.code, "REF_ERROR");
809 assert_eq!(error.message, "Ref message");
810 assert_eq!(error.path, Some("data.path".to_string()));
811
812 let error = ErrorInfo::simple_ref("NO_PATH", "No path message", None);
814 assert!(error.path.is_none());
815 }
816
817 #[test]
818 fn test_error_info_with_retry() {
819 let error = ErrorInfo::simple_ref("TEST", "Test", None);
820 assert!(error.retry_attempted.is_none());
821 assert!(error.retry_count.is_none());
822
823 let error = error.with_retry();
824 assert_eq!(error.retry_attempted, Some(true));
825 assert_eq!(error.retry_count, Some(1));
826
827 let error = error.with_retry();
828 assert_eq!(error.retry_attempted, Some(true));
829 assert_eq!(error.retry_count, Some(2));
830 }
831
832 #[test]
833 fn test_error_display_messages() {
834 assert_eq!(
836 DataflowError::Validation("test".to_string()).to_string(),
837 "Validation error: test"
838 );
839 assert_eq!(
840 DataflowError::Workflow("test".to_string()).to_string(),
841 "Workflow error: test"
842 );
843 assert_eq!(
844 DataflowError::Task("test".to_string()).to_string(),
845 "Task error: test"
846 );
847 assert_eq!(
848 DataflowError::FunctionNotFound("test".to_string()).to_string(),
849 "Function not found: test"
850 );
851 assert_eq!(
852 DataflowError::http(404, "Not Found").to_string(),
853 "HTTP error: 404 - Not Found"
854 );
855 assert_eq!(
856 DataflowError::Timeout("test".to_string()).to_string(),
857 "Timeout error: test"
858 );
859 }
860
861 #[test]
862 fn test_error_conversions() {
863 let json_str = "invalid json";
865 let serde_result: std::result::Result<serde_json::Value, _> =
866 serde_json::from_str(json_str);
867 if let Err(e) = serde_result {
868 let dataflow_err = DataflowError::from_serde(e);
869 assert!(matches!(dataflow_err, DataflowError::Deserialization(_)));
870 }
871 }
872
873 #[test]
874 fn service_error_carries_kind_detail_and_declared_retryability() {
875 let e = DataflowError::service("circuit_open", "upstream unavailable")
876 .detail("connector 'billing' breaker open")
877 .retryable(true)
878 .build();
879
880 assert_eq!(e.kind(), Some("circuit_open"));
881 assert_eq!(e.detail(), Some("connector 'billing' breaker open"));
882 assert!(e.retryable());
883 }
884
885 #[test]
886 fn function_execution_inherits_kind_detail_and_retryable_from_a_wrapped_service_source() {
887 let inner = DataflowError::service("circuit_open", "upstream unavailable")
893 .detail("connector 'billing' breaker open since 12:04")
894 .retryable(true)
895 .build();
896 let wrapped = DataflowError::function_execution("calling billing connector", Some(inner));
897
898 assert_eq!(wrapped.kind(), Some("circuit_open"));
899 assert_eq!(
900 wrapped.detail(),
901 Some("connector 'billing' breaker open since 12:04")
902 );
903 assert!(wrapped.retryable());
904
905 assert_eq!(service_error_code(&wrapped), "circuit_open");
908 }
909
910 #[test]
911 fn service_display_hides_the_detail_but_debug_shows_it() {
912 let e = DataflowError::service("circuit_open", "upstream unavailable")
913 .detail("SECRET-TOPOLOGY")
914 .build();
915
916 assert_eq!(e.to_string(), "upstream unavailable");
918 assert!(!e.to_string().contains("SECRET-TOPOLOGY"));
919 assert!(format!("{e:?}").contains("SECRET-TOPOLOGY"));
921 }
922
923 #[test]
924 fn service_retryability_is_independent_of_every_other_field() {
925 let yes = DataflowError::service("k", "m").retryable(true).build();
926 let no = DataflowError::service("k", "m").retryable(false).build();
927 assert!(yes.retryable());
928 assert!(!no.retryable());
929 assert!(!DataflowError::service("k", "m").build().retryable());
931 }
932
933 #[test]
934 fn kind_and_detail_are_none_for_every_engine_owned_variant() {
935 let variants = [
936 DataflowError::Validation("v".into()),
937 DataflowError::FunctionExecution {
938 context: "c".into(),
939 source: None,
940 },
941 DataflowError::LogicEvaluation("l".into()),
942 DataflowError::Deserialization("d".into()),
943 DataflowError::Workflow("w".into()),
944 DataflowError::Task("t".into()),
945 DataflowError::FunctionNotFound("f".into()),
946 DataflowError::Http {
947 status: 500,
948 message: "h".into(),
949 },
950 DataflowError::Timeout("to".into()),
951 DataflowError::Io("io".into()),
952 DataflowError::Unknown("u".into()),
953 ];
954 assert_eq!(variants.len(), 11, "one assertion per engine-owned variant");
955 for v in &variants {
956 assert_eq!(v.kind(), None, "kind() for {v:?}");
957 assert_eq!(v.detail(), None, "detail() for {v:?}");
958 }
959 }
960
961 #[test]
962 fn service_error_code_passes_kind_through_verbatim() {
963 let e = DataflowError::service("circuit_open", "m").build();
966 let info = ErrorInfo::new(None, None, e);
967 assert_eq!(info.code, "circuit_open");
968 }
969
970 #[test]
971 fn an_empty_kind_falls_back_rather_than_recording_an_empty_code() {
972 let e = DataflowError::service("", "m").build();
973 let info = ErrorInfo::new(None, None, e);
974 assert_eq!(info.code, "TASK_ERROR");
975 assert!(!info.code.is_empty());
976 }
977
978 #[test]
979 fn a_non_ascii_kind_is_neither_panicked_on_nor_mangled() {
980 let e = DataflowError::service("limite_dépassé", "m").build();
981 let info = ErrorInfo::new(None, None, e);
982 assert_eq!(info.code, "limite_dépassé");
984 }
985
986 #[test]
987 fn error_info_lifts_the_detail_and_omits_it_when_absent() {
988 let with = ErrorInfo::new(
989 None,
990 None,
991 DataflowError::service("k", "m").detail("op only").build(),
992 );
993 assert_eq!(with.detail.as_deref(), Some("op only"));
994 assert!(serde_json::to_string(&with).unwrap().contains("detail"));
995
996 let without = ErrorInfo::new(None, None, DataflowError::service("k", "m").build());
998 assert_eq!(without.detail, None);
999 assert!(!serde_json::to_string(&without).unwrap().contains("detail"));
1000
1001 let engine_owned = ErrorInfo::new(None, None, DataflowError::Task("t".into()));
1002 assert_eq!(engine_owned.detail, None);
1003 assert!(
1004 !serde_json::to_string(&engine_owned)
1005 .unwrap()
1006 .contains("detail"),
1007 "the JSON shape is unchanged for every pre-existing error"
1008 );
1009 }
1010
1011 #[test]
1012 fn a_service_error_round_trips_through_serde_with_and_without_detail() {
1013 for e in [
1014 DataflowError::service("k", "m")
1015 .detail("d")
1016 .retryable(true)
1017 .build(),
1018 DataflowError::service("k", "m").build(),
1019 ] {
1020 let json = serde_json::to_string(&e).unwrap();
1021 let back: DataflowError = serde_json::from_str(&json).unwrap();
1022 assert_eq!(back.kind(), e.kind());
1023 assert_eq!(back.detail(), e.detail());
1024 assert_eq!(back.retryable(), e.retryable());
1025 }
1026
1027 let bare = DataflowError::service("k", "m").build();
1029 assert!(!serde_json::to_string(&bare).unwrap().contains("detail"));
1030 }
1031}