Skip to main content

sz_rust_workflow/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9/// 工作流引擎错误码,覆盖 `WF_001`~`WF_051`,按功能分区。
10///
11/// | 区段 | 功能 | 数量 |
12/// |------|------|------|
13/// | WF_001-009 | 定义加载 | 5 |
14/// | WF_010-019 | 状态机 | 5 |
15/// | WF_020-029 | 审批流 | 5 |
16/// | WF_030-039 | 插件节点 | 4 |
17/// | WF_040-049 | 实例管理 | 3 |
18/// | WF_050-099 | 设计器 API | 2 |
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[non_exhaustive]
21pub enum WorkflowErrorCode {
22    // ── 定义加载 WF_001-009 ──
23    /// WF_001:定义格式不支持或解析失败
24    FormatUnsupported,
25    /// WF_002:定义结构不完整(缺必需字段/节点)
26    StructureIncomplete,
27    /// WF_003:存在不可达节点(Warning 级)
28    UnreachableNode,
29    /// WF_004:流程无法终止(无 end 节点或存在无法到达 end 的节点)
30    CannotTerminate,
31    /// WF_005:插件节点引用的插件未启用或命名规范违规
32    PluginUnavailable,
33    /// WF_006:定义冲突(同 flow_key+version 已存在)
34    DefinitionConflict,
35
36    // ── 状态机 WF_010-019 ──
37    /// WF_010:无匹配迁移(当前状态不接受该事件)
38    NoMatchingTransition,
39    /// WF_011:守卫表达式求值类型错误(非布尔)
40    GuardTypeError,
41    /// WF_012:迁移持久化失败
42    TransitionPersistFailed,
43    /// WF_013:守卫表达式含副作用调用
44    GuardSideEffect,
45    /// WF_014:实例不存在
46    InstanceNotFound,
47    /// WF_015:守卫求值失败(引用不存在字段等)
48    GuardEvalFailed,
49    /// WF_016:乐观锁冲突(实例版本号不匹配)
50    OptimisticLockConflict,
51
52    // ── 审批流 WF_020-029 ──
53    /// WF_020:候选人为空集合
54    NoCandidates,
55    /// WF_021:撤回非首个审批节点
56    WithdrawNotFirstNode,
57    /// WF_022:越权办理(actor 不属于候选人集合)
58    UnauthorizedHandle,
59    /// WF_023:实例非 running 状态,不可办理
60    InstanceNotHandleable,
61    /// WF_024:任务非 pending 状态,不可办理
62    TaskNotHandleable,
63    /// WF_026:加签目标非法
64    AddSignTargetInvalid,
65
66    // ── 插件节点 WF_030-039 ──
67    /// WF_030:能力不存在
68    CapabilityNotFound,
69    /// WF_031:能力调用超时
70    CapabilityTimeout,
71    /// WF_032:候选人能力返回格式错误(非数组)
72    CandidateFormatError,
73    /// WF_033:插件节点输出 Schema 校验失败
74    PluginOutputSchemaFailed,
75
76    // ── 实例管理 WF_040-049 ──
77    /// WF_040:实例已挂起,拒绝事件与办理
78    InstanceSuspended,
79    /// WF_041:非管理员无权操作
80    NotAdmin,
81    /// WF_042:实例状态非法转换
82    IllegalStatusTransition,
83
84    // ── 设计器 API WF_050-099 ──
85    /// WF_050:定义不存在(导出/查询时)
86    DefinitionNotFound,
87    /// WF_051:版本不存在(设置生效版本时)
88    VersionNotFound,
89}
90
91impl WorkflowErrorCode {
92    /// 返回 `WF_xxx` 格式的错误码字符串。
93    pub fn as_code(&self) -> &'static str {
94        match self {
95            Self::FormatUnsupported => "WF_001",
96            Self::StructureIncomplete => "WF_002",
97            Self::UnreachableNode => "WF_003",
98            Self::CannotTerminate => "WF_004",
99            Self::PluginUnavailable => "WF_005",
100            Self::DefinitionConflict => "WF_006",
101            Self::NoMatchingTransition => "WF_010",
102            Self::GuardTypeError => "WF_011",
103            Self::TransitionPersistFailed => "WF_012",
104            Self::GuardSideEffect => "WF_013",
105            Self::InstanceNotFound => "WF_014",
106            Self::GuardEvalFailed => "WF_015",
107            Self::OptimisticLockConflict => "WF_016",
108            Self::NoCandidates => "WF_020",
109            Self::WithdrawNotFirstNode => "WF_021",
110            Self::UnauthorizedHandle => "WF_022",
111            Self::InstanceNotHandleable => "WF_023",
112            Self::TaskNotHandleable => "WF_024",
113            Self::AddSignTargetInvalid => "WF_026",
114            Self::CapabilityNotFound => "WF_030",
115            Self::CapabilityTimeout => "WF_031",
116            Self::CandidateFormatError => "WF_032",
117            Self::PluginOutputSchemaFailed => "WF_033",
118            Self::InstanceSuspended => "WF_040",
119            Self::NotAdmin => "WF_041",
120            Self::IllegalStatusTransition => "WF_042",
121            Self::DefinitionNotFound => "WF_050",
122            Self::VersionNotFound => "WF_051",
123        }
124    }
125
126    /// 映射 HTTP 状态码。
127    ///
128    /// | 错误码 | HTTP | 语义 |
129    /// |--------|------|------|
130    /// | WF_014/WF_050/WF_051 | 404 | 资源不存在 |
131    /// | WF_022/WF_041 | 403 | 越权 |
132    /// | WF_006/WF_016/WF_042 | 409 | 冲突 |
133    /// | WF_023/WF_024/WF_021/WF_026 | 409 | 状态冲突 |
134    /// | WF_040 | 409 | 实例挂起 |
135    /// | 其他 | 400 | 请求错误 |
136    pub fn http_status(&self) -> u16 {
137        match self {
138            Self::InstanceNotFound | Self::DefinitionNotFound | Self::VersionNotFound => 404,
139            Self::UnauthorizedHandle | Self::NotAdmin => 403,
140            Self::DefinitionConflict
141            | Self::OptimisticLockConflict
142            | Self::IllegalStatusTransition
143            | Self::InstanceNotHandleable
144            | Self::TaskNotHandleable
145            | Self::WithdrawNotFirstNode
146            | Self::AddSignTargetInvalid
147            | Self::InstanceSuspended => 409,
148            _ => 400,
149        }
150    }
151}
152
153impl fmt::Display for WorkflowErrorCode {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        f.write_str(self.as_code())
156    }
157}
158
159/// 工作流引擎统一错误类型。
160#[derive(Debug, Clone, Error)]
161#[non_exhaustive]
162pub struct WorkflowError {
163    /// 错误码
164    pub code: WorkflowErrorCode,
165    /// 人类可读消息
166    pub message: String,
167    /// 结构化详情(附加上下文,如缺失字段名、节点 ID 等)
168    pub details: serde_json::Value,
169}
170
171impl fmt::Display for WorkflowError {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(f, "{}: {}", self.code, self.message)
174    }
175}
176
177impl WorkflowError {
178    /// 构造新错误。
179    pub fn new(code: WorkflowErrorCode, message: impl Into<String>) -> Self {
180        Self {
181            code,
182            message: message.into(),
183            details: serde_json::Value::Null,
184        }
185    }
186
187    /// 附结构化详情。
188    pub fn with_details(mut self, details: serde_json::Value) -> Self {
189        self.details = details;
190        self
191    }
192
193    /// 便捷构造:附单个字段详情。
194    pub fn with_field(
195        code: WorkflowErrorCode,
196        message: impl Into<String>,
197        field: &str,
198        value: &str,
199    ) -> Self {
200        Self {
201            code,
202            message: message.into(),
203            details: serde_json::json!({ field: value }),
204        }
205    }
206}
207
208/// 工作流引擎统一 Result。
209pub type WorkflowResult<T> = Result<T, WorkflowError>;
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn error_code_display_format() {
217        assert_eq!(WorkflowErrorCode::FormatUnsupported.to_string(), "WF_001");
218        assert_eq!(
219            WorkflowErrorCode::NoMatchingTransition.to_string(),
220            "WF_010"
221        );
222        assert_eq!(WorkflowErrorCode::NoCandidates.to_string(), "WF_020");
223        assert_eq!(WorkflowErrorCode::CapabilityNotFound.to_string(), "WF_030");
224        assert_eq!(WorkflowErrorCode::InstanceSuspended.to_string(), "WF_040");
225        assert_eq!(WorkflowErrorCode::DefinitionNotFound.to_string(), "WF_050");
226    }
227
228    #[test]
229    fn http_status_mapping() {
230        assert_eq!(WorkflowErrorCode::InstanceNotFound.http_status(), 404);
231        assert_eq!(WorkflowErrorCode::DefinitionNotFound.http_status(), 404);
232        assert_eq!(WorkflowErrorCode::VersionNotFound.http_status(), 404);
233        assert_eq!(WorkflowErrorCode::UnauthorizedHandle.http_status(), 403);
234        assert_eq!(WorkflowErrorCode::NotAdmin.http_status(), 403);
235        assert_eq!(WorkflowErrorCode::DefinitionConflict.http_status(), 409);
236        assert_eq!(WorkflowErrorCode::OptimisticLockConflict.http_status(), 409);
237        assert_eq!(
238            WorkflowErrorCode::IllegalStatusTransition.http_status(),
239            409
240        );
241        assert_eq!(WorkflowErrorCode::InstanceNotHandleable.http_status(), 409);
242        assert_eq!(WorkflowErrorCode::TaskNotHandleable.http_status(), 409);
243        assert_eq!(WorkflowErrorCode::WithdrawNotFirstNode.http_status(), 409);
244        assert_eq!(WorkflowErrorCode::AddSignTargetInvalid.http_status(), 409);
245        assert_eq!(WorkflowErrorCode::InstanceSuspended.http_status(), 409);
246        assert_eq!(WorkflowErrorCode::FormatUnsupported.http_status(), 400);
247        assert_eq!(WorkflowErrorCode::NoMatchingTransition.http_status(), 400);
248        assert_eq!(WorkflowErrorCode::GuardSideEffect.http_status(), 400);
249    }
250
251    #[test]
252    fn error_construct_and_display() {
253        let err = WorkflowError::new(WorkflowErrorCode::NoMatchingTransition, "无匹配迁移");
254        assert_eq!(err.code, WorkflowErrorCode::NoMatchingTransition);
255        assert_eq!(err.message, "无匹配迁移");
256        assert_eq!(err.details, serde_json::Value::Null);
257        assert_eq!(format!("{}", err), "WF_010: 无匹配迁移");
258
259        let err2 = WorkflowError::with_field(
260            WorkflowErrorCode::StructureIncomplete,
261            "缺少 start 节点",
262            "missing",
263            "start",
264        );
265        assert_eq!(err2.details["missing"], "start");
266    }
267
268    #[test]
269    fn error_with_details() {
270        let err = WorkflowError::new(WorkflowErrorCode::PluginUnavailable, "插件未启用")
271            .with_details(serde_json::json!({"plugin": "crm", "node_id": "n1"}));
272        assert_eq!(err.details["plugin"], "crm");
273        assert_eq!(err.details["node_id"], "n1");
274    }
275
276    #[test]
277    fn error_code_count() {
278        let all = [
279            WorkflowErrorCode::FormatUnsupported,
280            WorkflowErrorCode::StructureIncomplete,
281            WorkflowErrorCode::UnreachableNode,
282            WorkflowErrorCode::CannotTerminate,
283            WorkflowErrorCode::PluginUnavailable,
284            WorkflowErrorCode::DefinitionConflict,
285            WorkflowErrorCode::NoMatchingTransition,
286            WorkflowErrorCode::GuardTypeError,
287            WorkflowErrorCode::TransitionPersistFailed,
288            WorkflowErrorCode::GuardSideEffect,
289            WorkflowErrorCode::InstanceNotFound,
290            WorkflowErrorCode::GuardEvalFailed,
291            WorkflowErrorCode::OptimisticLockConflict,
292            WorkflowErrorCode::NoCandidates,
293            WorkflowErrorCode::WithdrawNotFirstNode,
294            WorkflowErrorCode::UnauthorizedHandle,
295            WorkflowErrorCode::InstanceNotHandleable,
296            WorkflowErrorCode::TaskNotHandleable,
297            WorkflowErrorCode::AddSignTargetInvalid,
298            WorkflowErrorCode::CapabilityNotFound,
299            WorkflowErrorCode::CapabilityTimeout,
300            WorkflowErrorCode::CandidateFormatError,
301            WorkflowErrorCode::PluginOutputSchemaFailed,
302            WorkflowErrorCode::InstanceSuspended,
303            WorkflowErrorCode::NotAdmin,
304            WorkflowErrorCode::IllegalStatusTransition,
305            WorkflowErrorCode::DefinitionNotFound,
306            WorkflowErrorCode::VersionNotFound,
307        ];
308        let codes: std::collections::HashSet<_> = all.iter().map(|c| c.as_code()).collect();
309        assert_eq!(codes.len(), 28, "28 个唯一错误码");
310    }
311}