1use std::error::Error as StdError;
4
5use thiserror::Error;
6
7pub type Result<T> = std::result::Result<T, BosonError>;
9
10#[derive(Debug, Error)]
12pub enum BosonError {
13 #[error("task not found: {0}")]
15 TaskNotFound(String),
16
17 #[error("job not found: {0}")]
19 JobNotFound(String),
20
21 #[error("run not found: {0}")]
23 RunNotFound(String),
24
25 #[error("task config not found: {0}")]
27 TaskConfigNotFound(String),
28
29 #[error("parameter error: {0}")]
31 ParamError(String),
32
33 #[error("signature mismatch: job expects {expected}, task has {actual}")]
35 SignatureMismatch {
36 expected: String,
38 actual: String,
40 },
41
42 #[error("invalid config: {0}")]
44 InvalidConfig(String),
45
46 #[error("backend error: {message}")]
48 Backend {
49 message: String,
51 #[source]
53 source: Option<Box<dyn StdError + Send + Sync>>,
54 },
55
56 #[error("internal error: {message}")]
58 Internal {
59 message: String,
61 #[source]
63 source: Option<Box<dyn StdError + Send + Sync>>,
64 },
65
66 #[error("enqueue rate limited for task: {0}")]
68 RateLimited(String),
69
70 #[error("unknown queue backend: {0}")]
72 UnknownBackend(String),
73}
74
75impl BosonError {
76 #[must_use]
78 pub fn backend(message: impl Into<String>) -> Self {
79 Self::Backend {
80 message: message.into(),
81 source: None,
82 }
83 }
84
85 #[must_use]
87 pub fn backend_source(
88 message: impl Into<String>,
89 source: impl StdError + Send + Sync + 'static,
90 ) -> Self {
91 Self::Backend {
92 message: message.into(),
93 source: Some(Box::new(source)),
94 }
95 }
96
97 #[must_use]
99 pub fn internal(message: impl Into<String>) -> Self {
100 Self::Internal {
101 message: message.into(),
102 source: None,
103 }
104 }
105
106 #[must_use]
108 pub fn internal_source(
109 message: impl Into<String>,
110 source: impl StdError + Send + Sync + 'static,
111 ) -> Self {
112 Self::Internal {
113 message: message.into(),
114 source: Some(Box::new(source)),
115 }
116 }
117
118 #[must_use]
120 pub fn backend_message(&self) -> Option<&str> {
121 match self {
122 Self::Backend { message, .. } => Some(message.as_str()),
123 _ => None,
124 }
125 }
126
127 #[must_use]
129 pub fn is_backend_unique_violation(&self) -> bool {
130 self.backend_message().is_some_and(|msg| {
131 msg.contains("UNIQUE") || msg.contains("unique") || msg.contains("Duplicate")
132 })
133 }
134}
135
136#[derive(Debug, Error)]
138pub enum IdentityError {
139 #[error("invalid actor: {0}")]
141 InvalidActor(String),
142}
143
144impl From<serde_json::Error> for BosonError {
145 fn from(err: serde_json::Error) -> Self {
146 Self::ParamError(err.to_string())
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use std::error::Error as StdError;
153
154 use super::BosonError;
155
156 #[derive(Debug, thiserror::Error)]
157 #[error("dial failed")]
158 struct DialFailed;
159
160 #[test]
161 fn backend_source_is_reachable_via_std_error() {
162 let err = BosonError::backend_source("nats connect", DialFailed);
163 assert!(err
164 .backend_message()
165 .is_some_and(|m| m.contains("nats connect")));
166 assert!(err
167 .source()
168 .is_some_and(|s| s.to_string().contains("dial failed")));
169 }
170
171 #[test]
172 fn unique_violation_helper_matches_message() {
173 let err = BosonError::backend("sql backend: UNIQUE constraint failed");
174 assert!(err.is_backend_unique_violation());
175 assert!(!BosonError::backend("timeout").is_backend_unique_violation());
176 }
177}