1use axum::http::StatusCode;
7use axum::response::{IntoResponse, Response};
8use axum::Json;
9use kranz_engine::error::EngineError;
10use serde::Serialize;
11use serde_json::json;
12use std::io::ErrorKind;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "snake_case")]
17pub enum ApiErrorCode {
18 MissionNotHosted,
19 TurnInFlight,
20 RepositoryBusy,
21 StalePlan,
22}
23
24#[derive(Debug)]
25pub struct ApiError {
26 pub status: StatusCode,
27 pub message: String,
28 pub code: Option<ApiErrorCode>,
29}
30
31impl ApiError {
32 pub fn not_found(message: impl Into<String>) -> Self {
33 Self {
34 status: StatusCode::NOT_FOUND,
35 message: message.into(),
36 code: None,
37 }
38 }
39
40 pub fn bad_request(message: impl Into<String>) -> Self {
41 Self {
42 status: StatusCode::BAD_REQUEST,
43 message: message.into(),
44 code: None,
45 }
46 }
47
48 pub fn conflict(message: impl Into<String>) -> Self {
49 Self {
50 status: StatusCode::CONFLICT,
51 message: message.into(),
52 code: None,
53 }
54 }
55
56 pub fn unauthorized(message: impl Into<String>) -> Self {
58 Self {
59 status: StatusCode::UNAUTHORIZED,
60 message: message.into(),
61 code: None,
62 }
63 }
64
65 pub fn forbidden(message: impl Into<String>) -> Self {
68 Self {
69 status: StatusCode::FORBIDDEN,
70 message: message.into(),
71 code: None,
72 }
73 }
74
75 pub fn internal(message: impl Into<String>) -> Self {
76 Self {
77 status: StatusCode::INTERNAL_SERVER_ERROR,
78 message: message.into(),
79 code: None,
80 }
81 }
82
83 pub fn unprocessable(message: impl Into<String>) -> Self {
86 Self {
87 status: StatusCode::UNPROCESSABLE_ENTITY,
88 message: message.into(),
89 code: None,
90 }
91 }
92
93 pub fn with_code(mut self, code: ApiErrorCode) -> Self {
94 self.code = Some(code);
95 self
96 }
97}
98
99impl IntoResponse for ApiError {
100 fn into_response(self) -> Response {
101 let mut body = json!({ "error": self.message });
102 if let Some(code) = self.code {
103 body["code"] = json!(code);
104 }
105 (self.status, Json(body)).into_response()
106 }
107}
108
109impl From<EngineError> for ApiError {
110 fn from(error: EngineError) -> Self {
111 match &error {
112 EngineError::Io(io) if io.kind() == ErrorKind::NotFound => {
113 ApiError::not_found(error.to_string())
114 }
115 EngineError::LockHeld(_) | EngineError::InvalidState(_) => {
119 ApiError::conflict(error.to_string())
120 }
121 EngineError::Config(_) => ApiError::bad_request(error.to_string()),
124 _ => ApiError::internal(error.to_string()),
125 }
126 }
127}