1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use lago_core::LagoError;
4use serde::Serialize;
5
6#[derive(Debug)]
8pub enum ApiError {
9 Lago(LagoError),
11 BadRequest(String),
13 NotFound(String),
15 Forbidden(String),
17 Conflict(String),
19 Internal(String),
21}
22
23#[derive(Serialize)]
25struct ErrorBody {
26 error: String,
27 message: String,
28}
29
30impl IntoResponse for ApiError {
31 fn into_response(self) -> Response {
32 let (status, error_type, message) = match &self {
33 ApiError::Lago(e) => match e {
34 LagoError::SessionNotFound(id) => (
35 StatusCode::NOT_FOUND,
36 "session_not_found",
37 format!("session not found: {id}"),
38 ),
39 LagoError::BranchNotFound(id) => (
40 StatusCode::NOT_FOUND,
41 "branch_not_found",
42 format!("branch not found: {id}"),
43 ),
44 LagoError::EventNotFound(id) => (
45 StatusCode::NOT_FOUND,
46 "event_not_found",
47 format!("event not found: {id}"),
48 ),
49 LagoError::BlobNotFound(hash) => (
50 StatusCode::NOT_FOUND,
51 "blob_not_found",
52 format!("blob not found: {hash}"),
53 ),
54 LagoError::FileNotFound(path) => (
55 StatusCode::NOT_FOUND,
56 "file_not_found",
57 format!("file not found: {path}"),
58 ),
59 LagoError::InvalidArgument(msg) => {
60 (StatusCode::BAD_REQUEST, "invalid_argument", msg.clone())
61 }
62 LagoError::SequenceConflict { expected, actual } => (
63 StatusCode::CONFLICT,
64 "sequence_conflict",
65 format!("sequence conflict: expected {expected}, got {actual}"),
66 ),
67 LagoError::PolicyDenied(msg) => {
68 (StatusCode::FORBIDDEN, "policy_denied", msg.clone())
69 }
70 LagoError::Serialization(e) => (
71 StatusCode::BAD_REQUEST,
72 "serialization_error",
73 format!("serialization error: {e}"),
74 ),
75 LagoError::HashLine(e) => (
76 StatusCode::BAD_REQUEST,
77 "hashline_error",
78 format!("hashline error: {e}"),
79 ),
80 LagoError::Sandbox(msg) => (
81 StatusCode::BAD_REQUEST,
82 "sandbox_error",
83 format!("sandbox error: {msg}"),
84 ),
85 _ => (
86 StatusCode::INTERNAL_SERVER_ERROR,
87 "internal_error",
88 format!("internal error: {e}"),
89 ),
90 },
91 ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg.clone()),
92 ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, "not_found", msg.clone()),
93 ApiError::Forbidden(msg) => (StatusCode::FORBIDDEN, "forbidden", msg.clone()),
94 ApiError::Conflict(msg) => (StatusCode::CONFLICT, "conflict", msg.clone()),
95 ApiError::Internal(msg) => (
96 StatusCode::INTERNAL_SERVER_ERROR,
97 "internal_error",
98 msg.clone(),
99 ),
100 };
101
102 let body = ErrorBody {
103 error: error_type.to_string(),
104 message,
105 };
106
107 (status, axum::Json(body)).into_response()
108 }
109}
110
111impl From<LagoError> for ApiError {
112 fn from(e: LagoError) -> Self {
113 ApiError::Lago(e)
114 }
115}
116
117impl From<serde_json::Error> for ApiError {
118 fn from(e: serde_json::Error) -> Self {
119 ApiError::BadRequest(format!("invalid JSON: {e}"))
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use axum::response::IntoResponse;
127
128 #[test]
129 fn api_error_from_lago_session_not_found() {
130 let e: ApiError = LagoError::SessionNotFound("S1".into()).into();
131 let resp = e.into_response();
132 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
133 }
134
135 #[test]
136 fn api_error_from_lago_branch_not_found() {
137 let e: ApiError = LagoError::BranchNotFound("B1".into()).into();
138 let resp = e.into_response();
139 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
140 }
141
142 #[test]
143 fn api_error_from_lago_policy_denied() {
144 let e: ApiError = LagoError::PolicyDenied("blocked".into()).into();
145 let resp = e.into_response();
146 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
147 }
148
149 #[test]
150 fn api_error_from_lago_sequence_conflict() {
151 let e: ApiError = LagoError::SequenceConflict {
152 expected: 5,
153 actual: 3,
154 }
155 .into();
156 let resp = e.into_response();
157 assert_eq!(resp.status(), StatusCode::CONFLICT);
158 }
159
160 #[test]
161 fn api_error_from_lago_invalid_argument() {
162 let e: ApiError = LagoError::InvalidArgument("bad input".into()).into();
163 let resp = e.into_response();
164 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
165 }
166
167 #[test]
168 fn api_error_from_lago_internal() {
169 let e: ApiError = LagoError::Journal("disk error".into()).into();
170 let resp = e.into_response();
171 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
172 }
173
174 #[test]
175 fn api_error_bad_request() {
176 let e = ApiError::BadRequest("bad".into());
177 let resp = e.into_response();
178 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
179 }
180
181 #[test]
182 fn api_error_not_found() {
183 let e = ApiError::NotFound("missing".into());
184 let resp = e.into_response();
185 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
186 }
187
188 #[test]
189 fn api_error_forbidden() {
190 let e = ApiError::Forbidden("policy denied".into());
191 let resp = e.into_response();
192 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
193 }
194
195 #[test]
196 fn api_error_conflict() {
197 let e = ApiError::Conflict("merge conflict".into());
198 let resp = e.into_response();
199 assert_eq!(resp.status(), StatusCode::CONFLICT);
200 }
201
202 #[test]
203 fn api_error_internal() {
204 let e = ApiError::Internal("oops".into());
205 let resp = e.into_response();
206 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
207 }
208
209 #[test]
210 fn api_error_from_serde_json() {
211 let err: Result<serde_json::Value, _> = serde_json::from_str("{bad");
212 let e: ApiError = err.unwrap_err().into();
213 match e {
214 ApiError::BadRequest(msg) => assert!(msg.contains("invalid JSON")),
215 _ => panic!("expected BadRequest"),
216 }
217 }
218
219 #[test]
220 fn api_error_from_lago_hashline() {
221 let hl_err = lago_core::hashline::HashLineError::LineOutOfBounds {
222 line_num: 10,
223 total_lines: 5,
224 };
225 let e: ApiError = LagoError::HashLine(hl_err).into();
226 let resp = e.into_response();
227 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
228 }
229
230 #[test]
231 fn api_error_from_lago_sandbox() {
232 let e: ApiError = LagoError::Sandbox("container failed".into()).into();
233 let resp = e.into_response();
234 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
235 }
236}