1use axum::http::StatusCode;
10use axum::response::{IntoResponse, Response};
11use axum::Json;
12use serde::Serialize;
13
14#[derive(Debug, Clone, Copy)]
17pub enum AppErrorKind {
18 Unauthorized,
19 BadRequest,
20 NotFound,
21 Conflict,
22 Internal,
23 FormationCycle,
27 FormationMultipleCoordinators,
28 FormationNoCoordinator,
29 FormationAuthorityNotNarrowing,
30}
31
32impl AppErrorKind {
33 pub fn status(self) -> StatusCode {
34 match self {
35 AppErrorKind::Unauthorized => StatusCode::UNAUTHORIZED,
36 AppErrorKind::BadRequest
37 | AppErrorKind::FormationCycle
38 | AppErrorKind::FormationMultipleCoordinators
39 | AppErrorKind::FormationNoCoordinator
40 | AppErrorKind::FormationAuthorityNotNarrowing => StatusCode::BAD_REQUEST,
41 AppErrorKind::NotFound => StatusCode::NOT_FOUND,
42 AppErrorKind::Conflict => StatusCode::CONFLICT,
43 AppErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
44 }
45 }
46
47 pub fn type_uri(self) -> &'static str {
51 match self {
52 AppErrorKind::Unauthorized => "/problems/unauthorized",
53 AppErrorKind::BadRequest => "/problems/bad-request",
54 AppErrorKind::NotFound => "/problems/not-found",
55 AppErrorKind::Conflict => "/problems/conflict",
56 AppErrorKind::Internal => "/problems/internal",
57 AppErrorKind::FormationCycle => "/problems/formation/cycle",
58 AppErrorKind::FormationMultipleCoordinators => {
59 "/problems/formation/multiple-coordinators"
60 }
61 AppErrorKind::FormationNoCoordinator => "/problems/formation/no-coordinator",
62 AppErrorKind::FormationAuthorityNotNarrowing => {
63 "/problems/formation/authority-not-narrowing"
64 }
65 }
66 }
67
68 pub fn title(self) -> &'static str {
69 match self {
70 AppErrorKind::Unauthorized => "Unauthorized",
71 AppErrorKind::BadRequest => "Bad Request",
72 AppErrorKind::NotFound => "Not Found",
73 AppErrorKind::Conflict => "Conflict",
74 AppErrorKind::Internal => "Internal Server Error",
75 AppErrorKind::FormationCycle => "Formation rejected: authority cycle",
76 AppErrorKind::FormationMultipleCoordinators => {
77 "Formation rejected: multiple coordinators"
78 }
79 AppErrorKind::FormationNoCoordinator => "Formation rejected: no coordinator",
80 AppErrorKind::FormationAuthorityNotNarrowing => {
81 "Formation rejected: authority does not narrow"
82 }
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
88pub struct AppError {
89 pub kind: AppErrorKind,
90 pub detail: String,
91}
92
93impl AppError {
94 pub fn new(kind: AppErrorKind, detail: impl Into<String>) -> Self {
95 Self {
96 kind,
97 detail: detail.into(),
98 }
99 }
100
101 pub fn bad_request(detail: impl Into<String>) -> Self {
102 Self::new(AppErrorKind::BadRequest, detail)
103 }
104
105 pub fn unauthorized(detail: impl Into<String>) -> Self {
106 Self::new(AppErrorKind::Unauthorized, detail)
107 }
108
109 pub fn not_found(detail: impl Into<String>) -> Self {
110 Self::new(AppErrorKind::NotFound, detail)
111 }
112
113 pub fn internal(detail: impl Into<String>) -> Self {
114 Self::new(AppErrorKind::Internal, detail)
115 }
116}
117
118#[derive(Debug, Serialize)]
120struct ProblemDetails<'a> {
121 #[serde(rename = "type")]
122 type_uri: &'a str,
123 title: &'a str,
124 status: u16,
125 detail: &'a str,
126}
127
128impl IntoResponse for AppError {
129 fn into_response(self) -> Response {
130 let status = self.kind.status();
131 let body = ProblemDetails {
132 type_uri: self.kind.type_uri(),
133 title: self.kind.title(),
134 status: status.as_u16(),
135 detail: &self.detail,
136 };
137 let mut resp = (status, Json(body)).into_response();
138 resp.headers_mut().insert(
140 axum::http::header::CONTENT_TYPE,
141 axum::http::HeaderValue::from_static("application/problem+json"),
142 );
143 resp
144 }
145}
146
147impl From<anyhow::Error> for AppError {
148 fn from(e: anyhow::Error) -> Self {
149 AppError::internal(format!("{e:#}"))
150 }
151}
152
153impl From<serde_json::Error> for AppError {
154 fn from(e: serde_json::Error) -> Self {
155 AppError::bad_request(format!("invalid json: {e}"))
156 }
157}