Skip to content
//! Typed API errors and their HTTP representation.

use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use twice_core::{ApiErrorBody, Host};

/// Everything a handler can fail with.
#[derive(Debug, thiserror::Error)]
pub(crate) enum ApiError {
    /// The `x-api-key` header was missing, malformed, or wrong.
    #[error("missing or invalid API key")]
    Unauthorized,
    /// The requested host is not a known application.
    #[error("no application known for host {0}")]
    UnknownApp(Host),
    /// The `{host}` path segment was not a valid hostname.
    #[error("invalid host {0:?}: {1}")]
    BadHost(String, twice_core::InvalidHost),
    /// The referenced job does not exist.
    #[error("no such job")]
    UnknownJob,
    /// Another job is already running against the same application.
    #[error("a {0} job is already running for this application")]
    JobInFlight(&'static str),
    /// The Docker daemon could not be queried.
    #[error("docker daemon error: {0}")]
    Docker(#[from] bollard::errors::Error),
    /// The `once` binary could not be spawned.
    #[error("could not run the once binary: {0}")]
    Spawn(#[from] std::io::Error),
}

impl ApiError {
    const fn parts(&self) -> (StatusCode, &'static str) {
        match self {
            Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized"),
            Self::UnknownApp(_) => (StatusCode::NOT_FOUND, "unknown_app"),
            Self::BadHost(_, _) => (StatusCode::BAD_REQUEST, "invalid_host"),
            Self::UnknownJob => (StatusCode::NOT_FOUND, "unknown_job"),
            Self::JobInFlight(_) => (StatusCode::CONFLICT, "job_in_flight"),
            Self::Docker(_) => (StatusCode::BAD_GATEWAY, "docker_unavailable"),
            Self::Spawn(_) => (StatusCode::INTERNAL_SERVER_ERROR, "spawn_failed"),
        }
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, code) = self.parts();
        let body = ApiErrorBody {
            error: code.to_owned(),
            message: self.to_string(),
        };
        (status, Json(body)).into_response()
    }
}