use crate::error::{CoreError, DatabaseError, Error};
use axum::response::{IntoResponse, Response};
use either::Either;
use nil_server_database::error::DieselError;
use std::ops::{ControlFlow, Try};
pub type MaybeResponse<L> = Either<L, Response>;
#[doc(hidden)]
#[macro_export]
macro_rules! res {
($status:ident) => {{
use axum::body::Body;
use axum::http::StatusCode;
use axum::response::Response;
let status = StatusCode::$status;
let body = if (status.is_client_error() || status.is_server_error())
&& let Some(reason) = status.canonical_reason()
{
Body::new(reason.to_string())
} else {
Body::empty()
};
Response::builder()
.status(status)
.body(body)
.unwrap()
}};
($status:ident, $data:expr) => {{
use axum::http::StatusCode;
use axum::response::IntoResponse;
(StatusCode::$status, $data).into_response()
}};
}
impl From<Error> for Response {
fn from(err: Error) -> Self {
from_err(err)
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
from_err(self)
}
}
pub(crate) fn from_err(err: impl Into<Error>) -> Response {
let err: Error = err.into();
tracing::error!(message = %err, error = ?err);
from_server_err(err)
}
#[expect(clippy::match_same_arms, clippy::needless_pass_by_value)]
fn from_core_err(err: CoreError) -> Response {
use CoreError::*;
let text = err.to_string();
match err {
ArmyNotFound(..) => res!(NOT_FOUND, text),
ArmyNotIdle(..) => res!(BAD_REQUEST, text),
BotAlreadySpawned(..) => res!(CONFLICT, text),
BotNotFound(..) => res!(NOT_FOUND, text),
BuildingStatsNotFound(..) => res!(NOT_FOUND, text),
BuildingStatsNotFoundForLevel(..) => res!(NOT_FOUND, text),
CannotDecreaseBuildingLevel(..) => res!(BAD_REQUEST, text),
CannotIncreaseBuildingLevel(..) => res!(BAD_REQUEST, text),
CheatingNotAllowed => res!(BAD_REQUEST, text),
CityNotFound(..) => res!(NOT_FOUND, text),
FailedToReadSavedata => res!(INTERNAL_SERVER_ERROR, text),
FailedToWriteSavedata => res!(INTERNAL_SERVER_ERROR, text),
Forbidden => res!(FORBIDDEN, text),
IndexOutOfBounds(..) => res!(BAD_REQUEST, text),
InsufficientResources => res!(BAD_REQUEST, text),
InsufficientUnits => res!(BAD_REQUEST, text),
ManeuverIsDone(..) => res!(INTERNAL_SERVER_ERROR, text),
ManeuverIsPending(..) => res!(INTERNAL_SERVER_ERROR, text),
ManeuverIsReturning(..) => res!(INTERNAL_SERVER_ERROR, text),
ManeuverNotFound(..) => res!(NOT_FOUND, text),
MineStatsNotFound(..) => res!(NOT_FOUND, text),
MineStatsNotFoundForLevel(..) => res!(NOT_FOUND, text),
NoPlayer => res!(BAD_REQUEST, text),
NotWaitingPlayer(..) => res!(BAD_REQUEST, text),
OriginIsDestination(..) => res!(BAD_REQUEST, text),
PlayerAlreadySpawned(..) => res!(CONFLICT, text),
PlayerNotFound(..) => res!(NOT_FOUND, text),
PrecursorNotFound(..) => res!(NOT_FOUND, text),
ReportNotFound(..) => res!(NOT_FOUND, text),
RoundAlreadyStarted => res!(CONFLICT, text),
RoundHasPendingPlayers => res!(BAD_REQUEST, text),
RoundNotStarted => res!(BAD_REQUEST, text),
StorageStatsNotFound(..) => res!(NOT_FOUND, text),
StorageStatsNotFoundForLevel(..) => res!(NOT_FOUND, text),
UnexpectedUnit(..) => res!(INTERNAL_SERVER_ERROR, text),
WallStatsNotFoundForLevel(..) => res!(NOT_FOUND, text),
WorldIsFull => res!(INTERNAL_SERVER_ERROR, text),
}
}
#[expect(clippy::match_same_arms)]
fn from_database_err(err: DatabaseError) -> Response {
use DatabaseError::*;
match err {
Core(err) => from_core_err(err),
Diesel(err) => from_diesel_err(&err),
DieselConnection(..) => res!(INTERNAL_SERVER_ERROR),
GameNotFound(..) => res!(NOT_FOUND, err.to_string()),
InvalidPassword => res!(BAD_REQUEST, err.to_string()),
InvalidUsername(..) => res!(BAD_REQUEST, err.to_string()),
Io(..) => res!(INTERNAL_SERVER_ERROR),
Jiff(..) => res!(INTERNAL_SERVER_ERROR),
UserAlreadyExists(..) => res!(CONFLICT, err.to_string()),
UserNotFound(..) => res!(NOT_FOUND, err.to_string()),
Unknown(..) => res!(INTERNAL_SERVER_ERROR),
}
}
fn from_diesel_err(err: &DieselError) -> Response {
if let DieselError::NotFound = &err {
res!(NOT_FOUND)
} else {
res!(INTERNAL_SERVER_ERROR)
}
}
#[expect(clippy::match_same_arms)]
fn from_server_err(err: Error) -> Response {
use Error::*;
match err {
Core(err) => from_core_err(err),
Database(err) => from_database_err(err),
IncorrectUserCredentials => res!(UNAUTHORIZED, err.to_string()),
IncorrectWorldCredentials(..) => res!(UNAUTHORIZED, err.to_string()),
Io(..) => res!(INTERNAL_SERVER_ERROR),
MissingPassword => res!(BAD_REQUEST, err.to_string()),
Unknown(..) => res!(INTERNAL_SERVER_ERROR),
WorldLimitReached => res!(INTERNAL_SERVER_ERROR),
WorldNotFound(..) => res!(NOT_FOUND, err.to_string()),
}
}
pub trait EitherExt<L, R> {
fn try_map_left<T, E, F>(self, f: F) -> Either<Response, R>
where
Self: Sized,
L: Try<Output = T, Residual = E>,
E: Into<Error>,
F: FnOnce(T) -> Response;
}
impl<L, R> EitherExt<L, R> for Either<L, R> {
fn try_map_left<T, E, F>(self, f: F) -> Either<Response, R>
where
Self: Sized,
L: Try<Output = T, Residual = E>,
E: Into<Error>,
F: FnOnce(T) -> Response,
{
match self {
Self::Left(left) => {
match left.branch() {
ControlFlow::Continue(value) => Either::Left(f(value)),
ControlFlow::Break(err) => Either::Left(from_err(err)),
}
}
Self::Right(right) => Either::Right(right),
}
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! bail_if_player_is_not_pending {
($world:expr, $player:expr) => {
if !$world.round().is_waiting_player($player) {
use nil_core::error::Error;
let err = Error::NotWaitingPlayer($player.clone());
return $crate::response::from_err(err);
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! bail_if_not_player {
($current_player:expr, $player:expr) => {
if $current_player != $player {
return $crate::res!(FORBIDDEN);
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! bail_if_city_is_not_owned_by {
($world:expr, $player:expr, $coord:expr) => {
if !$world
.city($coord)?
.is_owned_by_player_and(|id| $player == id)
{
return $crate::res!(FORBIDDEN);
}
};
}