-
Notifications
You must be signed in to change notification settings - Fork 134
/
Copy patherrors_internal.rs
139 lines (123 loc) · 4.37 KB
/
errors_internal.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
use std::borrow::Cow;
use hyper::Body;
use hyper::Response;
use routerify::RequestInfo;
use serde::Deserialize;
use serde::Serialize;
use tracing::Span;
use crate::api::ApiError;
#[derive(Serialize, Deserialize)]
pub struct ApiErrorStruct {
pub code: Cow<'static, str>,
pub message: Cow<'static, str>,
#[serde(flatten)]
pub data: serde_json::Value,
}
/// This macro builds an error enum that can be rendered to a `Response`. Every
/// error variant has a "code" (e.g `publishPayloadTooLarge`), "message" (a
/// human readable description of the error), and a "status code" (the HTTP
/// status code associated with the error). All three of these fields are
/// publicly visible to users, and thus should not contain any sensitive
/// information.
///
/// The variant name (e.g `NotFound`) will be used as both the variant name, and
/// after being converted to `camelCase` will be used as the error code.
///
/// Error variants can contain fields, which can be used when formatting the
/// error message. The fields are declared in the macro using the
/// `fields: { ... }` syntax. The fields are then available in the error message
/// using the `({ <field>* }) => <format string>` syntax.
///
/// By default, fields are not visible to users. If a field should be serialized
/// into the JSON response, the identifier should be added to the `data_fields`
/// list. The field will then be serialized into the `data` field of the JSON
/// response.
///
/// ### Example
///
/// ```rs
/// errors!(
/// NotFound {
/// status: NOT_FOUND,
/// "The requested resource was not found.",
/// },
/// DeploymentFailed {
/// status: BAD_REQUEST,
/// fields: { msg: Cow<'static, str>, deployment: Deployment },
/// data_fields: { deployment },
/// ({ msg }) => "The deployment failed: {msg}.",
/// },
/// );
/// ```
#[macro_export]
macro_rules! errors {
($($name:ident { status: $status:ident $(, fields: $fields:tt $(, data_fields: { $($data_field:ident),*$(,)? })?)? $(, headers: $({ $($headers_pattern:ident),*$(,)? } => )? [ $(($header_name:expr, $header_value:expr)),*$(,)? ])? $(, $msg_lit:literal)? $(, ($pattern:tt) => $msg_expr:tt)? $(,)? }),*$(,)?) => {
#[derive(Debug, Clone)]
pub enum ApiError {
$($name $($fields)?),*
}
impl ApiError {
pub fn status_code(&self) -> hyper::StatusCode {
match self {
$(Self::$name { .. } => hyper::StatusCode::$status),*
}
}
pub fn code(&self) -> &'static str {
match self {
$(Self::$name { .. } => const_format::map_ascii_case!(const_format::Case::Camel, stringify!($name))),*
}
}
pub fn message(&self) -> std::borrow::Cow<'static, str> {
match self {
$(Self::$name $({..} => std::borrow::Cow::Borrowed($msg_lit))? $($pattern => std::borrow::Cow::Owned(format!($msg_expr)))?),*
}
}
fn data(&self) -> serde_json::Value {
match self {
$(Self::$name { $($($($data_field),*,)?)? .. } => {
serde_json::json!({
$($($(
const_format::map_ascii_case!(const_format::Case::Camel, stringify!($data_field)): $data_field,
)*)?)?
})
})*
}
}
}
impl ApiError {
pub fn json(&self) -> String {
let err = $crate::errors_internal::ApiErrorStruct {
code: Cow::Borrowed(self.code()),
message: self.message(),
data: self.data()
};
serde_json::to_string_pretty(&err).unwrap()
}
pub fn json_response(&self) -> Response<Body> {
Response::builder()
.status(self.status_code())
.header("Content-Type", "application/json")
.body(Body::from(self.json()))
.unwrap()
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message())
}
}
impl std::error::Error for ApiError {}
};
}
pub async fn error_handler(
err: routerify::RouteError,
_: RequestInfo,
) -> Response<Body> {
// Because `routerify::RouteError` is a boxed error, it must be downcast
// first. Unwrap for simplicity.
let api_err = err.downcast::<ApiError>().unwrap();
let span = Span::current();
span.record("otel.status_code", "error");
api_err.json_response()
}