1use serde::{Deserialize, Serialize};
4
5use crate::Payload;
6
7#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq)]
9pub enum ActivityErrorKind {
10 Retryable,
12 Terminal,
14}
15
16#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, thiserror::Error)]
21#[error("{message}")]
22pub struct ActivityError {
23 pub kind: ActivityErrorKind,
25 pub message: String,
27 pub details: Option<Payload>,
29}
30
31impl ActivityError {
32 #[must_use]
34 pub fn is_retryable(&self) -> bool {
35 matches!(self.kind, ActivityErrorKind::Retryable)
36 }
37}
38
39#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, thiserror::Error)]
41#[error("{message}")]
42pub struct WorkflowError {
43 pub message: String,
45 pub details: Option<Payload>,
47}
48
49impl From<ActivityError> for WorkflowError {
50 fn from(error: ActivityError) -> Self {
51 Self {
52 message: error.message,
53 details: error.details,
54 }
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use serde_json::json;
61
62 use super::{ActivityError, ActivityErrorKind, WorkflowError};
63 use crate::Payload;
64
65 #[test]
66 fn activity_error_reports_retryable_classification() {
67 let error = ActivityError {
68 kind: ActivityErrorKind::Retryable,
69 message: String::from("temporary outage"),
70 details: None,
71 };
72
73 assert!(error.is_retryable());
74 }
75
76 #[test]
77 fn activity_error_reports_terminal_classification() {
78 let error = ActivityError {
79 kind: ActivityErrorKind::Terminal,
80 message: String::from("invalid request"),
81 details: None,
82 };
83
84 assert!(!error.is_retryable());
85 }
86
87 #[test]
88 fn errors_round_trip_through_json() -> Result<(), Box<dyn std::error::Error>> {
89 let activity_error = ActivityError {
90 kind: ActivityErrorKind::Retryable,
91 message: String::from("connection reset"),
92 details: Some(Payload::from_json(&json!({"retry_after_ms": 500}))?),
93 };
94 let json = serde_json::to_string(&activity_error)?;
95 let decoded: ActivityError = serde_json::from_str(&json)?;
96 assert_eq!(activity_error, decoded);
97
98 let workflow_error = WorkflowError {
99 message: String::from("workflow failed"),
100 details: None,
101 };
102 let json = serde_json::to_string(&workflow_error)?;
103 let decoded: WorkflowError = serde_json::from_str(&json)?;
104 assert_eq!(workflow_error, decoded);
105
106 Ok(())
107 }
108
109 #[test]
110 fn workflow_error_from_activity_error_preserves_message_and_details()
111 -> Result<(), Box<dyn std::error::Error>> {
112 let details = Payload::from_json(&json!({"code": "rate_limited", "after_ms": 1000}))?;
113 let activity_error = ActivityError {
114 kind: ActivityErrorKind::Terminal,
115 message: String::from("activity failed permanently"),
116 details: Some(details.clone()),
117 };
118
119 let workflow_error = WorkflowError::from(activity_error);
120
121 assert_eq!(workflow_error.message, "activity failed permanently");
122 assert_eq!(workflow_error.details, Some(details));
123 Ok(())
124 }
125}