1use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum ApiError {
7 #[error("invalid request: {0}")]
8 InvalidRequest(String),
9
10 #[error("task not found: {0}")]
11 TaskNotFound(String),
12
13 #[error("payload too large: {0}")]
14 PayloadTooLarge(String),
15
16 #[error("internal error: {0}")]
17 Internal(String),
18
19 #[error("core error: {0}")]
20 Core(#[from] solti_core::CoreError),
21}
22
23impl ApiError {
24 pub fn as_label(&self) -> &'static str {
29 match self {
30 ApiError::Core(solti_core::CoreError::InvalidSpec(_)) => "InvalidRequest",
31 ApiError::PayloadTooLarge(_) => "PayloadTooLarge",
32 ApiError::InvalidRequest(_) => "InvalidRequest",
33 ApiError::TaskNotFound(_) => "TaskNotFound",
34 ApiError::Internal(_) => "Internal",
35 ApiError::Core(_) => "Internal",
36 }
37 }
38}
39
40#[cfg(feature = "grpc")]
41impl From<ApiError> for tonic::Status {
42 fn from(err: ApiError) -> Self {
43 match err {
44 ApiError::PayloadTooLarge(msg) => tonic::Status::resource_exhausted(msg),
45 ApiError::InvalidRequest(msg) => tonic::Status::invalid_argument(msg),
46 ApiError::TaskNotFound(msg) => tonic::Status::not_found(msg),
47 ApiError::Internal(msg) => tonic::Status::internal(msg),
48 ApiError::Core(e) => core_to_status(e),
49 }
50 }
51}
52
53#[cfg(feature = "grpc")]
54fn core_to_status(e: solti_core::CoreError) -> tonic::Status {
55 use solti_core::CoreError;
56 match e {
57 CoreError::InvalidSpec(inner) => tonic::Status::invalid_argument(inner.to_string()),
58 CoreError::Supervisor(_) | CoreError::Mapping(_) | CoreError::Runner(_) => {
59 tonic::Status::internal(e.to_string())
60 }
61 }
62}
63
64#[cfg(feature = "http")]
65impl axum::response::IntoResponse for ApiError {
66 fn into_response(self) -> axum::response::Response {
67 use axum::http::StatusCode;
68
69 let label = self.as_label();
70 let (status, message) = match self {
71 ApiError::InvalidRequest(msg) => (StatusCode::BAD_REQUEST, msg),
72 ApiError::TaskNotFound(msg) => (StatusCode::NOT_FOUND, msg),
73 ApiError::PayloadTooLarge(msg) => (StatusCode::PAYLOAD_TOO_LARGE, msg),
74 ApiError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
75 ApiError::Core(e) => core_to_http_status(e),
76 };
77
78 let body = serde_json::json!({ "error": label, "message": message });
79 (status, axum::Json(body)).into_response()
80 }
81}
82
83#[cfg(feature = "http")]
84fn core_to_http_status(e: solti_core::CoreError) -> (axum::http::StatusCode, String) {
85 use axum::http::StatusCode;
86 use solti_core::CoreError;
87 match e {
88 CoreError::InvalidSpec(inner) => (StatusCode::BAD_REQUEST, inner.to_string()),
89 CoreError::Supervisor(_) | CoreError::Mapping(_) | CoreError::Runner(_) => {
90 (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn as_label_covers_all_direct_variants() {
101 assert_eq!(
102 ApiError::InvalidRequest("x".into()).as_label(),
103 "InvalidRequest"
104 );
105 assert_eq!(
106 ApiError::TaskNotFound("x".into()).as_label(),
107 "TaskNotFound"
108 );
109 assert_eq!(ApiError::Internal("x".into()).as_label(), "Internal");
110 }
111
112 #[test]
113 fn as_label_flattens_core_invalid_spec_to_invalid_request() {
114 let inner = solti_model::ModelError::Invalid("bad".into());
115 let e = ApiError::Core(solti_core::CoreError::InvalidSpec(inner));
116 assert_eq!(e.as_label(), "InvalidRequest");
117 }
118}