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 PolicyRefused,
15 Terminal,
17}
18
19#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, thiserror::Error)]
24#[error("{message}")]
25pub struct ActivityError {
26 pub kind: ActivityErrorKind,
28 pub message: String,
30 pub details: Option<Payload>,
32}
33
34impl ActivityError {
35 #[must_use]
40 pub fn is_retryable(&self) -> bool {
41 matches!(self.kind, ActivityErrorKind::Retryable)
42 }
43
44 #[must_use]
52 pub fn is_settled(&self) -> bool {
53 matches!(self.kind, ActivityErrorKind::Terminal)
54 }
55}
56
57#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, thiserror::Error)]
59#[error("{message}")]
60pub struct WorkflowError {
61 pub message: String,
63 pub details: Option<Payload>,
65}
66
67impl From<ActivityError> for WorkflowError {
68 fn from(error: ActivityError) -> Self {
69 Self {
70 message: error.message,
71 details: error.details,
72 }
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use serde_json::json;
79
80 use super::{ActivityError, ActivityErrorKind, WorkflowError};
81 use crate::Payload;
82
83 #[test]
84 fn activity_error_reports_retryable_classification() {
85 let error = ActivityError {
86 kind: ActivityErrorKind::Retryable,
87 message: String::from("temporary outage"),
88 details: None,
89 };
90
91 assert!(error.is_retryable());
92 }
93
94 #[test]
95 fn activity_error_reports_terminal_classification() {
96 let error = ActivityError {
97 kind: ActivityErrorKind::Terminal,
98 message: String::from("invalid request"),
99 details: None,
100 };
101
102 assert!(!error.is_retryable());
103 assert!(error.is_settled());
104 }
105
106 #[test]
107 fn policy_refused_is_neither_retryable_nor_settled() {
108 let error = ActivityError {
109 kind: ActivityErrorKind::PolicyRefused,
110 message: String::from("provider policy"),
111 details: None,
112 };
113
114 assert!(!error.is_retryable());
115 assert!(!error.is_settled());
116 }
117
118 #[test]
119 fn errors_round_trip_through_json() -> Result<(), Box<dyn std::error::Error>> {
120 let activity_error = ActivityError {
121 kind: ActivityErrorKind::Retryable,
122 message: String::from("connection reset"),
123 details: Some(Payload::from_json(&json!({"retry_after_ms": 500}))?),
124 };
125 let json = serde_json::to_string(&activity_error)?;
126 let decoded: ActivityError = serde_json::from_str(&json)?;
127 assert_eq!(activity_error, decoded);
128
129 let workflow_error = WorkflowError {
130 message: String::from("workflow failed"),
131 details: None,
132 };
133 let json = serde_json::to_string(&workflow_error)?;
134 let decoded: WorkflowError = serde_json::from_str(&json)?;
135 assert_eq!(workflow_error, decoded);
136
137 Ok(())
138 }
139
140 #[test]
141 fn workflow_error_from_activity_error_preserves_message_and_details()
142 -> Result<(), Box<dyn std::error::Error>> {
143 let details = Payload::from_json(&json!({"code": "rate_limited", "after_ms": 1000}))?;
144 let activity_error = ActivityError {
145 kind: ActivityErrorKind::Terminal,
146 message: String::from("activity failed permanently"),
147 details: Some(details.clone()),
148 };
149
150 let workflow_error = WorkflowError::from(activity_error);
151
152 assert_eq!(workflow_error.message, "activity failed permanently");
153 assert_eq!(workflow_error.details, Some(details));
154 Ok(())
155 }
156}