use std::fmt;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum WorkflowErrorCode {
FormatUnsupported,
StructureIncomplete,
UnreachableNode,
CannotTerminate,
PluginUnavailable,
DefinitionConflict,
NoMatchingTransition,
GuardTypeError,
TransitionPersistFailed,
GuardSideEffect,
InstanceNotFound,
GuardEvalFailed,
OptimisticLockConflict,
NoCandidates,
WithdrawNotFirstNode,
UnauthorizedHandle,
InstanceNotHandleable,
TaskNotHandleable,
AddSignTargetInvalid,
CapabilityNotFound,
CapabilityTimeout,
CandidateFormatError,
PluginOutputSchemaFailed,
InstanceSuspended,
NotAdmin,
IllegalStatusTransition,
DefinitionNotFound,
VersionNotFound,
}
impl WorkflowErrorCode {
pub fn as_code(&self) -> &'static str {
match self {
Self::FormatUnsupported => "WF_001",
Self::StructureIncomplete => "WF_002",
Self::UnreachableNode => "WF_003",
Self::CannotTerminate => "WF_004",
Self::PluginUnavailable => "WF_005",
Self::DefinitionConflict => "WF_006",
Self::NoMatchingTransition => "WF_010",
Self::GuardTypeError => "WF_011",
Self::TransitionPersistFailed => "WF_012",
Self::GuardSideEffect => "WF_013",
Self::InstanceNotFound => "WF_014",
Self::GuardEvalFailed => "WF_015",
Self::OptimisticLockConflict => "WF_016",
Self::NoCandidates => "WF_020",
Self::WithdrawNotFirstNode => "WF_021",
Self::UnauthorizedHandle => "WF_022",
Self::InstanceNotHandleable => "WF_023",
Self::TaskNotHandleable => "WF_024",
Self::AddSignTargetInvalid => "WF_026",
Self::CapabilityNotFound => "WF_030",
Self::CapabilityTimeout => "WF_031",
Self::CandidateFormatError => "WF_032",
Self::PluginOutputSchemaFailed => "WF_033",
Self::InstanceSuspended => "WF_040",
Self::NotAdmin => "WF_041",
Self::IllegalStatusTransition => "WF_042",
Self::DefinitionNotFound => "WF_050",
Self::VersionNotFound => "WF_051",
}
}
pub fn http_status(&self) -> u16 {
match self {
Self::InstanceNotFound | Self::DefinitionNotFound | Self::VersionNotFound => 404,
Self::UnauthorizedHandle | Self::NotAdmin => 403,
Self::DefinitionConflict
| Self::OptimisticLockConflict
| Self::IllegalStatusTransition
| Self::InstanceNotHandleable
| Self::TaskNotHandleable
| Self::WithdrawNotFirstNode
| Self::AddSignTargetInvalid
| Self::InstanceSuspended => 409,
_ => 400,
}
}
}
impl fmt::Display for WorkflowErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_code())
}
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub struct WorkflowError {
pub code: WorkflowErrorCode,
pub message: String,
pub details: serde_json::Value,
}
impl fmt::Display for WorkflowError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
impl WorkflowError {
pub fn new(code: WorkflowErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
details: serde_json::Value::Null,
}
}
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = details;
self
}
pub fn with_field(
code: WorkflowErrorCode,
message: impl Into<String>,
field: &str,
value: &str,
) -> Self {
Self {
code,
message: message.into(),
details: serde_json::json!({ field: value }),
}
}
}
pub type WorkflowResult<T> = Result<T, WorkflowError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_code_display_format() {
assert_eq!(WorkflowErrorCode::FormatUnsupported.to_string(), "WF_001");
assert_eq!(
WorkflowErrorCode::NoMatchingTransition.to_string(),
"WF_010"
);
assert_eq!(WorkflowErrorCode::NoCandidates.to_string(), "WF_020");
assert_eq!(WorkflowErrorCode::CapabilityNotFound.to_string(), "WF_030");
assert_eq!(WorkflowErrorCode::InstanceSuspended.to_string(), "WF_040");
assert_eq!(WorkflowErrorCode::DefinitionNotFound.to_string(), "WF_050");
}
#[test]
fn http_status_mapping() {
assert_eq!(WorkflowErrorCode::InstanceNotFound.http_status(), 404);
assert_eq!(WorkflowErrorCode::DefinitionNotFound.http_status(), 404);
assert_eq!(WorkflowErrorCode::VersionNotFound.http_status(), 404);
assert_eq!(WorkflowErrorCode::UnauthorizedHandle.http_status(), 403);
assert_eq!(WorkflowErrorCode::NotAdmin.http_status(), 403);
assert_eq!(WorkflowErrorCode::DefinitionConflict.http_status(), 409);
assert_eq!(WorkflowErrorCode::OptimisticLockConflict.http_status(), 409);
assert_eq!(
WorkflowErrorCode::IllegalStatusTransition.http_status(),
409
);
assert_eq!(WorkflowErrorCode::InstanceNotHandleable.http_status(), 409);
assert_eq!(WorkflowErrorCode::TaskNotHandleable.http_status(), 409);
assert_eq!(WorkflowErrorCode::WithdrawNotFirstNode.http_status(), 409);
assert_eq!(WorkflowErrorCode::AddSignTargetInvalid.http_status(), 409);
assert_eq!(WorkflowErrorCode::InstanceSuspended.http_status(), 409);
assert_eq!(WorkflowErrorCode::FormatUnsupported.http_status(), 400);
assert_eq!(WorkflowErrorCode::NoMatchingTransition.http_status(), 400);
assert_eq!(WorkflowErrorCode::GuardSideEffect.http_status(), 400);
}
#[test]
fn error_construct_and_display() {
let err = WorkflowError::new(WorkflowErrorCode::NoMatchingTransition, "无匹配迁移");
assert_eq!(err.code, WorkflowErrorCode::NoMatchingTransition);
assert_eq!(err.message, "无匹配迁移");
assert_eq!(err.details, serde_json::Value::Null);
assert_eq!(format!("{}", err), "WF_010: 无匹配迁移");
let err2 = WorkflowError::with_field(
WorkflowErrorCode::StructureIncomplete,
"缺少 start 节点",
"missing",
"start",
);
assert_eq!(err2.details["missing"], "start");
}
#[test]
fn error_with_details() {
let err = WorkflowError::new(WorkflowErrorCode::PluginUnavailable, "插件未启用")
.with_details(serde_json::json!({"plugin": "crm", "node_id": "n1"}));
assert_eq!(err.details["plugin"], "crm");
assert_eq!(err.details["node_id"], "n1");
}
#[test]
fn error_code_count() {
let all = [
WorkflowErrorCode::FormatUnsupported,
WorkflowErrorCode::StructureIncomplete,
WorkflowErrorCode::UnreachableNode,
WorkflowErrorCode::CannotTerminate,
WorkflowErrorCode::PluginUnavailable,
WorkflowErrorCode::DefinitionConflict,
WorkflowErrorCode::NoMatchingTransition,
WorkflowErrorCode::GuardTypeError,
WorkflowErrorCode::TransitionPersistFailed,
WorkflowErrorCode::GuardSideEffect,
WorkflowErrorCode::InstanceNotFound,
WorkflowErrorCode::GuardEvalFailed,
WorkflowErrorCode::OptimisticLockConflict,
WorkflowErrorCode::NoCandidates,
WorkflowErrorCode::WithdrawNotFirstNode,
WorkflowErrorCode::UnauthorizedHandle,
WorkflowErrorCode::InstanceNotHandleable,
WorkflowErrorCode::TaskNotHandleable,
WorkflowErrorCode::AddSignTargetInvalid,
WorkflowErrorCode::CapabilityNotFound,
WorkflowErrorCode::CapabilityTimeout,
WorkflowErrorCode::CandidateFormatError,
WorkflowErrorCode::PluginOutputSchemaFailed,
WorkflowErrorCode::InstanceSuspended,
WorkflowErrorCode::NotAdmin,
WorkflowErrorCode::IllegalStatusTransition,
WorkflowErrorCode::DefinitionNotFound,
WorkflowErrorCode::VersionNotFound,
];
let codes: std::collections::HashSet<_> = all.iter().map(|c| c.as_code()).collect();
assert_eq!(codes.len(), 28, "28 个唯一错误码");
}
}