use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DaemonError {
#[error("session not found: {id}")]
SessionNotFound {
id: String,
},
#[error("session not active: {id} (status: {status})")]
SessionNotActive {
id: String,
status: String,
},
#[error("overseer blocked: {reason}")]
OverseerBlocked {
reason: String,
},
#[error("tmux unavailable: {0}")]
TmuxUnavailable(String),
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("checkpoint not found: {id}")]
CheckpointNotFound {
id: String,
},
#[error("pair code invalid or expired")]
InvalidPairCode,
#[error("internal error: {0}")]
Internal(String),
#[error("service unavailable: {0}")]
ServiceUnavailable(String),
}
impl DaemonError {
pub fn status(&self) -> StatusCode {
match self {
Self::SessionNotFound { .. } | Self::CheckpointNotFound { .. } => StatusCode::NOT_FOUND,
Self::SessionNotActive { .. } => StatusCode::CONFLICT,
Self::OverseerBlocked { .. } => StatusCode::FORBIDDEN,
Self::InvalidRequest(_) | Self::InvalidPairCode => StatusCode::BAD_REQUEST,
Self::TmuxUnavailable(_) | Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
}
}
}
impl IntoResponse for DaemonError {
fn into_response(self) -> Response {
let status = self.status();
let body = Json(serde_json::json!({ "error": self.to_string() }));
(status, body).into_response()
}
}
impl From<std::io::Error> for DaemonError {
fn from(e: std::io::Error) -> Self {
Self::Internal(e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_status_codes_map() {
assert_eq!(
DaemonError::SessionNotFound { id: "x".into() }.status(),
StatusCode::NOT_FOUND
);
assert_eq!(
DaemonError::CheckpointNotFound { id: "x".into() }.status(),
StatusCode::NOT_FOUND
);
assert_eq!(
DaemonError::SessionNotActive {
id: "x".into(),
status: "stopped".into(),
}
.status(),
StatusCode::CONFLICT
);
assert_eq!(
DaemonError::OverseerBlocked { reason: "x".into() }.status(),
StatusCode::FORBIDDEN
);
assert_eq!(
DaemonError::InvalidRequest("x".into()).status(),
StatusCode::BAD_REQUEST
);
assert_eq!(
DaemonError::InvalidPairCode.status(),
StatusCode::BAD_REQUEST
);
assert_eq!(
DaemonError::TmuxUnavailable("x".into()).status(),
StatusCode::INTERNAL_SERVER_ERROR
);
assert_eq!(
DaemonError::Internal("x".into()).status(),
StatusCode::INTERNAL_SERVER_ERROR
);
assert_eq!(
DaemonError::ServiceUnavailable("x".into()).status(),
StatusCode::SERVICE_UNAVAILABLE
);
}
#[test]
fn error_messages_include_context() {
let e = DaemonError::SessionNotFound {
id: "tmpm-blue-fox".into(),
};
assert!(e.to_string().contains("tmpm-blue-fox"));
let e = DaemonError::SessionNotActive {
id: "abc".into(),
status: "stopped".into(),
};
assert!(e.to_string().contains("abc"));
assert!(e.to_string().contains("stopped"));
}
}