rsketch_server/
error.rs

1// Copyright 2025 Crrow
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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>;