Skip to main content

kindling_server/
error.rs

1//! Daemon error types and HTTP error mapping.
2
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use axum::Json;
6use kindling_service::ServiceError;
7use serde_json::json;
8
9/// Top-level daemon error.
10///
11/// Returned by [`serve`](crate::serve) for lifecycle failures (binding the
12/// socket, PID acquisition, IO). Per-request failures use
13/// [`ApiError`] instead so they can map to HTTP status codes.
14#[derive(Debug, thiserror::Error)]
15pub enum ServerError {
16    /// Another live daemon already holds the PID lock.
17    #[error("a kindling daemon is already running (pid {0})")]
18    AlreadyRunning(i32),
19
20    /// Failed to read/parse/write the PID file.
21    #[error("pid file error: {0}")]
22    Pid(String),
23
24    /// Socket bind / IO failure.
25    #[error(transparent)]
26    Io(#[from] std::io::Error),
27
28    /// A service/store failure surfaced during startup.
29    #[error(transparent)]
30    Service(#[from] ServiceError),
31}
32
33/// Per-request error mapped to an HTTP status + JSON body `{ "error": "…" }`.
34#[derive(Debug)]
35pub enum ApiError {
36    /// 400 — malformed request, missing project header, or validation failure.
37    BadRequest(String),
38    /// 404 — referenced entity does not exist.
39    NotFound(String),
40    /// 409 — lifecycle conflict (duplicate open / already closed).
41    Conflict(String),
42    /// 500 — store or other internal failure.
43    Internal(String),
44}
45
46impl ApiError {
47    fn parts(&self) -> (StatusCode, &str) {
48        match self {
49            ApiError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
50            ApiError::NotFound(m) => (StatusCode::NOT_FOUND, m),
51            ApiError::Conflict(m) => (StatusCode::CONFLICT, m),
52            ApiError::Internal(m) => (StatusCode::INTERNAL_SERVER_ERROR, m),
53        }
54    }
55}
56
57impl From<ServiceError> for ApiError {
58    fn from(err: ServiceError) -> Self {
59        match err {
60            ServiceError::Validation(_) => ApiError::BadRequest(err.to_string()),
61            ServiceError::NotFound(_) => ApiError::NotFound(err.to_string()),
62            ServiceError::Conflict(_) | ServiceError::AlreadyClosed(_) => {
63                ApiError::Conflict(err.to_string())
64            }
65            ServiceError::Store(_) | ServiceError::Provider(_) | ServiceError::Json(_) => {
66                ApiError::Internal(err.to_string())
67            }
68        }
69    }
70}
71
72impl IntoResponse for ApiError {
73    fn into_response(self) -> Response {
74        let (status, message) = self.parts();
75        (status, Json(json!({ "error": message }))).into_response()
76    }
77}