use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use ironflow_store::error::StoreError;
use serde::Serialize;
use serde_json::json;
use thiserror::Error;
use tracing::error;
use uuid::Uuid;
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Serialize)]
pub struct ErrorEnvelope {
pub code: String,
pub message: String,
}
#[derive(Debug, Error)]
pub enum ApiError {
#[error("run not found")]
RunNotFound(Uuid),
#[error("step not found")]
StepNotFound(Uuid),
#[error("workflow not found")]
WorkflowNotFound(String),
#[error("{0}")]
BadRequest(String),
#[error("authentication required")]
Unauthorized,
#[error("invalid credentials")]
InvalidCredentials,
#[error("email already exists")]
DuplicateEmail,
#[error("username already exists")]
DuplicateUsername,
#[error("API key not found")]
ApiKeyNotFound(Uuid),
#[error("user not found")]
UserNotFound(Uuid),
#[error("insufficient permissions")]
Forbidden,
#[error("insufficient scope")]
InsufficientScope,
#[error("database error")]
Store(#[from] StoreError),
#[error("internal server error")]
Internal(String),
}
impl ApiError {
fn code(&self) -> &str {
match self {
ApiError::RunNotFound(_) => "RUN_NOT_FOUND",
ApiError::StepNotFound(_) => "STEP_NOT_FOUND",
ApiError::WorkflowNotFound(_) => "WORKFLOW_NOT_FOUND",
ApiError::BadRequest(_) => "BAD_REQUEST",
ApiError::Unauthorized => "UNAUTHORIZED",
ApiError::InvalidCredentials => "INVALID_CREDENTIALS",
ApiError::DuplicateEmail => "DUPLICATE_EMAIL",
ApiError::DuplicateUsername => "DUPLICATE_USERNAME",
ApiError::ApiKeyNotFound(_) => "API_KEY_NOT_FOUND",
ApiError::UserNotFound(_) => "USER_NOT_FOUND",
ApiError::Forbidden => "FORBIDDEN",
ApiError::InsufficientScope => "INSUFFICIENT_SCOPE",
ApiError::Store(_) => "DATABASE_ERROR",
ApiError::Internal(_) => "INTERNAL_ERROR",
}
}
fn status(&self) -> StatusCode {
match self {
ApiError::RunNotFound(_) => StatusCode::NOT_FOUND,
ApiError::StepNotFound(_) => StatusCode::NOT_FOUND,
ApiError::WorkflowNotFound(_) => StatusCode::NOT_FOUND,
ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
ApiError::Unauthorized => StatusCode::UNAUTHORIZED,
ApiError::InvalidCredentials => StatusCode::UNAUTHORIZED,
ApiError::DuplicateEmail => StatusCode::CONFLICT,
ApiError::DuplicateUsername => StatusCode::CONFLICT,
ApiError::ApiKeyNotFound(_) => StatusCode::NOT_FOUND,
ApiError::UserNotFound(_) => StatusCode::NOT_FOUND,
ApiError::Forbidden => StatusCode::FORBIDDEN,
ApiError::InsufficientScope => StatusCode::FORBIDDEN,
ApiError::Store(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let status = self.status();
let code = self.code().to_string();
let message = self.to_string();
match &self {
ApiError::Store(e) => error!(error = %e, code = %code, "store error"),
ApiError::Internal(detail) => {
error!(detail = %detail, code = %code, "internal error")
}
_ => {}
}
let envelope = ErrorEnvelope { code, message };
(status, Json(json!({ "error": envelope }))).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_not_found_code() {
let err = ApiError::RunNotFound(Uuid::nil());
assert_eq!(err.code(), "RUN_NOT_FOUND");
}
#[test]
fn run_not_found_status() {
let err = ApiError::RunNotFound(Uuid::nil());
assert_eq!(err.status(), StatusCode::NOT_FOUND);
}
#[test]
fn bad_request_status() {
let err = ApiError::BadRequest("invalid field".to_string());
assert_eq!(err.status(), StatusCode::BAD_REQUEST);
assert_eq!(err.code(), "BAD_REQUEST");
}
#[test]
fn internal_error_status() {
let err = ApiError::Internal("something went wrong".to_string());
assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(err.code(), "INTERNAL_ERROR");
}
#[test]
fn error_to_response() {
let err = ApiError::BadRequest("invalid input".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn unauthorized_status() {
let err = ApiError::Unauthorized;
assert_eq!(err.status(), StatusCode::UNAUTHORIZED);
assert_eq!(err.code(), "UNAUTHORIZED");
}
#[test]
fn invalid_credentials_status() {
let err = ApiError::InvalidCredentials;
assert_eq!(err.status(), StatusCode::UNAUTHORIZED);
assert_eq!(err.code(), "INVALID_CREDENTIALS");
}
#[test]
fn duplicate_email_status() {
let err = ApiError::DuplicateEmail;
assert_eq!(err.status(), StatusCode::CONFLICT);
assert_eq!(err.code(), "DUPLICATE_EMAIL");
}
#[test]
fn duplicate_username_status() {
let err = ApiError::DuplicateUsername;
assert_eq!(err.status(), StatusCode::CONFLICT);
assert_eq!(err.code(), "DUPLICATE_USERNAME");
}
#[test]
fn workflow_not_found_status() {
let err = ApiError::WorkflowNotFound("test".to_string());
assert_eq!(err.status(), StatusCode::NOT_FOUND);
assert_eq!(err.code(), "WORKFLOW_NOT_FOUND");
}
#[test]
fn step_not_found_status() {
let err = ApiError::StepNotFound(Uuid::nil());
assert_eq!(err.status(), StatusCode::NOT_FOUND);
assert_eq!(err.code(), "STEP_NOT_FOUND");
}
#[test]
fn user_not_found_status() {
let err = ApiError::UserNotFound(Uuid::nil());
assert_eq!(err.status(), StatusCode::NOT_FOUND);
assert_eq!(err.code(), "USER_NOT_FOUND");
}
#[test]
fn forbidden_status() {
let err = ApiError::Forbidden;
assert_eq!(err.status(), StatusCode::FORBIDDEN);
assert_eq!(err.code(), "FORBIDDEN");
}
}