1use std::any::Any;
16
17use axum::{Json, response::IntoResponse};
18use rsketch_error::{ErrorExt, StackError, StatusCode};
19use serde::Serialize;
20use snafu::Snafu;
21use strum::EnumProperty;
22
23#[derive(Debug, Serialize)]
24pub struct ErrorBody {
25 pub code: StatusCode,
26 pub message: String,
27}
28
29#[derive(Debug, Snafu, strum_macros::EnumProperty)]
30#[snafu(visibility(pub))]
31pub enum ApiError {
32 #[snafu(display("Invalid argument: {reason}"))]
33 #[strum(props(status_code = "invalid_argument"))]
34 InvalidArgument { reason: String },
35
36 #[snafu(display("Not found: {resource}"))]
37 #[strum(props(status_code = "not_found"))]
38 NotFound { resource: String },
39
40 #[snafu(display("Unauthorized"))]
41 #[strum(props(status_code = "unauthorized"))]
42 Unauthorized,
43
44 #[snafu(display("Forbidden"))]
45 #[strum(props(status_code = "forbidden"))]
46 Forbidden,
47
48 #[snafu(display("Conflict: {reason}"))]
49 #[strum(props(status_code = "conflict"))]
50 Conflict { reason: String },
51
52 #[snafu(display("Internal error"))]
53 #[strum(props(status_code = "internal"))]
54 Internal,
55}
56
57impl ErrorExt for ApiError {
58 fn status_code(&self) -> StatusCode {
59 self.get_str("status_code")
60 .and_then(|value| value.parse().ok())
61 .unwrap_or(StatusCode::Unknown)
62 }
63
64 fn as_any(&self) -> &dyn Any { self as _ }
65}
66
67impl StackError for ApiError {
68 fn debug_fmt(&self, layer: usize, buf: &mut Vec<String>) {
69 buf.push(format!("{layer}: {self}"));
70 }
71
72 fn next(&self) -> Option<&dyn StackError> { None }
73}
74
75impl IntoResponse for ApiError {
76 fn into_response(self) -> axum::response::Response {
77 let body = Json(ErrorBody {
78 code: self.status_code(),
79 message: self.output_msg(),
80 });
81 (self.status_code().http_status(), body).into_response()
82 }
83}
84
85impl From<ApiError> for tonic::Status {
86 fn from(error: ApiError) -> Self {
87 Self::new(error.status_code().tonic_code(), error.output_msg())
88 }
89}
90
91pub type ApiResult<T> = std::result::Result<T, ApiError>;