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#[derive(Debug, Serialize)]
21pub struct ErrorEnvelope {
22 pub code: String,
24 pub message: String,
26}
27
28#[derive(Debug, Error)]
42pub enum ApiError {
43 #[error("run not found")]
45 RunNotFound(Uuid),
46
47 #[error("step not found")]
49 StepNotFound(Uuid),
50
51 #[error("workflow not found")]
53 WorkflowNotFound(String),
54
55 #[error("{0}")]
57 BadRequest(String),
58
59 #[error("authentication required")]
61 Unauthorized,
62
63 #[error("invalid credentials")]
65 InvalidCredentials,
66
67 #[error("email already exists")]
69 DuplicateEmail,
70
71 #[error("username already exists")]
73 DuplicateUsername,
74
75 #[error("database error")]
77 Store(#[from] StoreError),
78
79 #[error("internal server error")]
81 Internal(String),
82}
83
84impl ApiError {
85 fn code(&self) -> &str {
87 match self {
88 ApiError::RunNotFound(_) => "RUN_NOT_FOUND",
89 ApiError::StepNotFound(_) => "STEP_NOT_FOUND",
90 ApiError::WorkflowNotFound(_) => "WORKFLOW_NOT_FOUND",
91 ApiError::BadRequest(_) => "BAD_REQUEST",
92 ApiError::Unauthorized => "UNAUTHORIZED",
93 ApiError::InvalidCredentials => "INVALID_CREDENTIALS",
94 ApiError::DuplicateEmail => "DUPLICATE_EMAIL",
95 ApiError::DuplicateUsername => "DUPLICATE_USERNAME",
96 ApiError::Store(_) => "DATABASE_ERROR",
97 ApiError::Internal(_) => "INTERNAL_ERROR",
98 }
99 }
100
101 fn status(&self) -> StatusCode {
103 match self {
104 ApiError::RunNotFound(_) => StatusCode::NOT_FOUND,
105 ApiError::StepNotFound(_) => StatusCode::NOT_FOUND,
106 ApiError::WorkflowNotFound(_) => StatusCode::NOT_FOUND,
107 ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
108 ApiError::Unauthorized => StatusCode::UNAUTHORIZED,
109 ApiError::InvalidCredentials => StatusCode::UNAUTHORIZED,
110 ApiError::DuplicateEmail => StatusCode::CONFLICT,
111 ApiError::DuplicateUsername => StatusCode::CONFLICT,
112 ApiError::Store(_) => StatusCode::INTERNAL_SERVER_ERROR,
113 ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
114 }
115 }
116}
117
118impl IntoResponse for ApiError {
119 fn into_response(self) -> Response {
120 let status = self.status();
121 let code = self.code().to_string();
122 let message = self.to_string();
123
124 match &self {
126 ApiError::Store(e) => error!(error = %e, code = %code, "store error"),
127 ApiError::Internal(detail) => {
128 error!(detail = %detail, code = %code, "internal error")
129 }
130 _ => {}
131 }
132
133 let envelope = ErrorEnvelope { code, message };
134
135 (status, Json(json!({ "error": envelope }))).into_response()
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn run_not_found_code() {
145 let err = ApiError::RunNotFound(Uuid::nil());
146 assert_eq!(err.code(), "RUN_NOT_FOUND");
147 }
148
149 #[test]
150 fn run_not_found_status() {
151 let err = ApiError::RunNotFound(Uuid::nil());
152 assert_eq!(err.status(), StatusCode::NOT_FOUND);
153 }
154
155 #[test]
156 fn bad_request_status() {
157 let err = ApiError::BadRequest("invalid field".to_string());
158 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
159 assert_eq!(err.code(), "BAD_REQUEST");
160 }
161
162 #[test]
163 fn internal_error_status() {
164 let err = ApiError::Internal("something went wrong".to_string());
165 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
166 assert_eq!(err.code(), "INTERNAL_ERROR");
167 }
168
169 #[test]
170 fn error_to_response() {
171 let err = ApiError::BadRequest("invalid input".to_string());
172 let response = err.into_response();
173 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
174 }
175
176 #[test]
177 fn unauthorized_status() {
178 let err = ApiError::Unauthorized;
179 assert_eq!(err.status(), StatusCode::UNAUTHORIZED);
180 assert_eq!(err.code(), "UNAUTHORIZED");
181 }
182
183 #[test]
184 fn invalid_credentials_status() {
185 let err = ApiError::InvalidCredentials;
186 assert_eq!(err.status(), StatusCode::UNAUTHORIZED);
187 assert_eq!(err.code(), "INVALID_CREDENTIALS");
188 }
189
190 #[test]
191 fn duplicate_email_status() {
192 let err = ApiError::DuplicateEmail;
193 assert_eq!(err.status(), StatusCode::CONFLICT);
194 assert_eq!(err.code(), "DUPLICATE_EMAIL");
195 }
196
197 #[test]
198 fn duplicate_username_status() {
199 let err = ApiError::DuplicateUsername;
200 assert_eq!(err.status(), StatusCode::CONFLICT);
201 assert_eq!(err.code(), "DUPLICATE_USERNAME");
202 }
203
204 #[test]
205 fn workflow_not_found_status() {
206 let err = ApiError::WorkflowNotFound("test".to_string());
207 assert_eq!(err.status(), StatusCode::NOT_FOUND);
208 assert_eq!(err.code(), "WORKFLOW_NOT_FOUND");
209 }
210
211 #[test]
212 fn step_not_found_status() {
213 let err = ApiError::StepNotFound(Uuid::nil());
214 assert_eq!(err.status(), StatusCode::NOT_FOUND);
215 assert_eq!(err.code(), "STEP_NOT_FOUND");
216 }
217}