Skip to main content

caelix_core/
exception.rs

1#![allow(clippy::new_ret_no_self)]
2
3use std::collections::BTreeMap;
4
5use http::StatusCode;
6
7#[derive(Debug)]
8pub struct HttpException {
9    pub status: StatusCode,
10    pub message: String,
11    pub error: &'static str,
12    pub errors: Option<BTreeMap<String, Vec<String>>>,
13    pub source: Option<anyhow::Error>,
14}
15
16impl HttpException {
17    pub fn new(status: StatusCode, error: &'static str, message: impl Into<String>) -> Self {
18        Self {
19            status,
20            error,
21            message: message.into(),
22            errors: None,
23            source: None,
24        }
25    }
26
27    pub fn with_source(mut self, err: impl Into<anyhow::Error>) -> Self {
28        self.source = Some(err.into());
29        self
30    }
31
32    pub fn with_errors(mut self, errors: BTreeMap<String, Vec<String>>) -> Self {
33        self.errors = Some(errors);
34        self
35    }
36}
37
38pub(crate) fn startup_error(message: impl Into<String>) -> HttpException {
39    HttpException::new(
40        StatusCode::INTERNAL_SERVER_ERROR,
41        "Internal Server Error",
42        message,
43    )
44}
45
46pub struct BadRequestException;
47impl BadRequestException {
48    pub fn new(message: impl Into<String>) -> HttpException {
49        HttpException::new(StatusCode::BAD_REQUEST, "Bad Request", message)
50    }
51}
52
53pub struct UnauthorizedException;
54impl UnauthorizedException {
55    pub fn new(message: impl Into<String>) -> HttpException {
56        HttpException::new(StatusCode::UNAUTHORIZED, "Unauthorized", message)
57    }
58}
59
60pub struct PaymentRequiredException;
61impl PaymentRequiredException {
62    pub fn new(message: impl Into<String>) -> HttpException {
63        HttpException::new(StatusCode::PAYMENT_REQUIRED, "Payment Required", message)
64    }
65}
66
67pub struct ForbiddenException;
68impl ForbiddenException {
69    pub fn new(message: impl Into<String>) -> HttpException {
70        HttpException::new(StatusCode::FORBIDDEN, "Forbidden", message)
71    }
72}
73
74pub struct NotFoundException;
75impl NotFoundException {
76    pub fn new(message: impl Into<String>) -> HttpException {
77        HttpException::new(StatusCode::NOT_FOUND, "Not Found", message)
78    }
79}
80
81pub struct MethodNotAllowedException;
82impl MethodNotAllowedException {
83    pub fn new(message: impl Into<String>) -> HttpException {
84        HttpException::new(
85            StatusCode::METHOD_NOT_ALLOWED,
86            "Method Not Allowed",
87            message,
88        )
89    }
90}
91
92pub struct NotAcceptableException;
93impl NotAcceptableException {
94    pub fn new(message: impl Into<String>) -> HttpException {
95        HttpException::new(StatusCode::NOT_ACCEPTABLE, "Not Acceptable", message)
96    }
97}
98
99pub struct ProxyAuthenticationRequiredException;
100impl ProxyAuthenticationRequiredException {
101    pub fn new(message: impl Into<String>) -> HttpException {
102        HttpException::new(
103            StatusCode::PROXY_AUTHENTICATION_REQUIRED,
104            "Proxy Authentication Required",
105            message,
106        )
107    }
108}
109
110pub struct RequestTimeoutException;
111impl RequestTimeoutException {
112    pub fn new(message: impl Into<String>) -> HttpException {
113        HttpException::new(StatusCode::REQUEST_TIMEOUT, "Request Timeout", message)
114    }
115}
116
117pub struct ConflictException;
118impl ConflictException {
119    pub fn new(message: impl Into<String>) -> HttpException {
120        HttpException::new(StatusCode::CONFLICT, "Conflict", message)
121    }
122}
123
124pub struct GoneException;
125impl GoneException {
126    pub fn new(message: impl Into<String>) -> HttpException {
127        HttpException::new(StatusCode::GONE, "Gone", message)
128    }
129}
130
131pub struct LengthRequiredException;
132impl LengthRequiredException {
133    pub fn new(message: impl Into<String>) -> HttpException {
134        HttpException::new(StatusCode::LENGTH_REQUIRED, "Length Required", message)
135    }
136}
137
138pub struct PreconditionFailedException;
139impl PreconditionFailedException {
140    pub fn new(message: impl Into<String>) -> HttpException {
141        HttpException::new(
142            StatusCode::PRECONDITION_FAILED,
143            "Precondition Failed",
144            message,
145        )
146    }
147}
148
149pub struct PayloadTooLargeException;
150impl PayloadTooLargeException {
151    pub fn new(message: impl Into<String>) -> HttpException {
152        HttpException::new(StatusCode::PAYLOAD_TOO_LARGE, "Payload Too Large", message)
153    }
154}
155
156pub struct UriTooLongException;
157impl UriTooLongException {
158    pub fn new(message: impl Into<String>) -> HttpException {
159        HttpException::new(StatusCode::URI_TOO_LONG, "URI Too Long", message)
160    }
161}
162
163pub struct UnsupportedMediaTypeException;
164impl UnsupportedMediaTypeException {
165    pub fn new(message: impl Into<String>) -> HttpException {
166        HttpException::new(
167            StatusCode::UNSUPPORTED_MEDIA_TYPE,
168            "Unsupported Media Type",
169            message,
170        )
171    }
172}
173
174pub struct RangeNotSatisfiableException;
175impl RangeNotSatisfiableException {
176    pub fn new(message: impl Into<String>) -> HttpException {
177        HttpException::new(
178            StatusCode::RANGE_NOT_SATISFIABLE,
179            "Range Not Satisfiable",
180            message,
181        )
182    }
183}
184
185pub struct ExpectationFailedException;
186impl ExpectationFailedException {
187    pub fn new(message: impl Into<String>) -> HttpException {
188        HttpException::new(
189            StatusCode::EXPECTATION_FAILED,
190            "Expectation Failed",
191            message,
192        )
193    }
194}
195
196pub struct ImATeapotException;
197impl ImATeapotException {
198    pub fn new(message: impl Into<String>) -> HttpException {
199        HttpException::new(StatusCode::IM_A_TEAPOT, "I'm a teapot", message)
200    }
201}
202
203pub struct MisdirectedRequestException;
204impl MisdirectedRequestException {
205    pub fn new(message: impl Into<String>) -> HttpException {
206        HttpException::new(
207            StatusCode::MISDIRECTED_REQUEST,
208            "Misdirected Request",
209            message,
210        )
211    }
212}
213
214pub struct UnprocessableEntityException;
215impl UnprocessableEntityException {
216    pub fn new(message: impl Into<String>) -> HttpException {
217        HttpException::new(
218            StatusCode::UNPROCESSABLE_ENTITY,
219            "Unprocessable Entity",
220            message,
221        )
222    }
223}
224
225pub struct LockedException;
226impl LockedException {
227    pub fn new(message: impl Into<String>) -> HttpException {
228        HttpException::new(StatusCode::LOCKED, "Locked", message)
229    }
230}
231
232pub struct FailedDependencyException;
233impl FailedDependencyException {
234    pub fn new(message: impl Into<String>) -> HttpException {
235        HttpException::new(StatusCode::FAILED_DEPENDENCY, "Failed Dependency", message)
236    }
237}
238
239pub struct TooEarlyException;
240impl TooEarlyException {
241    pub fn new(message: impl Into<String>) -> HttpException {
242        HttpException::new(StatusCode::TOO_EARLY, "Too Early", message)
243    }
244}
245
246pub struct UpgradeRequiredException;
247impl UpgradeRequiredException {
248    pub fn new(message: impl Into<String>) -> HttpException {
249        HttpException::new(StatusCode::UPGRADE_REQUIRED, "Upgrade Required", message)
250    }
251}
252
253pub struct PreconditionRequiredException;
254impl PreconditionRequiredException {
255    pub fn new(message: impl Into<String>) -> HttpException {
256        HttpException::new(
257            StatusCode::PRECONDITION_REQUIRED,
258            "Precondition Required",
259            message,
260        )
261    }
262}
263
264pub struct TooManyRequestsException;
265impl TooManyRequestsException {
266    pub fn new(message: impl Into<String>) -> HttpException {
267        HttpException::new(StatusCode::TOO_MANY_REQUESTS, "Too Many Requests", message)
268    }
269}
270
271pub struct RequestHeaderFieldsTooLargeException;
272impl RequestHeaderFieldsTooLargeException {
273    pub fn new(message: impl Into<String>) -> HttpException {
274        HttpException::new(
275            StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
276            "Request Header Fields Too Large",
277            message,
278        )
279    }
280}
281
282pub struct UnavailableForLegalReasonsException;
283impl UnavailableForLegalReasonsException {
284    pub fn new(message: impl Into<String>) -> HttpException {
285        HttpException::new(
286            StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
287            "Unavailable For Legal Reasons",
288            message,
289        )
290    }
291}
292
293pub struct InternalServerErrorException;
294impl InternalServerErrorException {
295    pub fn new(err: impl Into<anyhow::Error>) -> HttpException {
296        HttpException::new(
297            StatusCode::INTERNAL_SERVER_ERROR,
298            "Internal Server Error",
299            "Internal Server Error",
300        )
301        .with_source(err)
302    }
303}
304
305#[cfg(feature = "sqlx")]
306impl From<sqlx::Error> for HttpException {
307    fn from(err: sqlx::Error) -> Self {
308        InternalServerErrorException::new(err)
309    }
310}
311
312#[cfg(feature = "validator")]
313impl From<validator::ValidationErrors> for HttpException {
314    fn from(err: validator::ValidationErrors) -> Self {
315        BadRequestException::new("Validation failed").with_errors(format_validation_errors(&err))
316    }
317}
318
319#[cfg(feature = "validator")]
320fn format_validation_errors(err: &validator::ValidationErrors) -> BTreeMap<String, Vec<String>> {
321    let mut errors = BTreeMap::new();
322    collect_validation_errors("", err, &mut errors);
323    errors
324}
325
326#[cfg(feature = "validator")]
327fn collect_validation_errors(
328    prefix: &str,
329    err: &validator::ValidationErrors,
330    field_errors: &mut BTreeMap<String, Vec<String>>,
331) {
332    let mut fields = err.errors().iter().collect::<Vec<_>>();
333    fields.sort_by(|(left, _), (right, _)| left.as_ref().cmp(right.as_ref()));
334
335    for (field, kind) in fields {
336        let path = if prefix.is_empty() {
337            (*field).to_string()
338        } else {
339            format!("{prefix}.{field}")
340        };
341
342        collect_validation_error_kind(&path, kind, field_errors);
343    }
344}
345
346#[cfg(feature = "validator")]
347fn collect_validation_error_kind(
348    path: &str,
349    kind: &validator::ValidationErrorsKind,
350    field_errors: &mut BTreeMap<String, Vec<String>>,
351) {
352    match kind {
353        validator::ValidationErrorsKind::Field(errors) => {
354            for error in errors {
355                field_errors
356                    .entry(path.to_string())
357                    .or_default()
358                    .push(format_validation_error(error));
359            }
360        }
361        validator::ValidationErrorsKind::Struct(errors) => {
362            collect_validation_errors(path, errors, field_errors);
363        }
364        validator::ValidationErrorsKind::List(errors) => {
365            for (index, errors) in errors {
366                collect_validation_errors(&format!("{path}[{index}]"), errors, field_errors);
367            }
368        }
369    }
370}
371
372#[cfg(feature = "validator")]
373fn format_validation_error(error: &validator::ValidationError) -> String {
374    if let Some(message) = &error.message {
375        return message.to_string();
376    }
377
378    match error.code.as_ref() {
379        "email" => "must be a valid email".to_string(),
380        "length" => format_length_validation_error(error),
381        "required" => "is required".to_string(),
382        "url" => "must be a valid URL".to_string(),
383        "regex" => "has an invalid format".to_string(),
384        "contains" => match validation_param(error, "needle") {
385            Some(needle) => format!("must contain {needle}"),
386            None => "must contain the required value".to_string(),
387        },
388        "does_not_contain" => match validation_param(error, "needle") {
389            Some(needle) => format!("must not contain {needle}"),
390            None => "contains a forbidden value".to_string(),
391        },
392        "must_match" => match validation_param(error, "other") {
393            Some(other) => format!("must match {other}"),
394            None => "does not match".to_string(),
395        },
396        "ip" => "must be a valid IP address".to_string(),
397        "ipv4" => "must be a valid IPv4 address".to_string(),
398        "ipv6" => "must be a valid IPv6 address".to_string(),
399        code => format!("is invalid ({code})"),
400    }
401}
402
403#[cfg(feature = "validator")]
404fn format_length_validation_error(error: &validator::ValidationError) -> String {
405    match (
406        validation_param(error, "equal"),
407        validation_param(error, "min"),
408        validation_param(error, "max"),
409    ) {
410        (Some(equal), _, _) => format!("must be exactly {equal} characters"),
411        (None, Some(min), Some(max)) => {
412            format!("must be between {min} and {max} characters")
413        }
414        (None, Some(min), None) => format!("must be at least {min} characters"),
415        (None, None, Some(max)) => format!("must be at most {max} characters"),
416        (None, None, None) => "has an invalid length".to_string(),
417    }
418}
419
420#[cfg(feature = "validator")]
421fn validation_param(error: &validator::ValidationError, name: &str) -> Option<String> {
422    let value = error.params.get(name)?;
423
424    match value {
425        serde_json::Value::String(value) => Some(value.clone()),
426        serde_json::Value::Number(value) => Some(value.to_string()),
427        serde_json::Value::Bool(value) => Some(value.to_string()),
428        serde_json::Value::Null => None,
429        value => Some(value.to_string()),
430    }
431}
432
433pub struct NotImplementedException;
434impl NotImplementedException {
435    pub fn new(message: impl Into<String>) -> HttpException {
436        HttpException::new(StatusCode::NOT_IMPLEMENTED, "Not Implemented", message)
437    }
438}
439
440pub struct BadGatewayException;
441impl BadGatewayException {
442    pub fn new(message: impl Into<String>) -> HttpException {
443        HttpException::new(StatusCode::BAD_GATEWAY, "Bad Gateway", message)
444    }
445}
446
447pub struct ServiceUnavailableException;
448impl ServiceUnavailableException {
449    pub fn new(message: impl Into<String>) -> HttpException {
450        HttpException::new(
451            StatusCode::SERVICE_UNAVAILABLE,
452            "Service Unavailable",
453            message,
454        )
455    }
456}
457
458pub struct GatewayTimeoutException;
459impl GatewayTimeoutException {
460    pub fn new(message: impl Into<String>) -> HttpException {
461        HttpException::new(StatusCode::GATEWAY_TIMEOUT, "Gateway Timeout", message)
462    }
463}
464
465pub struct HttpVersionNotSupportedException;
466impl HttpVersionNotSupportedException {
467    pub fn new(message: impl Into<String>) -> HttpException {
468        HttpException::new(
469            StatusCode::HTTP_VERSION_NOT_SUPPORTED,
470            "HTTP Version Not Supported",
471            message,
472        )
473    }
474}
475
476pub struct VariantAlsoNegotiatesException;
477impl VariantAlsoNegotiatesException {
478    pub fn new(message: impl Into<String>) -> HttpException {
479        HttpException::new(
480            StatusCode::VARIANT_ALSO_NEGOTIATES,
481            "Variant Also Negotiates",
482            message,
483        )
484    }
485}
486
487pub struct InsufficientStorageException;
488impl InsufficientStorageException {
489    pub fn new(message: impl Into<String>) -> HttpException {
490        HttpException::new(
491            StatusCode::INSUFFICIENT_STORAGE,
492            "Insufficient Storage",
493            message,
494        )
495    }
496}
497
498pub struct LoopDetectedException;
499impl LoopDetectedException {
500    pub fn new(message: impl Into<String>) -> HttpException {
501        HttpException::new(StatusCode::LOOP_DETECTED, "Loop Detected", message)
502    }
503}
504
505pub struct NotExtendedException;
506impl NotExtendedException {
507    pub fn new(message: impl Into<String>) -> HttpException {
508        HttpException::new(StatusCode::NOT_EXTENDED, "Not Extended", message)
509    }
510}
511
512pub struct NetworkAuthenticationRequiredException;
513impl NetworkAuthenticationRequiredException {
514    pub fn new(message: impl Into<String>) -> HttpException {
515        HttpException::new(
516            StatusCode::NETWORK_AUTHENTICATION_REQUIRED,
517            "Network Authentication Required",
518            message,
519        )
520    }
521}