use serde::{Deserialize, Serialize};
use serde_json::Value;
string_constants! {
ErrorTypes {
CONFIGURATION => "https://serverlessworkflow.io/spec/1.0.0/errors/configuration",
VALIDATION => "https://serverlessworkflow.io/spec/1.0.0/errors/validation",
EXPRESSION => "https://serverlessworkflow.io/spec/1.0.0/errors/expression",
AUTHENTICATION => "https://serverlessworkflow.io/spec/1.0.0/errors/authentication",
AUTHORIZATION => "https://serverlessworkflow.io/spec/1.0.0/errors/authorization",
TIMEOUT => "https://serverlessworkflow.io/spec/1.0.0/errors/timeout",
COMMUNICATION => "https://serverlessworkflow.io/spec/1.0.0/errors/communication",
RUNTIME => "https://serverlessworkflow.io/spec/1.0.0/errors/runtime",
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ErrorType(String);
impl ErrorType {
pub fn uri_template(template: &str) -> Self {
ErrorType(template.to_string())
}
pub fn runtime_expression(expression: &str) -> Self {
ErrorType(expression.to_string())
}
pub fn is_runtime_expression(&self) -> bool {
self.0.starts_with("${")
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for ErrorType {
fn from(s: String) -> Self {
ErrorType(s)
}
}
impl From<&str> for ErrorType {
fn from(s: &str) -> Self {
ErrorType(s.to_string())
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct ErrorDefinition {
#[serde(rename = "type")]
pub type_: ErrorType,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub title: Option<String>,
pub status: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
}
macro_rules! define_error_type {
($factory:ident, $is:ident, $const:ident, $title:literal, $status:expr) => {
#[doc = concat!("Creates a new ", stringify!($factory))]
pub fn $factory(detail: Option<String>, instance: Option<String>) -> Self {
Self::new(
ErrorTypes::$const,
$title,
serde_json::json!($status),
detail,
instance,
)
}
#[doc = concat!("Checks if this error is a ", stringify!($factory))]
pub fn $is(&self) -> bool {
self.type_.as_str() == ErrorTypes::$const
}
};
}
impl ErrorDefinition {
pub fn new(
type_: &str,
title: &str,
status: Value,
detail: Option<String>,
instance: Option<String>,
) -> Self {
Self {
type_: ErrorType::uri_template(type_),
title: Some(title.to_string()),
status,
detail,
instance,
}
}
define_error_type!(
configuration_error,
is_configuration_error,
CONFIGURATION,
"Configuration Error",
400
);
define_error_type!(
validation_error,
is_validation_error,
VALIDATION,
"Validation Error",
400
);
define_error_type!(
expression_error,
is_expression_error,
EXPRESSION,
"Expression Error",
400
);
define_error_type!(
authentication_error,
is_authentication_error,
AUTHENTICATION,
"Authentication Error",
401
);
define_error_type!(
authorization_error,
is_authorization_error,
AUTHORIZATION,
"Authorization Error",
403
);
define_error_type!(
timeout_error,
is_timeout_error,
TIMEOUT,
"Timeout Error",
408
);
define_error_type!(
communication_error,
is_communication_error,
COMMUNICATION,
"Communication Error",
500
);
define_error_type!(
runtime_error,
is_runtime_error,
RUNTIME,
"Runtime Error",
500
);
}
define_one_of_or_reference!(
OneOfErrorDefinitionOrReference, Error(ErrorDefinition)
);
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_error_definition_new() {
let err = ErrorDefinition::new(
ErrorTypes::RUNTIME,
"Runtime Error",
json!(500),
Some("Something went wrong".to_string()),
Some("/task/1".to_string()),
);
assert_eq!(err.type_.as_str(), ErrorTypes::RUNTIME);
assert_eq!(err.title, Some("Runtime Error".to_string()));
assert_eq!(err.status, json!(500));
assert_eq!(err.detail, Some("Something went wrong".to_string()));
assert_eq!(err.instance, Some("/task/1".to_string()));
}
#[test]
fn test_error_type_check_methods() {
let err = ErrorDefinition::validation_error(None, None);
assert!(err.is_validation_error());
assert!(!err.is_runtime_error());
let err = ErrorDefinition::runtime_error(None, None);
assert!(err.is_runtime_error());
assert!(!err.is_communication_error());
let err = ErrorDefinition::authentication_error(None, None);
assert!(err.is_authentication_error());
let err = ErrorDefinition::authorization_error(None, None);
assert!(err.is_authorization_error());
let err = ErrorDefinition::timeout_error(None, None);
assert!(err.is_timeout_error());
let err = ErrorDefinition::communication_error(None, None);
assert!(err.is_communication_error());
let err = ErrorDefinition::configuration_error(None, None);
assert!(err.is_configuration_error());
let err = ErrorDefinition::expression_error(None, None);
assert!(err.is_expression_error());
}
#[test]
fn test_error_type_enum() {
let uri =
ErrorType::uri_template("https://serverlessworkflow.io/spec/1.0.0/errors/runtime");
assert_eq!(
uri.as_str(),
"https://serverlessworkflow.io/spec/1.0.0/errors/runtime"
);
assert!(!uri.is_runtime_expression());
let expr = ErrorType::runtime_expression("${ .errorType }");
assert_eq!(expr.as_str(), "${ .errorType }");
assert!(expr.is_runtime_expression());
}
#[test]
fn test_error_definition_serialize() {
let err = ErrorDefinition::new(
ErrorTypes::COMMUNICATION,
"Communication Error",
json!(500),
None,
None,
);
let json_str = serde_json::to_string(&err).unwrap();
assert!(json_str.contains(
"\"type\":\"https://serverlessworkflow.io/spec/1.0.0/errors/communication\""
));
assert!(json_str.contains("\"title\":\"Communication Error\""));
assert!(json_str.contains("\"status\":500"));
assert!(!json_str.contains("detail"));
assert!(!json_str.contains("instance"));
}
#[test]
fn test_error_definition_deserialize() {
let json = r#"{
"type": "https://serverlessworkflow.io/spec/1.0.0/errors/runtime",
"title": "Runtime Error",
"status": 500,
"detail": "Something failed",
"instance": "/task/step1"
}"#;
let err: ErrorDefinition = serde_json::from_str(json).unwrap();
assert_eq!(
err.type_.as_str(),
"https://serverlessworkflow.io/spec/1.0.0/errors/runtime"
);
assert_eq!(err.title, Some("Runtime Error".to_string()));
assert_eq!(err.detail, Some("Something failed".to_string()));
}
#[test]
fn test_oneof_error_reference_deserialize() {
let json = r#""someErrorRef""#;
let oneof: OneOfErrorDefinitionOrReference = serde_json::from_str(json).unwrap();
match oneof {
OneOfErrorDefinitionOrReference::Reference(name) => {
assert_eq!(name, "someErrorRef");
}
_ => panic!("Expected Reference variant"),
}
}
#[test]
fn test_oneof_error_inline_deserialize() {
let json = r#"{
"type": "https://serverlessworkflow.io/spec/1.0.0/errors/timeout",
"title": "Timeout Error",
"status": 408
}"#;
let oneof: OneOfErrorDefinitionOrReference = serde_json::from_str(json).unwrap();
match oneof {
OneOfErrorDefinitionOrReference::Error(err) => {
assert_eq!(
err.type_.as_str(),
"https://serverlessworkflow.io/spec/1.0.0/errors/timeout"
);
}
_ => panic!("Expected Error variant"),
}
}
#[test]
fn test_error_definition_roundtrip() {
let json = r#"{
"type": "https://serverlessworkflow.io/spec/1.0.0/errors/communication",
"title": "Communication Error",
"status": 500,
"detail": "Connection refused",
"instance": "/task/step2"
}"#;
let err: ErrorDefinition = serde_json::from_str(json).unwrap();
let serialized = serde_json::to_string(&err).unwrap();
let deserialized: ErrorDefinition = serde_json::from_str(&serialized).unwrap();
assert_eq!(err, deserialized);
}
#[test]
fn test_oneof_error_reference_roundtrip() {
let oneof = OneOfErrorDefinitionOrReference::Reference("myErrorRef".to_string());
let serialized = serde_json::to_string(&oneof).unwrap();
assert_eq!(serialized, r#""myErrorRef""#);
let deserialized: OneOfErrorDefinitionOrReference =
serde_json::from_str(&serialized).unwrap();
assert_eq!(oneof, deserialized);
}
#[test]
fn test_oneof_error_inline_roundtrip() {
let json = r#"{
"type": "https://serverlessworkflow.io/spec/1.0.0/errors/authentication",
"title": "Auth Error",
"status": 401
}"#;
let oneof: OneOfErrorDefinitionOrReference = serde_json::from_str(json).unwrap();
let serialized = serde_json::to_string(&oneof).unwrap();
let deserialized: OneOfErrorDefinitionOrReference =
serde_json::from_str(&serialized).unwrap();
assert_eq!(oneof, deserialized);
}
#[test]
fn test_error_type_runtime_expression_detection() {
let uri_type =
ErrorType::uri_template("https://serverlessworkflow.io/spec/1.0.0/errors/runtime");
assert!(!uri_type.is_runtime_expression());
let expr_type = ErrorType::runtime_expression("${ .errorType }");
assert!(expr_type.is_runtime_expression());
let uri_with_expr = ErrorType::uri_template("${ .dynamicError }");
assert!(uri_with_expr.is_runtime_expression());
}
#[test]
fn test_error_definition_with_runtime_type() {
let json = r#"{
"type": "${ .error.type }",
"title": "Dynamic Error",
"status": 500
}"#;
let err: ErrorDefinition = serde_json::from_str(json).unwrap();
assert!(err.type_.is_runtime_expression());
}
#[test]
fn test_standard_error_factory_methods() {
let config = ErrorDefinition::configuration_error(Some("bad config".to_string()), None);
assert!(config.is_configuration_error());
assert_eq!(config.status, json!(400));
let validation = ErrorDefinition::validation_error(None, None);
assert!(validation.is_validation_error());
assert_eq!(validation.status, json!(400));
let expr = ErrorDefinition::expression_error(None, None);
assert!(expr.is_expression_error());
assert_eq!(expr.status, json!(400));
let authn = ErrorDefinition::authentication_error(None, None);
assert!(authn.is_authentication_error());
assert_eq!(authn.status, json!(401));
let authz = ErrorDefinition::authorization_error(None, None);
assert!(authz.is_authorization_error());
assert_eq!(authz.status, json!(403));
let timeout = ErrorDefinition::timeout_error(None, None);
assert!(timeout.is_timeout_error());
assert_eq!(timeout.status, json!(408));
let comm = ErrorDefinition::communication_error(None, None);
assert!(comm.is_communication_error());
assert_eq!(comm.status, json!(500));
let runtime = ErrorDefinition::runtime_error(None, None);
assert!(runtime.is_runtime_error());
assert_eq!(runtime.status, json!(500));
}
#[test]
fn test_error_definition_without_optional_title() {
let json = r#"{
"type": "https://serverlessworkflow.io/spec/1.0.0/errors/timeout",
"status": 408,
"detail": "Request took too long"
}"#;
let err: ErrorDefinition = serde_json::from_str(json).unwrap();
assert_eq!(
err.type_.as_str(),
"https://serverlessworkflow.io/spec/1.0.0/errors/timeout"
);
assert_eq!(err.title, None);
assert_eq!(err.status, json!(408));
assert_eq!(err.detail, Some("Request took too long".to_string()));
}
#[test]
fn test_error_definition_serialize_skips_none_title() {
let err = ErrorDefinition {
type_: ErrorType::uri_template(
"https://serverlessworkflow.io/spec/1.0.0/errors/timeout",
),
title: None,
status: json!(408),
detail: Some("Timed out".to_string()),
instance: None,
};
let json_str = serde_json::to_string(&err).unwrap();
assert!(!json_str.contains("title"));
assert!(json_str.contains("\"detail\":\"Timed out\""));
}
}