Skip to main content

swf_core/models/
error.rs

1use serde::{Deserialize, Serialize};
2
3string_constants! {
4    /// Standard error type URIs based on the Serverless Workflow specification
5    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/// Represents the type of an error, which can be either a URI template or a runtime expression.
18///
19/// Runtime expressions are detected by the `${` prefix in the string value.
20/// Since serde(untagged) cannot distinguish between the two forms at deserialization,
21/// this is stored as a simple newtype wrapper rather than an enum.
22#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ErrorType(String);
25
26impl ErrorType {
27    /// Creates a new URI template error type
28    pub fn uri_template(template: &str) -> Self {
29        ErrorType(template.to_string())
30    }
31
32    /// Creates a new runtime expression error type
33    pub fn runtime_expression(expression: &str) -> Self {
34        ErrorType(expression.to_string())
35    }
36
37    /// Checks if this is a runtime expression (starts with '${')
38    pub fn is_runtime_expression(&self) -> bool {
39        self.0.starts_with("${")
40    }
41
42    /// Gets the string value
43    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/// Represents the definition an error to raise, following RFC 7807 (Problem Details)
61#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
62pub struct ErrorDefinition {
63    /// Gets/sets an uri that reference the type of the described error
64    #[serde(rename = "type")]
65    pub type_: ErrorType,
66
67    /// Gets/sets a short, human-readable summary of the error type.
68    /// It SHOULD NOT change from occurrence to occurrence of the error, except for purposes of localization.
69    /// Can be a runtime expression (${...}) or a literal string.
70    #[serde(skip_serializing_if = "Option::is_none", default)]
71    pub title: Option<String>,
72
73    /// Gets/sets the status code generated by the origin for this occurrence of the error (RFC 7807).
74    /// Must be an integer.
75    pub status: i32,
76
77    /// Gets/sets a human-readable explanation specific to this occurrence of the error.
78    /// Can be a runtime expression (${...}) or a literal string.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub detail: Option<String>,
81
82    /// Gets/sets a URI reference that identifies the specific occurrence of the error.
83    /// Can be a JSON Pointer or a runtime expression.
84    #[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    /// Initializes a new ErrorDefinition
103    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    /// A error definition or a reference to one
179    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    // Additional tests matching Go SDK patterns
315
316    #[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        // URI-based error types should not be detected as runtime expressions
358        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        // Runtime expression types should be detected
363        let expr_type = ErrorType::runtime_expression("${ .errorType }");
364        assert!(expr_type.is_runtime_expression());
365
366        // A URI that starts with ${ should be detected as runtime expression
367        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        // Error with runtime expression as type
374        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        // Test all standard error factory methods produce correct types
386        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        // Matches Go SDK pattern where title is optional (omitempty)
422        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}