Skip to main content

swf_core/models/
error.rs

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