1use serde::{Deserialize, Serialize};
2
3string_constants! {
4 ErrorTypes {
6 CONFIGURATION => "https://serverlessworkflow.io/spec/1.0.0/errors/configuration",
7 VALIDATION => "https://serverlessworkflow.io/spec/1.0.0/errors/validation",
8 EXPRESSION => "https://serverlessworkflow.io/spec/1.0.0/errors/expression",
9 AUTHENTICATION => "https://serverlessworkflow.io/spec/1.0.0/errors/authentication",
10 AUTHORIZATION => "https://serverlessworkflow.io/spec/1.0.0/errors/authorization",
11 TIMEOUT => "https://serverlessworkflow.io/spec/1.0.0/errors/timeout",
12 COMMUNICATION => "https://serverlessworkflow.io/spec/1.0.0/errors/communication",
13 RUNTIME => "https://serverlessworkflow.io/spec/1.0.0/errors/runtime",
14 }
15}
16
17#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ErrorType(String);
25
26impl ErrorType {
27 pub fn uri_template(template: &str) -> Self {
29 ErrorType(template.to_string())
30 }
31
32 pub fn runtime_expression(expression: &str) -> Self {
34 ErrorType(expression.to_string())
35 }
36
37 pub fn is_runtime_expression(&self) -> bool {
39 self.0.starts_with("${")
40 }
41
42 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46}
47
48impl From<String> for ErrorType {
49 fn from(s: String) -> Self {
50 ErrorType(s)
51 }
52}
53
54impl From<&str> for ErrorType {
55 fn from(s: &str) -> Self {
56 ErrorType(s.to_string())
57 }
58}
59
60#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
62pub struct ErrorDefinition {
63 #[serde(rename = "type")]
65 pub type_: ErrorType,
66
67 #[serde(skip_serializing_if = "Option::is_none", default)]
71 pub title: Option<String>,
72
73 pub status: i32,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
80 pub detail: Option<String>,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
85 pub instance: Option<String>,
86}
87macro_rules! define_error_type {
88 ($factory:ident, $is:ident, $const:ident, $title:literal, $status:expr) => {
89 #[doc = concat!("Creates a new ", stringify!($factory))]
90 pub fn $factory(detail: Option<String>, instance: Option<String>) -> Self {
91 Self::new(ErrorTypes::$const, $title, $status, detail, instance)
92 }
93
94 #[doc = concat!("Checks if this error is a ", stringify!($factory))]
95 pub fn $is(&self) -> bool {
96 self.type_.as_str() == ErrorTypes::$const
97 }
98 };
99}
100
101impl ErrorDefinition {
102 pub fn new(
104 type_: &str,
105 title: &str,
106 status: i32,
107 detail: Option<String>,
108 instance: Option<String>,
109 ) -> Self {
110 Self {
111 type_: ErrorType::uri_template(type_),
112 title: Some(title.to_string()),
113 status,
114 detail,
115 instance,
116 }
117 }
118
119 define_error_type!(
120 configuration_error,
121 is_configuration_error,
122 CONFIGURATION,
123 "Configuration Error",
124 400
125 );
126 define_error_type!(
127 validation_error,
128 is_validation_error,
129 VALIDATION,
130 "Validation Error",
131 400
132 );
133 define_error_type!(
134 expression_error,
135 is_expression_error,
136 EXPRESSION,
137 "Expression Error",
138 400
139 );
140 define_error_type!(
141 authentication_error,
142 is_authentication_error,
143 AUTHENTICATION,
144 "Authentication Error",
145 401
146 );
147 define_error_type!(
148 authorization_error,
149 is_authorization_error,
150 AUTHORIZATION,
151 "Authorization Error",
152 403
153 );
154 define_error_type!(
155 timeout_error,
156 is_timeout_error,
157 TIMEOUT,
158 "Timeout Error",
159 408
160 );
161 define_error_type!(
162 communication_error,
163 is_communication_error,
164 COMMUNICATION,
165 "Communication Error",
166 500
167 );
168 define_error_type!(
169 runtime_error,
170 is_runtime_error,
171 RUNTIME,
172 "Runtime Error",
173 500
174 );
175}
176
177define_one_of_or_reference!(
178 OneOfErrorDefinitionOrReference, Error(ErrorDefinition)
180);
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_error_definition_new() {
188 let err = ErrorDefinition::new(
189 ErrorTypes::RUNTIME,
190 "Runtime Error",
191 500,
192 Some("Something went wrong".to_string()),
193 Some("/task/1".to_string()),
194 );
195 assert_eq!(err.type_.as_str(), ErrorTypes::RUNTIME);
196 assert_eq!(err.title, Some("Runtime Error".to_string()));
197 assert_eq!(err.status, 500);
198 assert_eq!(err.detail, Some("Something went wrong".to_string()));
199 assert_eq!(err.instance, Some("/task/1".to_string()));
200 }
201
202 #[test]
203 fn test_error_type_check_methods() {
204 let err = ErrorDefinition::validation_error(None, None);
205 assert!(err.is_validation_error());
206 assert!(!err.is_runtime_error());
207
208 let err = ErrorDefinition::runtime_error(None, None);
209 assert!(err.is_runtime_error());
210 assert!(!err.is_communication_error());
211
212 let err = ErrorDefinition::authentication_error(None, None);
213 assert!(err.is_authentication_error());
214
215 let err = ErrorDefinition::authorization_error(None, None);
216 assert!(err.is_authorization_error());
217
218 let err = ErrorDefinition::timeout_error(None, None);
219 assert!(err.is_timeout_error());
220
221 let err = ErrorDefinition::communication_error(None, None);
222 assert!(err.is_communication_error());
223
224 let err = ErrorDefinition::configuration_error(None, None);
225 assert!(err.is_configuration_error());
226
227 let err = ErrorDefinition::expression_error(None, None);
228 assert!(err.is_expression_error());
229 }
230
231 #[test]
232 fn test_error_type_enum() {
233 let uri =
234 ErrorType::uri_template("https://serverlessworkflow.io/spec/1.0.0/errors/runtime");
235 assert_eq!(
236 uri.as_str(),
237 "https://serverlessworkflow.io/spec/1.0.0/errors/runtime"
238 );
239 assert!(!uri.is_runtime_expression());
240
241 let expr = ErrorType::runtime_expression("${ .errorType }");
242 assert_eq!(expr.as_str(), "${ .errorType }");
243 assert!(expr.is_runtime_expression());
244 }
245
246 #[test]
247 fn test_error_definition_serialize() {
248 let err = ErrorDefinition::new(
249 ErrorTypes::COMMUNICATION,
250 "Communication Error",
251 500,
252 None,
253 None,
254 );
255 let json_str = serde_json::to_string(&err).unwrap();
256 assert!(json_str.contains(
257 "\"type\":\"https://serverlessworkflow.io/spec/1.0.0/errors/communication\""
258 ));
259 assert!(json_str.contains("\"title\":\"Communication Error\""));
260 assert!(json_str.contains("\"status\":500"));
261 assert!(!json_str.contains("detail"));
262 assert!(!json_str.contains("instance"));
263 }
264
265 #[test]
266 fn test_error_definition_deserialize() {
267 let json = r#"{
268 "type": "https://serverlessworkflow.io/spec/1.0.0/errors/runtime",
269 "title": "Runtime Error",
270 "status": 500,
271 "detail": "Something failed",
272 "instance": "/task/step1"
273 }"#;
274 let err: ErrorDefinition = serde_json::from_str(json).unwrap();
275 assert_eq!(
276 err.type_.as_str(),
277 "https://serverlessworkflow.io/spec/1.0.0/errors/runtime"
278 );
279 assert_eq!(err.title, Some("Runtime Error".to_string()));
280 assert_eq!(err.detail, Some("Something failed".to_string()));
281 }
282
283 #[test]
284 fn test_oneof_error_reference_deserialize() {
285 let json = r#""someErrorRef""#;
286 let oneof: OneOfErrorDefinitionOrReference = serde_json::from_str(json).unwrap();
287 match oneof {
288 OneOfErrorDefinitionOrReference::Reference(name) => {
289 assert_eq!(name, "someErrorRef");
290 }
291 _ => panic!("Expected Reference variant"),
292 }
293 }
294
295 #[test]
296 fn test_oneof_error_inline_deserialize() {
297 let json = r#"{
298 "type": "https://serverlessworkflow.io/spec/1.0.0/errors/timeout",
299 "title": "Timeout Error",
300 "status": 408
301 }"#;
302 let oneof: OneOfErrorDefinitionOrReference = serde_json::from_str(json).unwrap();
303 match oneof {
304 OneOfErrorDefinitionOrReference::Error(err) => {
305 assert_eq!(
306 err.type_.as_str(),
307 "https://serverlessworkflow.io/spec/1.0.0/errors/timeout"
308 );
309 }
310 _ => panic!("Expected Error variant"),
311 }
312 }
313
314 #[test]
317 fn test_error_definition_roundtrip() {
318 let json = r#"{
319 "type": "https://serverlessworkflow.io/spec/1.0.0/errors/communication",
320 "title": "Communication Error",
321 "status": 500,
322 "detail": "Connection refused",
323 "instance": "/task/step2"
324 }"#;
325 let err: ErrorDefinition = serde_json::from_str(json).unwrap();
326 let serialized = serde_json::to_string(&err).unwrap();
327 let deserialized: ErrorDefinition = serde_json::from_str(&serialized).unwrap();
328 assert_eq!(err, deserialized);
329 }
330
331 #[test]
332 fn test_oneof_error_reference_roundtrip() {
333 let oneof = OneOfErrorDefinitionOrReference::Reference("myErrorRef".to_string());
334 let serialized = serde_json::to_string(&oneof).unwrap();
335 assert_eq!(serialized, r#""myErrorRef""#);
336 let deserialized: OneOfErrorDefinitionOrReference =
337 serde_json::from_str(&serialized).unwrap();
338 assert_eq!(oneof, deserialized);
339 }
340
341 #[test]
342 fn test_oneof_error_inline_roundtrip() {
343 let json = r#"{
344 "type": "https://serverlessworkflow.io/spec/1.0.0/errors/authentication",
345 "title": "Auth Error",
346 "status": 401
347 }"#;
348 let oneof: OneOfErrorDefinitionOrReference = serde_json::from_str(json).unwrap();
349 let serialized = serde_json::to_string(&oneof).unwrap();
350 let deserialized: OneOfErrorDefinitionOrReference =
351 serde_json::from_str(&serialized).unwrap();
352 assert_eq!(oneof, deserialized);
353 }
354
355 #[test]
356 fn test_error_type_runtime_expression_detection() {
357 let uri_type =
359 ErrorType::uri_template("https://serverlessworkflow.io/spec/1.0.0/errors/runtime");
360 assert!(!uri_type.is_runtime_expression());
361
362 let expr_type = ErrorType::runtime_expression("${ .errorType }");
364 assert!(expr_type.is_runtime_expression());
365
366 let uri_with_expr = ErrorType::uri_template("${ .dynamicError }");
368 assert!(uri_with_expr.is_runtime_expression());
369 }
370
371 #[test]
372 fn test_error_definition_with_runtime_type() {
373 let json = r#"{
375 "type": "${ .error.type }",
376 "title": "Dynamic Error",
377 "status": 500
378 }"#;
379 let err: ErrorDefinition = serde_json::from_str(json).unwrap();
380 assert!(err.type_.is_runtime_expression());
381 }
382
383 #[test]
384 fn test_standard_error_factory_methods() {
385 let config = ErrorDefinition::configuration_error(Some("bad config".to_string()), None);
387 assert!(config.is_configuration_error());
388 assert_eq!(config.status, 400);
389
390 let validation = ErrorDefinition::validation_error(None, None);
391 assert!(validation.is_validation_error());
392 assert_eq!(validation.status, 400);
393
394 let expr = ErrorDefinition::expression_error(None, None);
395 assert!(expr.is_expression_error());
396 assert_eq!(expr.status, 400);
397
398 let authn = ErrorDefinition::authentication_error(None, None);
399 assert!(authn.is_authentication_error());
400 assert_eq!(authn.status, 401);
401
402 let authz = ErrorDefinition::authorization_error(None, None);
403 assert!(authz.is_authorization_error());
404 assert_eq!(authz.status, 403);
405
406 let timeout = ErrorDefinition::timeout_error(None, None);
407 assert!(timeout.is_timeout_error());
408 assert_eq!(timeout.status, 408);
409
410 let comm = ErrorDefinition::communication_error(None, None);
411 assert!(comm.is_communication_error());
412 assert_eq!(comm.status, 500);
413
414 let runtime = ErrorDefinition::runtime_error(None, None);
415 assert!(runtime.is_runtime_error());
416 assert_eq!(runtime.status, 500);
417 }
418
419 #[test]
420 fn test_error_definition_without_optional_title() {
421 let json = r#"{
423 "type": "https://serverlessworkflow.io/spec/1.0.0/errors/timeout",
424 "status": 408,
425 "detail": "Request took too long"
426 }"#;
427 let err: ErrorDefinition = serde_json::from_str(json).unwrap();
428 assert_eq!(
429 err.type_.as_str(),
430 "https://serverlessworkflow.io/spec/1.0.0/errors/timeout"
431 );
432 assert_eq!(err.title, None);
433 assert_eq!(err.status, 408);
434 assert_eq!(err.detail, Some("Request took too long".to_string()));
435 }
436
437 #[test]
438 fn test_error_definition_serialize_skips_none_title() {
439 let err = ErrorDefinition {
440 type_: ErrorType::uri_template(
441 "https://serverlessworkflow.io/spec/1.0.0/errors/timeout",
442 ),
443 title: None,
444 status: 408,
445 detail: Some("Timed out".to_string()),
446 instance: None,
447 };
448 let json_str = serde_json::to_string(&err).unwrap();
449 assert!(!json_str.contains("title"));
450 assert!(json_str.contains("\"detail\":\"Timed out\""));
451 }
452}