Skip to main content

open_chess_server/
error.rs

1//! API errors, in Lichess's shape: `{"error": "..."}` with a status code.
2
3use axum::Json;
4use axum::http::StatusCode;
5use axum::response::{IntoResponse, Response};
6use serde_json::json;
7use tracing::warn;
8
9#[derive(Debug)]
10pub struct ApiError {
11    pub status: StatusCode,
12    pub message: String,
13}
14
15pub type ApiResult<T> = Result<T, ApiError>;
16
17impl ApiError {
18    pub fn new(status: StatusCode, message: impl Into<String>) -> ApiError {
19        ApiError { status, message: message.into() }
20    }
21
22    pub fn bad(message: impl Into<String>) -> ApiError {
23        ApiError::new(StatusCode::BAD_REQUEST, message)
24    }
25
26    pub fn not_found(what: &str) -> ApiError {
27        ApiError::new(StatusCode::NOT_FOUND, format!("No such {what}"))
28    }
29
30    pub fn unauthorized() -> ApiError {
31        ApiError::new(StatusCode::UNAUTHORIZED, "No such token")
32    }
33
34    pub fn forbidden(message: impl Into<String>) -> ApiError {
35        ApiError::new(StatusCode::FORBIDDEN, message)
36    }
37
38    pub fn missing_scope(scopes: &[&str]) -> ApiError {
39        ApiError::forbidden(format!("Missing scope: {}", scopes.join(" or ")))
40    }
41
42    pub fn conflict(message: impl Into<String>) -> ApiError {
43        ApiError::new(StatusCode::CONFLICT, message)
44    }
45
46    pub fn too_many(message: impl Into<String>) -> ApiError {
47        ApiError::new(StatusCode::TOO_MANY_REQUESTS, message)
48    }
49
50    /// The Board/Bot API's answer to a move or action that can't happen now.
51    pub fn not_your_turn() -> ApiError {
52        ApiError::bad("Not your turn, or game already over")
53    }
54}
55
56impl From<anyhow::Error> for ApiError {
57    fn from(e: anyhow::Error) -> Self {
58        warn!("internal error: {e:#}");
59        ApiError::new(StatusCode::SERVICE_UNAVAILABLE, "temporarily unavailable, try again")
60    }
61}
62
63impl From<tokio_postgres::Error> for ApiError {
64    fn from(e: tokio_postgres::Error) -> Self {
65        ApiError::from(anyhow::Error::from(e))
66    }
67}
68
69impl IntoResponse for ApiError {
70    fn into_response(self) -> Response {
71        (self.status, Json(json!({ "error": self.message }))).into_response()
72    }
73}