Skip to main content

lightshuttle_control/
error.rs

1//! JSON-shaped HTTP errors returned by every REST endpoint.
2
3use axum::Json;
4use axum::http::StatusCode;
5use axum::response::{IntoResponse, Response};
6use lightshuttle_runtime::LifecycleHandleError;
7use serde::Serialize;
8
9/// Wire representation of an API error.
10#[derive(Debug, Serialize)]
11pub struct ApiErrorBody {
12    /// Short, machine-friendly slug describing the error category.
13    pub error: String,
14    /// Resource name when the error is scoped to a single resource.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub resource: Option<String>,
17}
18
19/// Translates `LifecycleHandleError` variants into HTTP status + JSON
20/// body. Kept as a plain struct (not an enum) because every handler
21/// reduces failures to one of three buckets: not found, not yet
22/// implemented, or runtime fault.
23#[derive(Debug)]
24pub struct ApiError {
25    status: StatusCode,
26    body: ApiErrorBody,
27}
28
29impl ApiError {
30    /// 404 with `{"error":"unknown resource","resource":"<name>"}`.
31    #[must_use]
32    pub fn unknown_resource(name: impl Into<String>) -> Self {
33        Self {
34            status: StatusCode::NOT_FOUND,
35            body: ApiErrorBody {
36                error: "unknown resource".to_owned(),
37                resource: Some(name.into()),
38            },
39        }
40    }
41
42    /// 501 with `{"error":"not supported","resource":null}`.
43    #[must_use]
44    pub fn not_supported(op: &'static str) -> Self {
45        Self {
46            status: StatusCode::NOT_IMPLEMENTED,
47            body: ApiErrorBody {
48                error: format!("operation `{op}` is not supported yet"),
49                resource: None,
50            },
51        }
52    }
53
54    /// 500 with `{"error":"runtime error","resource":null}`.
55    #[must_use]
56    pub fn runtime(message: impl Into<String>) -> Self {
57        Self {
58            status: StatusCode::INTERNAL_SERVER_ERROR,
59            body: ApiErrorBody {
60                error: message.into(),
61                resource: None,
62            },
63        }
64    }
65}
66
67impl From<LifecycleHandleError> for ApiError {
68    fn from(err: LifecycleHandleError) -> Self {
69        match err {
70            LifecycleHandleError::UnknownResource(name) => Self::unknown_resource(name),
71            LifecycleHandleError::NotSupported(op) => Self::not_supported(op),
72            LifecycleHandleError::Runtime(e) => Self::runtime(e.to_string()),
73        }
74    }
75}
76
77impl IntoResponse for ApiError {
78    fn into_response(self) -> Response {
79        (self.status, Json(self.body)).into_response()
80    }
81}