1use super::AuthoredPath;
4use serde::ser::SerializeStruct;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use std::borrow::Cow;
7use std::fmt;
8use std::str::FromStr;
9use std::sync::Arc;
10
11mod error_code_serde {
12 use super::{Cow, Deserialize, Deserializer, FromStr, Serializer};
13 use datalogic_rs::ErrorCode;
14
15 pub(super) fn serialize<S: Serializer>(code: &ErrorCode, s: S) -> Result<S::Ok, S::Error> {
16 s.serialize_str(code.as_str())
17 }
18
19 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<ErrorCode, D::Error> {
20 let text = Cow::<str>::deserialize(d)?;
21 ErrorCode::from_str(&text).map_err(serde::de::Error::custom)
22 }
23}
24
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32#[non_exhaustive]
33pub struct ExprPathStep {
34 pub node_id: u32,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub operator: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub arg_index: Option<u32>,
42 pub json_pointer: String,
44}
45
46impl From<datalogic_rs::PathStep> for ExprPathStep {
47 fn from(step: datalogic_rs::PathStep) -> Self {
48 Self {
49 node_id: step.node_id,
50 operator: step.operator,
51 arg_index: step.arg_index,
52 json_pointer: step.json_pointer,
53 }
54 }
55}
56
57impl From<&datalogic_rs::PathStep> for ExprPathStep {
58 fn from(step: &datalogic_rs::PathStep) -> Self {
59 Self {
60 node_id: step.node_id,
61 operator: step.operator.clone(),
62 arg_index: step.arg_index,
63 json_pointer: step.json_pointer.clone(),
64 }
65 }
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
71#[non_exhaustive]
72pub struct BudgetCause {
73 pub used: u64,
75 pub limit: u64,
77}
78
79impl BudgetCause {
80 pub const fn new(used: u64, limit: u64) -> Self {
82 Self { used, limit }
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
93#[non_exhaustive]
94pub struct ExprCause {
95 #[serde(with = "error_code_serde")]
97 pub code: datalogic_rs::ErrorCode,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub operator: Option<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub expr_path: Option<String>,
105 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 pub steps: Vec<ExprPathStep>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub budget: Option<BudgetCause>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub ops: Option<u64>,
116}
117
118impl ExprCause {
119 pub const fn ops_spent(&self) -> Option<u64> {
121 self.ops
122 }
123
124 pub(crate) fn from_datalogic(
127 e: &datalogic_rs::Error,
128 logic: Option<&datalogic_rs::Logic>,
129 ops: Option<u64>,
130 ) -> Self {
131 let (expr_path, steps) = match logic {
132 Some(logic) => {
133 let resolved = e.resolve_path(logic);
134 let expr_path = resolved.last().map(|s| s.json_pointer.clone());
135 let steps = resolved.into_iter().map(ExprPathStep::from).collect();
136 (expr_path, steps)
137 }
138 None => (None, Vec::new()),
139 };
140 let budget = match e.kind {
141 datalogic_rs::ErrorKind::BudgetExceeded { budget, spent } => {
142 Some(BudgetCause::new(spent, budget))
143 }
144 _ => None,
145 };
146 Self {
147 code: e.code(),
148 operator: e.operator().map(str::to_owned),
149 expr_path,
150 steps,
151 budget,
152 ops: ops.or_else(|| budget.as_ref().map(|b| b.used)),
153 }
154 }
155
156 pub(crate) fn type_mismatch(ops: Option<u64>) -> Self {
159 Self {
160 code: datalogic_rs::ErrorCode::TypeError,
161 operator: None,
162 expr_path: Some(String::new()),
163 steps: Vec::new(),
164 budget: None,
165 ops,
166 }
167 }
168
169 pub(crate) fn budget_exceeded(used: u64, limit: u64, ops: Option<u64>) -> Self {
172 Self {
173 code: datalogic_rs::ErrorCode::BudgetExceeded,
174 operator: None,
175 expr_path: None,
176 steps: Vec::new(),
177 budget: Some(BudgetCause::new(used, limit)),
178 ops,
179 }
180 }
181}
182
183#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "snake_case")]
186#[non_exhaustive]
187pub enum SiteKind {
188 WorkflowCondition,
190 GroupCondition,
192 TaskCondition,
194 FilterCondition,
196 ValidationRule,
198 ValidationMessage,
200 MappingPath,
202 MappingLogic,
204 LoopOver,
206 ForEachOver,
208 Task,
210}
211
212#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
214#[non_exhaustive]
215pub struct ErrorSite {
216 pub kind: SiteKind,
218 pub workflow_id: Arc<str>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub task_id: Option<Arc<str>>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub group_id: Option<Arc<str>>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub rule_index: Option<usize>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub mapping_index: Option<usize>,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub target_path: Option<Arc<str>>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub authored_path: Option<AuthoredPath>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub loop_counter: Option<i64>,
241}
242
243impl ErrorSite {
244 pub fn new(kind: SiteKind, workflow_id: Arc<str>) -> Self {
246 Self {
247 kind,
248 workflow_id,
249 task_id: None,
250 group_id: None,
251 rule_index: None,
252 mapping_index: None,
253 target_path: None,
254 authored_path: None,
255 loop_counter: None,
256 }
257 }
258
259 #[must_use]
261 pub fn with_task(mut self, task_id: Arc<str>) -> Self {
262 self.task_id = Some(task_id);
263 self
264 }
265
266 #[must_use]
268 pub fn with_group(mut self, group_id: Arc<str>) -> Self {
269 self.group_id = Some(group_id);
270 self
271 }
272
273 #[must_use]
275 pub fn with_path(mut self, path: AuthoredPath) -> Self {
276 self.authored_path = Some(path);
277 self
278 }
279
280 #[must_use]
282 pub fn with_loop_counter(mut self, loop_counter: Option<i64>) -> Self {
283 self.loop_counter = loop_counter;
284 self
285 }
286}
287
288#[derive(Clone)]
290pub struct Error {
291 kind: ErrorKind,
292 message: Cow<'static, str>,
293 retryable: bool,
294 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
295 expr_cause: Option<Arc<ExprCause>>,
296 site: Option<Arc<ErrorSite>>,
297}
298
299#[derive(Clone, Debug, PartialEq, Eq)]
301#[non_exhaustive]
302pub enum ErrorKind {
303 Config,
305 Compile,
307 Eval,
309 Budget,
311 TimedOut,
313 Cancelled,
315 Service {
317 code: Cow<'static, str>,
319 },
320}
321
322pub(crate) const TASK_ERROR: &str = "TASK_ERROR";
325pub(crate) const TASK_STATUS_ERROR: &str = "TASK_STATUS_ERROR";
327pub(crate) const VALIDATION_ERROR: &str = "VALIDATION_ERROR";
329pub(crate) const MAPPING_ERROR: &str = "MAPPING_ERROR";
332pub(crate) const EVALUATION_ERROR: &str = "EVALUATION_ERROR";
335
336impl Error {
337 pub(crate) fn new(kind: ErrorKind, message: impl Into<Cow<'static, str>>) -> Self {
338 Self {
339 retryable: kind == ErrorKind::TimedOut,
340 kind,
341 message: message.into(),
342 source: None,
343 expr_cause: None,
344 site: None,
345 }
346 }
347
348 pub fn service(
350 code: impl Into<Cow<'static, str>>,
351 message: impl Into<Cow<'static, str>>,
352 ) -> Self {
353 Self {
354 kind: ErrorKind::Service { code: code.into() },
355 message: message.into(),
356 retryable: false,
357 source: None,
358 expr_cause: None,
359 site: None,
360 }
361 }
362
363 pub fn budget(message: impl Into<Cow<'static, str>>) -> Self {
365 Self::new(ErrorKind::Budget, message)
366 }
367
368 pub fn kind(&self) -> &ErrorKind {
370 &self.kind
371 }
372
373 pub fn message(&self) -> &str {
375 &self.message
376 }
377
378 #[must_use]
380 pub fn with_retryable(mut self, yes: bool) -> Self {
381 self.retryable = yes;
382 self
383 }
384
385 #[must_use]
387 pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
388 self.source = Some(Arc::new(source));
389 self
390 }
391
392 pub fn datalogic_error(&self) -> Option<&datalogic_rs::Error> {
395 self.source
396 .as_deref()
397 .and_then(|s| s.downcast_ref::<datalogic_rs::Error>())
398 }
399
400 #[must_use]
402 pub fn with_expr_cause(mut self, cause: ExprCause) -> Self {
403 self.expr_cause = Some(Arc::new(cause));
404 self
405 }
406
407 pub fn expr_cause(&self) -> Option<&ExprCause> {
410 self.expr_cause.as_deref()
411 }
412
413 pub(crate) fn expr_cause_arc(&self) -> Option<Arc<ExprCause>> {
414 self.expr_cause.clone()
415 }
416
417 #[must_use]
419 pub fn with_site(mut self, site: ErrorSite) -> Self {
420 self.site = Some(Arc::new(site));
421 self
422 }
423
424 pub fn site(&self) -> Option<&ErrorSite> {
426 self.site.as_deref()
427 }
428
429 pub(crate) fn site_arc(&self) -> Option<Arc<ErrorSite>> {
430 self.site.clone()
431 }
432
433 pub fn is_retryable(&self) -> bool {
436 self.retryable
437 }
438
439 #[must_use]
442 pub fn wrap(mut self, context: impl fmt::Display) -> Self {
443 self.message = Cow::Owned(format!("{context}: {}", self.message));
444 self
445 }
446
447 pub(crate) fn with_message(mut self, message: impl Into<Cow<'static, str>>) -> Self {
450 self.message = message.into();
451 self
452 }
453
454 pub fn code(&self) -> &str {
456 match &self.kind {
457 ErrorKind::Service { code } if !code.is_empty() => code,
458 _ => self.code_static(),
459 }
460 }
461
462 pub(crate) fn code_static(&self) -> &'static str {
465 match &self.kind {
466 ErrorKind::Eval => "LOGIC_ERROR",
467 ErrorKind::Budget => "BUDGET_EXCEEDED",
468 ErrorKind::TimedOut => "TIMEOUT_ERROR",
469 ErrorKind::Cancelled => "CANCELLED",
470 ErrorKind::Config => "CONFIG_ERROR",
471 ErrorKind::Compile => "COMPILATION_ERROR",
472 ErrorKind::Service { .. } => TASK_ERROR,
473 }
474 }
475
476 pub(crate) fn record_text(&self) -> Cow<'_, str> {
481 let prefix = match &self.kind {
482 ErrorKind::Eval => "Logic evaluation error: ",
483 ErrorKind::TimedOut => "Timeout error: ",
484 ErrorKind::Budget
485 | ErrorKind::Config
486 | ErrorKind::Compile
487 | ErrorKind::Cancelled
488 | ErrorKind::Service { .. } => return Cow::Borrowed(&self.message),
489 };
490 Cow::Owned(format!("{prefix}{}", self.message))
491 }
492}
493
494impl fmt::Display for Error {
495 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496 f.write_str(&self.message)
497 }
498}
499
500impl fmt::Debug for Error {
501 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502 let mut s = f.debug_struct("Error");
503 s.field("kind", &self.kind)
504 .field("message", &self.message)
505 .field("retryable", &self.is_retryable())
506 .field("source", &self.source);
507 if let Some(cause) = &self.expr_cause {
508 s.field("expr_cause", cause);
509 }
510 if let Some(site) = &self.site {
511 s.field("site", site);
512 }
513 s.finish_non_exhaustive()
514 }
515}
516
517impl std::error::Error for Error {
518 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
519 self.source.as_deref().map(|s| s as _)
520 }
521}
522
523pub type Result<T> = std::result::Result<T, Error>;
525
526#[derive(Clone, Debug, Serialize, Deserialize)]
532#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
533#[non_exhaustive]
534pub struct ErrorRecord {
535 pub code: String,
537 pub message: String,
539 pub path: Option<String>,
541 #[serde(skip_serializing_if = "Option::is_none")]
544 #[cfg_attr(feature = "ts", ts(optional))]
545 pub workflow_id: Option<Arc<str>>,
546 #[serde(skip_serializing_if = "Option::is_none")]
548 #[cfg_attr(feature = "ts", ts(optional))]
549 pub task_id: Option<Arc<str>>,
550 #[serde(skip_serializing_if = "Option::is_none")]
552 #[cfg_attr(feature = "ts", ts(optional))]
553 pub timestamp: Option<Arc<str>>,
554 #[serde(default, skip_serializing_if = "Option::is_none")]
556 #[cfg_attr(feature = "ts", ts(optional))]
557 pub detail: Option<String>,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
560 #[cfg_attr(feature = "ts", ts(optional))]
561 pub element_index: Option<usize>,
562 #[serde(skip)]
565 #[cfg_attr(feature = "ts", ts(skip))]
566 pub cause: Option<Arc<ExprCause>>,
567 #[serde(skip)]
570 #[cfg_attr(feature = "ts", ts(skip))]
571 pub site: Option<Arc<ErrorSite>>,
572}
573
574impl PartialEq for ErrorRecord {
575 fn eq(&self, other: &Self) -> bool {
576 self.code == other.code
577 && self.message == other.message
578 && self.path == other.path
579 && self.workflow_id == other.workflow_id
580 && self.task_id == other.task_id
581 && self.timestamp == other.timestamp
582 && self.detail == other.detail
583 && self.element_index == other.element_index
584 }
585}
586
587impl Eq for ErrorRecord {}
588
589impl ErrorRecord {
590 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
592 Self {
593 code: code.into(),
594 message: message.into(),
595 path: None,
596 workflow_id: None,
597 task_id: None,
598 timestamp: None,
599 detail: None,
600 element_index: None,
601 cause: None,
602 site: None,
603 }
604 }
605
606 pub fn cause(&self) -> Option<&ExprCause> {
609 self.cause.as_deref()
610 }
611
612 #[must_use]
614 pub fn with_cause(mut self, cause: ExprCause) -> Self {
615 self.cause = Some(Arc::new(cause));
616 self
617 }
618
619 pub fn site(&self) -> Option<&ErrorSite> {
621 self.site.as_deref()
622 }
623
624 #[must_use]
626 pub fn with_site(mut self, site: ErrorSite) -> Self {
627 self.site = Some(Arc::new(site));
628 self
629 }
630
631 pub(crate) fn stamp(
638 &mut self,
639 workflow_id: &Arc<str>,
640 task_id: Option<&Arc<str>>,
641 now: &Arc<str>,
642 element_index: Option<usize>,
643 ) {
644 if self.workflow_id.is_none() {
645 self.workflow_id = Some(Arc::clone(workflow_id));
646 }
647 if self.task_id.is_none() {
648 self.task_id = task_id.map(Arc::clone);
649 }
650 if self.timestamp.is_none() {
651 self.timestamp = Some(Arc::clone(now));
652 }
653 if self.element_index.is_none() {
654 self.element_index = element_index;
655 }
656 }
657}
658
659impl From<&Error> for ErrorRecord {
660 fn from(error: &Error) -> Self {
661 let mut record = Self::new(error.code(), error.message());
662 record.cause = error.expr_cause_arc();
663 record.site = error.site_arc();
664 record
665 }
666}
667
668impl Serialize for Error {
671 fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
672 let mut o = s.serialize_struct("Error", 3)?;
673 o.serialize_field("code", self.code())?;
674 o.serialize_field("message", self.message())?;
675 o.serialize_field("retryable", &self.is_retryable())?;
676 o.end()
677 }
678}
679
680#[cfg(feature = "ts")]
683crate::base::ts::ts_decl!(
684 Error,
685 "DataflowError",
686 [
687 " An error as JSON: its code, its message, and whether a retry may",
688 " succeed.",
689 ],
690 |_cfg| {
691 use crate::base::ts::{Prop, object};
692 object(&[
693 Prop::new("code", "string", &[" The recorded code."]),
694 Prop::new("message", "string", &[" The human-readable message."]),
695 Prop::new("retryable", "boolean", &[" May a retry succeed?"]),
696 ])
697 }
698);
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703
704 #[test]
705 fn record_text_prefixes_two_kinds_and_is_the_message_otherwise() {
706 let m = "the message";
708 for (kind, expected) in [
709 (ErrorKind::Eval, "Logic evaluation error: the message"),
710 (ErrorKind::TimedOut, "Timeout error: the message"),
711 (ErrorKind::Budget, m),
712 (ErrorKind::Service { code: "X".into() }, m),
713 (ErrorKind::Config, m),
714 (ErrorKind::Compile, m),
715 (ErrorKind::Cancelled, m),
716 ] {
717 assert_eq!(
718 Error::new(kind.clone(), m).record_text(),
719 expected,
720 "{kind:?}"
721 );
722 }
723 }
724
725 fn kinds() -> Vec<(ErrorKind, &'static str)> {
726 vec![
727 (ErrorKind::Eval, "LOGIC_ERROR"),
728 (ErrorKind::Budget, "BUDGET_EXCEEDED"),
729 (ErrorKind::TimedOut, "TIMEOUT_ERROR"),
730 (ErrorKind::Cancelled, "CANCELLED"),
731 (ErrorKind::Config, "CONFIG_ERROR"),
732 (ErrorKind::Compile, "COMPILATION_ERROR"),
733 (
734 ErrorKind::Service {
735 code: "X_FAIL".into(),
736 },
737 "X_FAIL",
738 ),
739 (ErrorKind::Service { code: "".into() }, "TASK_ERROR"),
740 ]
741 }
742
743 #[test]
744 fn code_gives_each_row_of_the_table() {
745 for (kind, code) in kinds() {
746 assert_eq!(Error::new(kind, "m").code(), code);
747 }
748 }
749
750 #[test]
751 fn only_a_timeout_is_retryable_by_default() {
752 for (kind, _) in kinds() {
753 let timed_out = kind == ErrorKind::TimedOut;
754 assert_eq!(Error::new(kind, "m").is_retryable(), timed_out);
755 }
756 assert!(Error::service("A", "m").with_retryable(true).is_retryable());
757 assert!(
758 !Error::new(ErrorKind::TimedOut, "m")
759 .with_retryable(false)
760 .is_retryable()
761 );
762 }
763
764 #[test]
765 fn wrap_keeps_kind_retryable_and_source() {
766 let source = std::io::Error::other("disk");
767 let error = Error::new(ErrorKind::Eval, "bad")
768 .with_retryable(true)
769 .with_source(source)
770 .wrap("task t");
771 assert_eq!(error.to_string(), "task t: bad");
772 assert_eq!(error.kind(), &ErrorKind::Eval);
773 assert!(error.is_retryable());
774 let source = std::error::Error::source(&error).expect("source kept");
775 assert_eq!(source.to_string(), "disk");
776 }
777
778 #[test]
779 fn debug_shows_kind_message_and_retryable() {
780 let text = format!("{:?}", Error::new(ErrorKind::Budget, "over"));
781 assert!(text.contains("Budget") && text.contains("over") && text.contains("retryable"));
782 }
783
784 #[test]
785 fn a_stored_record_with_3x_retry_keys_reads_and_writes_without_them() {
786 let stored =
787 r#"{"code":"X","message":"m","path":null,"retry_attempted":true,"retry_count":2}"#;
788 let record: ErrorRecord = serde_json::from_str(stored).unwrap();
789 assert_eq!(
790 serde_json::to_string(&record).unwrap(),
791 r#"{"code":"X","message":"m","path":null}"#
792 );
793 }
794
795 #[test]
796 fn stamp_fills_only_what_the_record_leaves_unset() {
797 let mut record = ErrorRecord::new("X", "m");
798 record.task_id = Some(Arc::from("own"));
799 record.stamp(
800 &Arc::from("w"),
801 Some(&Arc::from("t")),
802 &Arc::from("now"),
803 Some(3),
804 );
805 assert_eq!(
806 serde_json::to_string(&record).unwrap(),
807 r#"{"code":"X","message":"m","path":null,"workflow_id":"w","task_id":"own","timestamp":"now","element_index":3}"#
808 );
809 let mut workflow_own = ErrorRecord::new("X", "m");
810 workflow_own.stamp(&Arc::from("w"), None, &Arc::from("now"), None);
811 assert_eq!(workflow_own.task_id, None);
812 }
813
814 #[test]
815 fn a_record_from_an_error_carries_only_code_and_message() {
816 let record = ErrorRecord::from(&Error::service("X", "boom"));
817 assert_eq!(
818 serde_json::to_string(&record).unwrap(),
819 r#"{"code":"X","message":"boom","path":null}"#
820 );
821 }
822}