1use serde_json::{json, Value};
2use std::fmt;
3
4#[derive(Debug, Clone)]
6pub struct ErrorFields {
7 pub message: String,
8 pub task: String,
9 pub instance: String,
10 pub status: Option<Value>,
11 pub title: Option<String>,
12 pub detail: Option<String>,
13 pub original_type: Option<String>,
14}
15
16impl ErrorFields {
17 fn new(
18 message: impl Into<String>,
19 task: impl Into<String>,
20 instance: impl Into<String>,
21 ) -> Self {
22 Self {
23 message: message.into(),
24 task: task.into(),
25 instance: instance.into(),
26 status: None,
27 title: None,
28 detail: None,
29 original_type: None,
30 }
31 }
32
33 fn with_status(mut self, status: Option<Value>) -> Self {
34 self.status = status;
35 self
36 }
37
38 fn with_title(mut self, title: Option<String>) -> Self {
39 self.title = title;
40 self
41 }
42
43 fn with_detail(mut self, detail: Option<String>) -> Self {
44 self.detail = detail;
45 self
46 }
47
48 fn with_original_type(mut self, original_type: Option<String>) -> Self {
49 self.original_type = original_type;
50 self
51 }
52
53 fn instance_opt(&self) -> Option<&str> {
54 if self.instance.is_empty() {
55 None
56 } else {
57 Some(&self.instance)
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum ErrorKind {
65 Validation,
66 Expression,
67 Runtime,
68 Timeout,
69 Communication,
70 Authentication,
71 Authorization,
72 Configuration,
73}
74
75impl ErrorKind {
76 pub fn as_str(&self) -> &'static str {
78 match self {
79 ErrorKind::Validation => "validation",
80 ErrorKind::Expression => "expression",
81 ErrorKind::Runtime => "runtime",
82 ErrorKind::Timeout => "timeout",
83 ErrorKind::Communication => "communication",
84 ErrorKind::Authentication => "authentication",
85 ErrorKind::Authorization => "authorization",
86 ErrorKind::Configuration => "configuration",
87 }
88 }
89
90 pub fn type_uri(&self) -> &'static str {
92 match self {
93 ErrorKind::Validation => "https://serverlessworkflow.io/spec/1.0.0/errors/validation",
94 ErrorKind::Expression => "https://serverlessworkflow.io/spec/1.0.0/errors/expression",
95 ErrorKind::Runtime => "https://serverlessworkflow.io/spec/1.0.0/errors/runtime",
96 ErrorKind::Timeout => "https://serverlessworkflow.io/spec/1.0.0/errors/timeout",
97 ErrorKind::Communication => {
98 "https://serverlessworkflow.io/spec/1.0.0/errors/communication"
99 }
100 ErrorKind::Authentication => {
101 "https://serverlessworkflow.io/spec/1.0.0/errors/authentication"
102 }
103 ErrorKind::Authorization => {
104 "https://serverlessworkflow.io/spec/1.0.0/errors/authorization"
105 }
106 ErrorKind::Configuration => {
107 "https://serverlessworkflow.io/spec/1.0.0/errors/configuration"
108 }
109 }
110 }
111
112 pub fn from_type_str(error_type: &str) -> Self {
116 const TYPE_MAP: &[(&str, ErrorKind)] = &[
117 ("validation", ErrorKind::Validation),
118 ("expression", ErrorKind::Expression),
119 ("timeout", ErrorKind::Timeout),
120 ("communication", ErrorKind::Communication),
121 ("authentication", ErrorKind::Authentication),
122 ("authorization", ErrorKind::Authorization),
123 ("configuration", ErrorKind::Configuration),
124 ];
125 TYPE_MAP
126 .iter()
127 .find(|(suffix, _)| {
128 error_type.ends_with(suffix)
129 && (error_type.len() == suffix.len()
130 || error_type
131 .as_bytes()
132 .get(error_type.len() - suffix.len() - 1)
133 == Some(&b'/'))
134 })
135 .map(|(_, kind)| *kind)
136 .unwrap_or(ErrorKind::Runtime)
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct WorkflowError {
143 kind: ErrorKind,
144 fields: ErrorFields,
145}
146
147impl std::error::Error for WorkflowError {}
148
149impl fmt::Display for WorkflowError {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 write!(
152 f,
153 "{} error in task '{}': {}",
154 self.kind.as_str(),
155 self.fields.task,
156 self.fields.message
157 )
158 }
159}
160
161impl WorkflowError {
162 pub fn kind(&self) -> ErrorKind {
164 self.kind
165 }
166
167 pub fn fields(&self) -> &ErrorFields {
169 &self.fields
170 }
171
172 pub fn validation(message: impl Into<String>, task: impl Into<String>) -> Self {
174 Self {
175 kind: ErrorKind::Validation,
176 fields: ErrorFields::new(message, task, ""),
177 }
178 }
179
180 pub fn expression(message: impl Into<String>, task: impl Into<String>) -> Self {
182 Self {
183 kind: ErrorKind::Expression,
184 fields: ErrorFields::new(message, task, ""),
185 }
186 }
187
188 pub fn runtime(
190 message: impl Into<String>,
191 task: impl Into<String>,
192 instance: impl Into<String>,
193 ) -> Self {
194 Self {
195 kind: ErrorKind::Runtime,
196 fields: ErrorFields::new(message, task, instance),
197 }
198 }
199
200 pub fn runtime_simple(message: impl Into<String>, task: impl Into<String>) -> Self {
202 Self::runtime(message, task, "")
203 }
204
205 pub fn timeout(message: impl Into<String>, task: impl Into<String>) -> Self {
208 Self {
209 kind: ErrorKind::Timeout,
210 fields: ErrorFields::new(message, task, "").with_status(Some(json!(408))),
211 }
212 }
213
214 pub fn communication(message: impl Into<String>, task: impl Into<String>) -> Self {
216 Self {
217 kind: ErrorKind::Communication,
218 fields: ErrorFields::new(message, task, ""),
219 }
220 }
221
222 pub fn communication_with_status(
224 message: impl Into<String>,
225 task: impl Into<String>,
226 status_code: u16,
227 ) -> Self {
228 Self {
229 kind: ErrorKind::Communication,
230 fields: ErrorFields::new(message, task, "").with_status(Some(Value::from(status_code))),
231 }
232 }
233
234 pub fn typed(
236 error_type: &str,
237 detail: String,
238 task: String,
239 instance: String,
240 status: Option<Value>,
241 title: Option<String>,
242 ) -> Self {
243 let details = if detail.is_empty() {
244 None
245 } else {
246 Some(detail)
247 };
248 let fields = ErrorFields::new(details.clone().unwrap_or_default(), task, instance)
249 .with_status(status)
250 .with_title(title)
251 .with_detail(details)
252 .with_original_type(Some(error_type.to_string()));
253
254 let kind = ErrorKind::from_type_str(error_type);
255
256 Self { kind, fields }
257 }
258
259 pub fn error_type(&self) -> &str {
261 self.fields
262 .original_type
263 .as_deref()
264 .unwrap_or(self.kind.type_uri())
265 }
266
267 pub fn error_type_short(&self) -> &str {
269 if let Some(ot) = &self.fields.original_type {
270 if let Some(short) = ot.rsplit('/').next() {
271 return short;
272 }
273 }
274 self.kind.as_str()
275 }
276
277 pub fn task(&self) -> &str {
279 &self.fields.task
280 }
281
282 pub fn instance(&self) -> Option<&str> {
284 self.fields.instance_opt()
285 }
286
287 pub fn status(&self) -> Option<&Value> {
289 self.fields.status.as_ref()
290 }
291
292 pub fn title(&self) -> Option<&str> {
294 self.fields.title.as_deref()
295 }
296
297 pub fn detail(&self) -> Option<&str> {
299 self.fields.detail.as_deref()
300 }
301
302 pub fn to_value(&self) -> Value {
304 let mut map = serde_json::Map::new();
305 map.insert(
306 "type".to_string(),
307 Value::String(self.error_type().to_string()),
308 );
309 if let Some(status) = self.status() {
310 map.insert("status".to_string(), status.clone());
311 }
312 if let Some(title) = self.title() {
313 map.insert("title".to_string(), Value::String(title.to_string()));
314 }
315 if let Some(detail) = self.detail() {
316 map.insert("details".to_string(), Value::String(detail.to_string()));
317 }
318 if let Some(instance) = self.instance() {
319 map.insert("instance".to_string(), Value::String(instance.to_string()));
320 }
321 Value::Object(map)
322 }
323
324 pub fn with_instance(self, instance: impl Into<String>) -> Self {
326 let new_instance = instance.into();
327 let inst = if self.fields.instance.is_empty() || self.fields.instance == "/" {
328 new_instance
329 } else {
330 self.fields.instance.clone()
331 };
332
333 Self {
334 kind: self.kind,
335 fields: ErrorFields {
336 message: self.fields.message,
337 task: self.fields.task,
338 instance: inst,
339 status: self.fields.status,
340 title: self.fields.title,
341 detail: self.fields.detail,
342 original_type: self.fields.original_type,
343 },
344 }
345 }
346}
347
348pub type WorkflowResult<T> = Result<T, WorkflowError>;
350
351pub fn serialize_to_value<T: serde::Serialize>(
354 value: &T,
355 label: &str,
356 task_name: &str,
357) -> WorkflowResult<Value> {
358 serde_json::to_value(value).map_err(|e| {
359 WorkflowError::runtime(
360 format!("failed to serialize {}: {}", label, e),
361 task_name,
362 "",
363 )
364 })
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn test_error_type_validation() {
373 let err = WorkflowError::validation("invalid input", "task1");
374 assert_eq!(err.error_type_short(), "validation");
375 assert!(err.error_type().ends_with("/validation"));
376 assert_eq!(err.task(), "task1");
377 }
378
379 #[test]
380 fn test_error_type_expression() {
381 let err = WorkflowError::expression("bad jq", "task2");
382 assert_eq!(err.error_type_short(), "expression");
383 }
384
385 #[test]
386 fn test_error_type_runtime() {
387 let err = WorkflowError::runtime("something failed", "task3", "/ref");
388 assert_eq!(err.error_type_short(), "runtime");
389 assert_eq!(err.instance(), Some("/ref"));
390 }
391
392 #[test]
393 fn test_error_type_timeout() {
394 let err = WorkflowError::timeout("timed out", "task4");
395 assert_eq!(err.error_type_short(), "timeout");
396 assert!(err.instance().is_none());
397 }
398
399 #[test]
400 fn test_error_type_communication() {
401 let err = WorkflowError::communication("connection refused", "task5");
402 assert_eq!(err.error_type_short(), "communication");
403 }
404
405 #[test]
406 fn test_error_with_instance() {
407 let err = WorkflowError::runtime("invalid", "task1", "").with_instance("/ref/task1");
408 assert_eq!(err.error_type_short(), "runtime");
409 assert_eq!(err.instance(), Some("/ref/task1"));
410 }
411
412 #[test]
413 fn test_error_with_instance_preserves_type() {
414 let err = WorkflowError::timeout("timed out", "task1").with_instance("/ref/task1");
415 assert_eq!(err.error_type_short(), "timeout");
416 assert_eq!(err.instance(), Some("/ref/task1"));
417 }
418
419 #[test]
420 fn test_error_task_name() {
421 let err = WorkflowError::timeout("timeout", "myTask");
422 assert_eq!(err.task(), "myTask");
423 }
424
425 #[test]
426 fn test_error_display() {
427 let err = WorkflowError::validation("bad input", "task1");
428 let msg = format!("{}", err);
429 assert!(msg.contains("bad input"));
430 assert!(msg.contains("task1"));
431 }
432
433 #[test]
434 fn test_error_typed_with_status() {
435 let err = WorkflowError::typed(
436 "https://serverlessworkflow.io/spec/1.0.0/errors/transient",
437 "Something went wrong".to_string(),
438 "testTask".to_string(),
439 "/do/0/testTask".to_string(),
440 Some(Value::from(503)),
441 Some("Transient Error".to_string()),
442 );
443 assert_eq!(err.error_type_short(), "transient");
444 assert_eq!(err.status(), Some(&Value::from(503)));
445 assert_eq!(err.title(), Some("Transient Error"));
446 assert_eq!(err.detail(), Some("Something went wrong"));
447 }
448
449 #[test]
450 fn test_error_to_value() {
451 let err = WorkflowError::typed(
452 "https://serverlessworkflow.io/spec/1.0.0/errors/authentication",
453 "Auth failed".to_string(),
454 "authTask".to_string(),
455 "".to_string(),
456 Some(Value::from(401)),
457 Some("Auth Error".to_string()),
458 );
459 let val = err.to_value();
460 assert_eq!(
461 val["type"],
462 "https://serverlessworkflow.io/spec/1.0.0/errors/authentication"
463 );
464 assert_eq!(val["status"], 401);
465 assert_eq!(val["title"], "Auth Error");
466 assert_eq!(val["details"], "Auth failed");
467 }
468
469 #[test]
470 fn test_error_kind() {
471 let err = WorkflowError::timeout("timed out", "task1");
472 assert_eq!(err.kind(), ErrorKind::Timeout);
473 }
474}