1use axum::Json;
2use axum::http::StatusCode;
3use axum::response::{IntoResponse, Response};
4use serde_json::json;
5
6#[derive(Debug)]
7pub enum ServerError {
8 BadRequest(String),
9 BadEnvelope(String),
10 DecryptFailed(String),
11 AuthFailed,
12 Forbidden,
13 NotFound,
14 Conflict {
15 code: String,
16 message: String,
17 details: Option<serde_json::Value>,
18 },
19 InvalidPath(String),
20 InvalidRevision,
21 InvalidProjectState(String),
22 ServerKeyMismatch,
23 PayloadTooLarge,
24 Internal(String),
25}
26
27impl IntoResponse for ServerError {
28 fn into_response(self) -> Response {
29 let (status, code, message, details): (
30 StatusCode,
31 &str,
32 String,
33 Option<serde_json::Value>,
34 ) = match &self {
35 ServerError::BadRequest(msg) => {
36 (StatusCode::BAD_REQUEST, "bad_request", msg.clone(), None)
37 }
38 ServerError::BadEnvelope(msg) => {
39 (StatusCode::BAD_REQUEST, "bad_envelope", msg.clone(), None)
40 }
41 ServerError::DecryptFailed(msg) => {
42 (StatusCode::BAD_REQUEST, "decrypt_failed", msg.clone(), None)
43 }
44 ServerError::AuthFailed => (
45 StatusCode::UNAUTHORIZED,
46 "auth_failed",
47 "authentication failed".into(),
48 None,
49 ),
50 ServerError::Forbidden => (
51 StatusCode::FORBIDDEN,
52 "forbidden",
53 "insufficient capabilities".into(),
54 None,
55 ),
56 ServerError::NotFound => (
57 StatusCode::NOT_FOUND,
58 "not_found",
59 "resource not found".into(),
60 None,
61 ),
62 ServerError::Conflict {
63 code,
64 message,
65 details,
66 } => {
67 return (StatusCode::CONFLICT, Json(json!({"ok": false, "error": {"code": code, "message": message, "details": details}}))).into_response();
68 }
69 ServerError::InvalidPath(msg) => {
70 (StatusCode::BAD_REQUEST, "invalid_path", msg.clone(), None)
71 }
72 ServerError::InvalidRevision => (
73 StatusCode::BAD_REQUEST,
74 "invalid_revision",
75 "revision mismatch".into(),
76 None,
77 ),
78 ServerError::InvalidProjectState(msg) => (
79 StatusCode::BAD_REQUEST,
80 "invalid_project_state",
81 msg.clone(),
82 None,
83 ),
84 ServerError::ServerKeyMismatch => (
85 StatusCode::BAD_REQUEST,
86 "server_key_mismatch",
87 "unknown server key".into(),
88 None,
89 ),
90 ServerError::PayloadTooLarge => (
91 StatusCode::PAYLOAD_TOO_LARGE,
92 "payload_too_large",
93 "project storage limit exceeded".into(),
94 None,
95 ),
96 ServerError::Internal(msg) => (
97 StatusCode::INTERNAL_SERVER_ERROR,
98 "internal",
99 msg.clone(),
100 None,
101 ),
102 };
103
104 let body = Json(json!({
105 "ok": false,
106 "error": { "code": code, "message": message, "details": details }
107 }));
108 (status, body).into_response()
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use axum::response::IntoResponse;
116
117 fn status_of(err: ServerError) -> StatusCode {
118 err.into_response().status()
119 }
120
121 #[test]
122 fn test_bad_request_status() {
123 assert_eq!(
124 status_of(ServerError::BadRequest("x".into())),
125 StatusCode::BAD_REQUEST
126 );
127 }
128
129 #[test]
130 fn test_auth_failed_status() {
131 assert_eq!(status_of(ServerError::AuthFailed), StatusCode::UNAUTHORIZED);
132 }
133
134 #[test]
135 fn test_forbidden_status() {
136 assert_eq!(status_of(ServerError::Forbidden), StatusCode::FORBIDDEN);
137 }
138
139 #[test]
140 fn test_not_found_status() {
141 assert_eq!(status_of(ServerError::NotFound), StatusCode::NOT_FOUND);
142 }
143
144 #[test]
145 fn test_conflict_status() {
146 let err = ServerError::Conflict {
147 code: "c".into(),
148 message: "m".into(),
149 details: None,
150 };
151 assert_eq!(status_of(err), StatusCode::CONFLICT);
152 }
153
154 #[test]
155 fn test_internal_status() {
156 assert_eq!(
157 status_of(ServerError::Internal("x".into())),
158 StatusCode::INTERNAL_SERVER_ERROR
159 );
160 }
161
162 #[test]
163 fn test_payload_too_large_status() {
164 assert_eq!(
165 status_of(ServerError::PayloadTooLarge),
166 StatusCode::PAYLOAD_TOO_LARGE
167 );
168 }
169}