1use std::fmt;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[non_exhaustive]
21pub enum WorkflowErrorCode {
22 FormatUnsupported,
25 StructureIncomplete,
27 UnreachableNode,
29 CannotTerminate,
31 PluginUnavailable,
33 DefinitionConflict,
35
36 NoMatchingTransition,
39 GuardTypeError,
41 TransitionPersistFailed,
43 GuardSideEffect,
45 InstanceNotFound,
47 GuardEvalFailed,
49 OptimisticLockConflict,
51
52 NoCandidates,
55 WithdrawNotFirstNode,
57 UnauthorizedHandle,
59 InstanceNotHandleable,
61 TaskNotHandleable,
63 AddSignTargetInvalid,
65
66 CapabilityNotFound,
69 CapabilityTimeout,
71 CandidateFormatError,
73 PluginOutputSchemaFailed,
75
76 InstanceSuspended,
79 NotAdmin,
81 IllegalStatusTransition,
83
84 DefinitionNotFound,
87 VersionNotFound,
89}
90
91impl WorkflowErrorCode {
92 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 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#[derive(Debug, Clone, Error)]
161#[non_exhaustive]
162pub struct WorkflowError {
163 pub code: WorkflowErrorCode,
165 pub message: String,
167 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 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 pub fn with_details(mut self, details: serde_json::Value) -> Self {
189 self.details = details;
190 self
191 }
192
193 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
208pub 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}