1use http::StatusCode;
5use std::fmt;
6
7pub type Result<T, E = Error> = std::result::Result<T, E>;
9
10#[derive(Debug)]
15pub struct Error {
16 status: StatusCode,
17 code: &'static str,
18 message: String,
19 details: Option<serde_json::Value>,
20}
21
22impl Error {
23 pub fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
25 Self {
26 status,
27 code,
28 message: message.into(),
29 details: None,
30 }
31 }
32
33 pub fn bad_request(message: impl Into<String>) -> Self {
34 Self::new(StatusCode::BAD_REQUEST, "JC0400", message)
35 }
36 pub fn not_found() -> Self {
37 Self::new(StatusCode::NOT_FOUND, "JC0404", "not found")
38 }
39 pub fn method_not_allowed() -> Self {
40 Self::new(
41 StatusCode::METHOD_NOT_ALLOWED,
42 "JC0405",
43 "method not allowed",
44 )
45 }
46 pub fn conflict(message: impl Into<String>) -> Self {
48 Self::new(StatusCode::CONFLICT, "JC0409", message)
49 }
50 pub fn payload_too_large() -> Self {
51 Self::new(StatusCode::PAYLOAD_TOO_LARGE, "JC0413", "payload too large")
52 }
53 pub fn unsupported_media_type() -> Self {
56 Self::new(
57 StatusCode::UNSUPPORTED_MEDIA_TYPE,
58 "JC0415",
59 "unsupported media type",
60 )
61 }
62 pub fn unprocessable(message: impl Into<String>) -> Self {
63 Self::new(StatusCode::UNPROCESSABLE_ENTITY, "JC0422", message)
64 }
65 pub fn too_many_requests() -> Self {
70 Self::new(
71 StatusCode::TOO_MANY_REQUESTS,
72 "JC0429",
73 "rate limit exceeded",
74 )
75 }
76 pub fn job_failed(message: impl Into<String>) -> Self {
80 Self::new(StatusCode::INTERNAL_SERVER_ERROR, "JC0521", message)
81 }
82 pub fn unauthorized() -> Self {
84 Self::new(
85 StatusCode::UNAUTHORIZED,
86 "JC0401",
87 "authentication required",
88 )
89 }
90 pub fn forbidden() -> Self {
92 Self::new(StatusCode::FORBIDDEN, "JC0403", "forbidden")
93 }
94 pub fn handler_timeout() -> Self {
96 Self::new(
97 StatusCode::SERVICE_UNAVAILABLE,
98 "JC0503",
99 "handler timed out",
100 )
101 }
102 pub fn internal(message: impl Into<String>) -> Self {
103 Self::new(StatusCode::INTERNAL_SERVER_ERROR, "JC0500", message)
104 }
105 pub fn missing_dependency(type_name: &str) -> Self {
107 Self::new(
108 StatusCode::INTERNAL_SERVER_ERROR,
109 "JC1001",
110 format!("no provider registered for dependency `{type_name}`"),
111 )
112 }
113
114 pub fn dependency_cycle() -> Self {
116 Self::new(
117 StatusCode::INTERNAL_SERVER_ERROR,
118 "JC1002",
119 "dependency cycle or chain too deep",
120 )
121 }
122
123 pub fn task_context() -> Self {
125 Self::new(
126 StatusCode::INTERNAL_SERVER_ERROR,
127 "JC1003",
128 "dependency requires an HTTP request",
129 )
130 }
131
132 pub fn with_details(mut self, details: serde_json::Value) -> Self {
135 self.details = Some(details);
136 self
137 }
138
139 pub fn details(&self) -> Option<&serde_json::Value> {
140 self.details.as_ref()
141 }
142
143 pub fn status(&self) -> StatusCode {
144 self.status
145 }
146 pub fn code(&self) -> &'static str {
147 self.code
148 }
149 pub fn message(&self) -> &str {
150 &self.message
151 }
152}
153
154impl fmt::Display for Error {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 write!(f, "{}: {}", self.code, self.message)
157 }
158}
159
160impl std::error::Error for Error {}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn errors_carry_status_and_stable_code() {
168 assert_eq!(Error::not_found().status(), StatusCode::NOT_FOUND);
169 assert_eq!(Error::not_found().code(), "JC0404");
170 assert_eq!(Error::method_not_allowed().code(), "JC0405");
171 assert_eq!(Error::bad_request("nope").status(), StatusCode::BAD_REQUEST);
172 assert_eq!(Error::payload_too_large().code(), "JC0413");
173 assert_eq!(Error::unsupported_media_type().code(), "JC0415");
174 assert_eq!(Error::unsupported_media_type().status().as_u16(), 415);
175 assert_eq!(Error::unprocessable("bad field").code(), "JC0422");
176 assert_eq!(Error::too_many_requests().code(), "JC0429");
177 assert_eq!(Error::too_many_requests().status().as_u16(), 429);
178 assert_eq!(Error::job_failed("boom").code(), "JC0521");
179 assert_eq!(Error::job_failed("boom").status().as_u16(), 500);
180 assert_eq!(
181 Error::internal("boom").status(),
182 StatusCode::INTERNAL_SERVER_ERROR
183 );
184 let e = Error::missing_dependency("app::Db");
185 assert_eq!(e.code(), "JC1001");
186 assert_eq!(e.status(), StatusCode::INTERNAL_SERVER_ERROR);
187 assert!(e.message().contains("app::Db"));
188 assert_eq!(Error::dependency_cycle().code(), "JC1002");
189 assert_eq!(Error::handler_timeout().code(), "JC0503");
190 assert_eq!(
191 Error::handler_timeout().status(),
192 StatusCode::SERVICE_UNAVAILABLE
193 );
194 assert_eq!(Error::unauthorized().code(), "JC0401");
195 assert_eq!(Error::unauthorized().status(), StatusCode::UNAUTHORIZED);
196 assert_eq!(Error::forbidden().code(), "JC0403");
197 assert_eq!(Error::forbidden().status(), StatusCode::FORBIDDEN);
198 }
199
200 #[test]
201 fn details_attach_and_default_to_none() {
202 let plain = Error::unprocessable("validation failed");
203 assert!(plain.details().is_none());
204 let detailed = Error::unprocessable("validation failed").with_details(
205 serde_json::json!([{ "field": "title", "message": "must not be empty" }]),
206 );
207 assert!(detailed.details().unwrap().is_array());
208 }
209
210 #[test]
211 fn display_includes_code_and_message() {
212 let e = Error::bad_request("missing body");
213 assert_eq!(format!("{e}"), "JC0400: missing body");
214 }
215}