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 signing_unconfigured() -> Self {
108 Self::new(
109 StatusCode::SERVICE_UNAVAILABLE,
110 "JC0511",
111 "signed URLs are not configured — set JERRYCAN_SECRET to enable them",
112 )
113 }
114 pub fn internal(message: impl Into<String>) -> Self {
115 Self::new(StatusCode::INTERNAL_SERVER_ERROR, "JC0500", message)
116 }
117 pub fn missing_dependency(type_name: &str) -> Self {
119 Self::new(
120 StatusCode::INTERNAL_SERVER_ERROR,
121 "JC1001",
122 format!("no provider registered for dependency `{type_name}`"),
123 )
124 }
125
126 pub fn dependency_cycle() -> Self {
128 Self::new(
129 StatusCode::INTERNAL_SERVER_ERROR,
130 "JC1002",
131 "dependency cycle or chain too deep",
132 )
133 }
134
135 pub fn task_context() -> Self {
137 Self::new(
138 StatusCode::INTERNAL_SERVER_ERROR,
139 "JC1003",
140 "dependency requires an HTTP request",
141 )
142 }
143
144 pub fn with_details(mut self, details: serde_json::Value) -> Self {
147 self.details = Some(details);
148 self
149 }
150
151 pub fn details(&self) -> Option<&serde_json::Value> {
152 self.details.as_ref()
153 }
154
155 pub fn status(&self) -> StatusCode {
156 self.status
157 }
158 pub fn code(&self) -> &'static str {
159 self.code
160 }
161 pub fn message(&self) -> &str {
162 &self.message
163 }
164}
165
166impl fmt::Display for Error {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 write!(f, "{}: {}", self.code, self.message)
169 }
170}
171
172impl std::error::Error for Error {}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn errors_carry_status_and_stable_code() {
180 assert_eq!(Error::not_found().status(), StatusCode::NOT_FOUND);
181 assert_eq!(Error::not_found().code(), "JC0404");
182 assert_eq!(Error::method_not_allowed().code(), "JC0405");
183 assert_eq!(Error::bad_request("nope").status(), StatusCode::BAD_REQUEST);
184 assert_eq!(Error::payload_too_large().code(), "JC0413");
185 assert_eq!(Error::unsupported_media_type().code(), "JC0415");
186 assert_eq!(Error::unsupported_media_type().status().as_u16(), 415);
187 assert_eq!(Error::unprocessable("bad field").code(), "JC0422");
188 assert_eq!(Error::too_many_requests().code(), "JC0429");
189 assert_eq!(Error::too_many_requests().status().as_u16(), 429);
190 assert_eq!(Error::job_failed("boom").code(), "JC0521");
191 assert_eq!(Error::job_failed("boom").status().as_u16(), 500);
192 assert_eq!(
193 Error::internal("boom").status(),
194 StatusCode::INTERNAL_SERVER_ERROR
195 );
196 let e = Error::missing_dependency("app::Db");
197 assert_eq!(e.code(), "JC1001");
198 assert_eq!(e.status(), StatusCode::INTERNAL_SERVER_ERROR);
199 assert!(e.message().contains("app::Db"));
200 assert_eq!(Error::dependency_cycle().code(), "JC1002");
201 assert_eq!(Error::handler_timeout().code(), "JC0503");
202 assert_eq!(
203 Error::handler_timeout().status(),
204 StatusCode::SERVICE_UNAVAILABLE
205 );
206 assert_eq!(Error::unauthorized().code(), "JC0401");
207 assert_eq!(Error::unauthorized().status(), StatusCode::UNAUTHORIZED);
208 assert_eq!(Error::forbidden().code(), "JC0403");
209 assert_eq!(Error::forbidden().status(), StatusCode::FORBIDDEN);
210 }
211
212 #[test]
213 fn details_attach_and_default_to_none() {
214 let plain = Error::unprocessable("validation failed");
215 assert!(plain.details().is_none());
216 let detailed = Error::unprocessable("validation failed").with_details(
217 serde_json::json!([{ "field": "title", "message": "must not be empty" }]),
218 );
219 assert!(detailed.details().unwrap().is_array());
220 }
221
222 #[test]
223 fn display_includes_code_and_message() {
224 let e = Error::bad_request("missing body");
225 assert_eq!(format!("{e}"), "JC0400: missing body");
226 }
227}