use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::{Value, json};
#[derive(Debug)]
pub enum ApiError {
BadRequest(String),
Unauthorized,
UnknownRun(String),
UnknownAgent(String),
RunExists(String),
WrongState(String),
MissingDriveToken(String),
InvalidDriveToken(String),
UnsupportedEventKind(String),
Divergence(String),
PayloadTooLarge(String),
ModelExecutorUnavailable(String),
ModelExecution(String),
UnknownTool(String),
ToolRegistryUnavailable(String),
ToolExecution(String),
InvalidGraph {
message: String,
errors: Value,
},
UnknownGraph(String),
NotAGraphRun(String),
InvalidForkNode(String),
OriginNeedsReconciliation {
message: String,
intent: Value,
},
WriteReplayHazard {
message: String,
writes: Value,
},
NeedsReconciliation {
message: String,
intent: Value,
},
Internal(String),
}
impl ApiError {
fn status_and_code(&self) -> (StatusCode, &'static str) {
match self {
ApiError::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
ApiError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized"),
ApiError::UnknownRun(_) => (StatusCode::NOT_FOUND, "unknown_run"),
ApiError::UnknownAgent(_) => (StatusCode::NOT_FOUND, "unknown_agent"),
ApiError::RunExists(_) => (StatusCode::CONFLICT, "run_exists"),
ApiError::WrongState(_) => (StatusCode::CONFLICT, "wrong_state"),
ApiError::InvalidGraph { .. } => (StatusCode::BAD_REQUEST, "invalid_graph"),
ApiError::UnknownGraph(_) => (StatusCode::NOT_FOUND, "unknown_graph"),
ApiError::NotAGraphRun(_) => (StatusCode::CONFLICT, "not_a_graph_run"),
ApiError::InvalidForkNode(_) => (StatusCode::CONFLICT, "invalid_fork_node"),
ApiError::OriginNeedsReconciliation { .. } => {
(StatusCode::CONFLICT, "origin_needs_reconciliation")
}
ApiError::WriteReplayHazard { .. } => (StatusCode::CONFLICT, "write_replay_hazard"),
ApiError::NeedsReconciliation { .. } => (StatusCode::CONFLICT, "needs_reconciliation"),
ApiError::MissingDriveToken(_) => (StatusCode::UNAUTHORIZED, "missing_drive_token"),
ApiError::InvalidDriveToken(_) => (StatusCode::FORBIDDEN, "invalid_drive_token"),
ApiError::UnsupportedEventKind(_) => {
(StatusCode::UNPROCESSABLE_ENTITY, "unsupported_event_kind")
}
ApiError::Divergence(_) => (StatusCode::CONFLICT, "divergence"),
ApiError::PayloadTooLarge(_) => (StatusCode::PAYLOAD_TOO_LARGE, "payload_too_large"),
ApiError::ModelExecutorUnavailable(_) => (
StatusCode::SERVICE_UNAVAILABLE,
"model_executor_unavailable",
),
ApiError::ModelExecution(_) => (StatusCode::BAD_GATEWAY, "model_execution"),
ApiError::UnknownTool(_) => (StatusCode::NOT_FOUND, "unknown_tool"),
ApiError::ToolRegistryUnavailable(_) => {
(StatusCode::SERVICE_UNAVAILABLE, "tool_registry_unavailable")
}
ApiError::ToolExecution(_) => (StatusCode::BAD_GATEWAY, "tool_execution"),
ApiError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal"),
}
}
fn message(&self) -> String {
match self {
ApiError::BadRequest(m)
| ApiError::UnknownRun(m)
| ApiError::UnknownAgent(m)
| ApiError::RunExists(m)
| ApiError::WrongState(m)
| ApiError::Internal(m)
| ApiError::MissingDriveToken(m)
| ApiError::InvalidDriveToken(m)
| ApiError::UnsupportedEventKind(m)
| ApiError::Divergence(m)
| ApiError::PayloadTooLarge(m)
| ApiError::ModelExecutorUnavailable(m)
| ApiError::ModelExecution(m)
| ApiError::UnknownTool(m)
| ApiError::ToolRegistryUnavailable(m)
| ApiError::ToolExecution(m)
| ApiError::UnknownGraph(m)
| ApiError::NotAGraphRun(m)
| ApiError::InvalidForkNode(m)
| ApiError::InvalidGraph { message: m, .. }
| ApiError::OriginNeedsReconciliation { message: m, .. }
| ApiError::WriteReplayHazard { message: m, .. }
| ApiError::NeedsReconciliation { message: m, .. } => m.clone(),
ApiError::Unauthorized => "missing or invalid bearer token".to_owned(),
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, code) = self.status_and_code();
let message = self.message();
let mut error = json!({ "code": code, "message": message });
match self {
ApiError::NeedsReconciliation { intent, .. }
| ApiError::OriginNeedsReconciliation { intent, .. } => {
error["details"] = json!({ "intent": intent });
}
ApiError::WriteReplayHazard { writes, .. } => {
error["details"] = json!({ "writes": writes });
}
ApiError::InvalidGraph { errors, .. } => {
error["details"] = json!({ "errors": errors });
}
_ => {}
}
(status, Json(json!({ "error": error }))).into_response()
}
}