1use axum::Json;
8use axum::http::StatusCode;
9use axum::response::{IntoResponse, Response};
10use ironflow_store::error::StoreError;
11use serde::Serialize;
12use serde_json::json;
13use thiserror::Error;
14use tracing::error;
15use uuid::Uuid;
16
17#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
21#[derive(Debug, Serialize)]
22pub struct ErrorEnvelope {
23 pub code: String,
25 pub message: String,
27}
28
29#[derive(Debug, Error)]
43pub enum ApiError {
44 #[error("run not found")]
46 RunNotFound(Uuid),
47
48 #[error("step not found")]
50 StepNotFound(Uuid),
51
52 #[error("workflow not found")]
54 WorkflowNotFound(String),
55
56 #[error("{0}")]
58 BadRequest(String),
59
60 #[error("authentication required")]
62 Unauthorized,
63
64 #[error("invalid credentials")]
66 InvalidCredentials,
67
68 #[error("email already exists")]
70 DuplicateEmail,
71
72 #[error("username already exists")]
74 DuplicateUsername,
75
76 #[error("API key not found")]
78 ApiKeyNotFound(Uuid),
79
80 #[error("user not found")]
82 UserNotFound(Uuid),
83
84 #[error("insufficient permissions")]
86 Forbidden,
87
88 #[error("insufficient scope")]
90 InsufficientScope,
91
92 #[error("database error")]
94 Store(#[from] StoreError),
95
96 #[error("internal server error")]
98 Internal(String),
99}
100
101impl ApiError {
102 fn code(&self) -> &str {
104 match self {
105 ApiError::RunNotFound(_) => "RUN_NOT_FOUND",
106 ApiError::StepNotFound(_) => "STEP_NOT_FOUND",
107 ApiError::WorkflowNotFound(_) => "WORKFLOW_NOT_FOUND",
108 ApiError::BadRequest(_) => "BAD_REQUEST",
109 ApiError::Unauthorized => "UNAUTHORIZED",
110 ApiError::InvalidCredentials => "INVALID_CREDENTIALS",
111 ApiError::DuplicateEmail => "DUPLICATE_EMAIL",
112 ApiError::DuplicateUsername => "DUPLICATE_USERNAME",
113 ApiError::ApiKeyNotFound(_) => "API_KEY_NOT_FOUND",
114 ApiError::UserNotFound(_) => "USER_NOT_FOUND",
115 ApiError::Forbidden => "FORBIDDEN",
116 ApiError::InsufficientScope => "INSUFFICIENT_SCOPE",
117 ApiError::Store(_) => "DATABASE_ERROR",
118 ApiError::Internal(_) => "INTERNAL_ERROR",
119 }
120 }
121
122 fn status(&self) -> StatusCode {
124 match self {
125 ApiError::RunNotFound(_) => StatusCode::NOT_FOUND,
126 ApiError::StepNotFound(_) => StatusCode::NOT_FOUND,
127 ApiError::WorkflowNotFound(_) => StatusCode::NOT_FOUND,
128 ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
129 ApiError::Unauthorized => StatusCode::UNAUTHORIZED,
130 ApiError::InvalidCredentials => StatusCode::UNAUTHORIZED,
131 ApiError::DuplicateEmail => StatusCode::CONFLICT,
132 ApiError::DuplicateUsername => StatusCode::CONFLICT,
133 ApiError::ApiKeyNotFound(_) => StatusCode::NOT_FOUND,
134 ApiError::UserNotFound(_) => StatusCode::NOT_FOUND,
135 ApiError::Forbidden => StatusCode::FORBIDDEN,
136 ApiError::InsufficientScope => StatusCode::FORBIDDEN,
137 ApiError::Store(_) => StatusCode::INTERNAL_SERVER_ERROR,
138 ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
139 }
140 }
141}
142
143impl IntoResponse for ApiError {
144 fn into_response(self) -> Response {
145 let status = self.status();
146 let code = self.code().to_string();
147 let message = self.to_string();
148
149 match &self {
151 ApiError::Store(e) => error!(error = %e, code = %code, "store error"),
152 ApiError::Internal(detail) => {
153 error!(detail = %detail, code = %code, "internal error")
154 }
155 _ => {}
156 }
157
158 let envelope = ErrorEnvelope { code, message };
159
160 (status, Json(json!({ "error": envelope }))).into_response()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn run_not_found_code() {
170 let err = ApiError::RunNotFound(Uuid::nil());
171 assert_eq!(err.code(), "RUN_NOT_FOUND");
172 }
173
174 #[test]
175 fn run_not_found_status() {
176 let err = ApiError::RunNotFound(Uuid::nil());
177 assert_eq!(err.status(), StatusCode::NOT_FOUND);
178 }
179
180 #[test]
181 fn bad_request_status() {
182 let err = ApiError::BadRequest("invalid field".to_string());
183 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
184 assert_eq!(err.code(), "BAD_REQUEST");
185 }
186
187 #[test]
188 fn internal_error_status() {
189 let err = ApiError::Internal("something went wrong".to_string());
190 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
191 assert_eq!(err.code(), "INTERNAL_ERROR");
192 }
193
194 #[test]
195 fn error_to_response() {
196 let err = ApiError::BadRequest("invalid input".to_string());
197 let response = err.into_response();
198 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
199 }
200
201 #[test]
202 fn unauthorized_status() {
203 let err = ApiError::Unauthorized;
204 assert_eq!(err.status(), StatusCode::UNAUTHORIZED);
205 assert_eq!(err.code(), "UNAUTHORIZED");
206 }
207
208 #[test]
209 fn invalid_credentials_status() {
210 let err = ApiError::InvalidCredentials;
211 assert_eq!(err.status(), StatusCode::UNAUTHORIZED);
212 assert_eq!(err.code(), "INVALID_CREDENTIALS");
213 }
214
215 #[test]
216 fn duplicate_email_status() {
217 let err = ApiError::DuplicateEmail;
218 assert_eq!(err.status(), StatusCode::CONFLICT);
219 assert_eq!(err.code(), "DUPLICATE_EMAIL");
220 }
221
222 #[test]
223 fn duplicate_username_status() {
224 let err = ApiError::DuplicateUsername;
225 assert_eq!(err.status(), StatusCode::CONFLICT);
226 assert_eq!(err.code(), "DUPLICATE_USERNAME");
227 }
228
229 #[test]
230 fn workflow_not_found_status() {
231 let err = ApiError::WorkflowNotFound("test".to_string());
232 assert_eq!(err.status(), StatusCode::NOT_FOUND);
233 assert_eq!(err.code(), "WORKFLOW_NOT_FOUND");
234 }
235
236 #[test]
237 fn step_not_found_status() {
238 let err = ApiError::StepNotFound(Uuid::nil());
239 assert_eq!(err.status(), StatusCode::NOT_FOUND);
240 assert_eq!(err.code(), "STEP_NOT_FOUND");
241 }
242
243 #[test]
244 fn user_not_found_status() {
245 let err = ApiError::UserNotFound(Uuid::nil());
246 assert_eq!(err.status(), StatusCode::NOT_FOUND);
247 assert_eq!(err.code(), "USER_NOT_FOUND");
248 }
249
250 #[test]
251 fn forbidden_status() {
252 let err = ApiError::Forbidden;
253 assert_eq!(err.status(), StatusCode::FORBIDDEN);
254 assert_eq!(err.code(), "FORBIDDEN");
255 }
256}