use axum::http::StatusCode;
use axum::response::IntoResponse;
use crate::session_manager::ManagedError;
#[derive(Debug, thiserror::Error)]
pub enum ResumeManagedError {
#[error("session not found: {0}")]
NotFound(String),
#[error("invalid state transition: {0}")]
InvalidState(String),
#[error("{0}")]
WorkspaceGone(String),
#[error("{0}")]
PaneGone(String),
#[error("{0}")]
Other(String),
}
impl From<ManagedError> for ResumeManagedError {
fn from(e: ManagedError) -> Self {
match e {
ManagedError::SessionNotFound(id) => ResumeManagedError::NotFound(id),
ManagedError::InvalidState(_, reason) => ResumeManagedError::InvalidState(reason),
e @ ManagedError::WorkspaceMissing(..) => {
ResumeManagedError::WorkspaceGone(e.to_string())
}
e @ ManagedError::PaneGone(..) => ResumeManagedError::PaneGone(e.to_string()),
other => ResumeManagedError::Other(other.to_string()),
}
}
}
fn unresumable_response(msg: String, reason: &'static str) -> axum::response::Response {
let mut resp = (StatusCode::UNPROCESSABLE_ENTITY, msg).into_response();
resp.headers_mut().insert(
axum::http::HeaderName::from_static("x-trusty-resume-reason"),
axum::http::HeaderValue::from_static(reason),
);
resp
}
impl IntoResponse for ResumeManagedError {
fn into_response(self) -> axum::response::Response {
match self {
ResumeManagedError::NotFound(id) => {
(StatusCode::NOT_FOUND, format!("session {id} not found")).into_response()
}
ResumeManagedError::InvalidState(reason) => {
(StatusCode::CONFLICT, reason).into_response()
}
ResumeManagedError::WorkspaceGone(msg) => {
unresumable_response(msg, "workspace_missing")
}
ResumeManagedError::PaneGone(msg) => unresumable_response(msg, "pane_gone"),
ResumeManagedError::Other(msg) => {
(StatusCode::INTERNAL_SERVER_ERROR, msg).into_response()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unresumable_response_tags_reason_header_per_failure_class() {
for (reason, msg) in [
(
"workspace_missing",
"workspace directory /gone no longer exists",
),
("pane_gone", "recorded pane %42 no longer exists"),
] {
let resp = unresumable_response(msg.to_string(), reason);
assert_eq!(
resp.status(),
StatusCode::UNPROCESSABLE_ENTITY,
"unresumable_response must always answer 422, got {:?} for reason {reason:?}",
resp.status()
);
let header = resp
.headers()
.get("x-trusty-resume-reason")
.and_then(|v| v.to_str().ok());
assert_eq!(
header,
Some(reason),
"x-trusty-resume-reason header must carry the exact reason passed in"
);
}
}
}