1use crate::error::{CoreError, DatabaseError, Error};
5use axum::response::{IntoResponse, Response};
6use either::Either;
7use nil_server_database::error::DieselError;
8use std::ops::{ControlFlow, Try};
9
10pub type MaybeResponse<L> = Either<L, Response>;
11
12#[doc(hidden)]
13#[macro_export]
14macro_rules! res {
15 ($status:ident) => {{
16 use axum::body::Body;
17 use axum::http::StatusCode;
18 use axum::response::Response;
19
20 let status = StatusCode::$status;
21 let body = if (status.is_client_error() || status.is_server_error())
22 && let Some(reason) = status.canonical_reason()
23 {
24 Body::new(reason.to_string())
25 } else {
26 Body::empty()
27 };
28
29 Response::builder()
30 .status(status)
31 .body(body)
32 .unwrap()
33 }};
34 ($status:ident, $data:expr) => {{
35 use axum::http::StatusCode;
36 use axum::response::IntoResponse;
37
38 (StatusCode::$status, $data).into_response()
39 }};
40}
41
42impl From<Error> for Response {
43 fn from(err: Error) -> Self {
44 from_err(err)
45 }
46}
47
48impl IntoResponse for Error {
49 fn into_response(self) -> Response {
50 from_err(self)
51 }
52}
53
54pub(crate) fn from_err(err: impl Into<Error>) -> Response {
55 let err: Error = err.into();
56 tracing::error!(message = %err, error = ?err);
57 from_server_err(err)
58}
59
60#[expect(clippy::match_same_arms, clippy::needless_pass_by_value)]
61fn from_core_err(err: CoreError) -> Response {
62 use CoreError::*;
63
64 let text = err.to_string();
65 match err {
66 ArmyNotFound(..) => res!(NOT_FOUND, text),
67 ArmyNotIdle(..) => res!(BAD_REQUEST, text),
68 BotAlreadySpawned(..) => res!(CONFLICT, text),
69 BotNotFound(..) => res!(NOT_FOUND, text),
70 BuildingStatsNotFound(..) => res!(NOT_FOUND, text),
71 BuildingStatsNotFoundForLevel(..) => res!(NOT_FOUND, text),
72 CannotDecreaseBuildingLevel(..) => res!(BAD_REQUEST, text),
73 CannotIncreaseBuildingLevel(..) => res!(BAD_REQUEST, text),
74 CheatingNotAllowed => res!(BAD_REQUEST, text),
75 CityNotFound(..) => res!(NOT_FOUND, text),
76 FailedToDeserializeEvent => res!(INTERNAL_SERVER_ERROR, text),
77 FailedToReadSavedata => res!(INTERNAL_SERVER_ERROR, text),
78 FailedToSerializeEvent => res!(INTERNAL_SERVER_ERROR, text),
79 FailedToWriteSavedata => res!(INTERNAL_SERVER_ERROR, text),
80 FieldNotEmpty(..) => res!(BAD_REQUEST, text),
81 Forbidden => res!(FORBIDDEN, text),
82 IndexOutOfBounds(..) => res!(BAD_REQUEST, text),
83 InsufficientGold => res!(BAD_REQUEST, text),
84 InsufficientResources => res!(BAD_REQUEST, text),
85 InsufficientUnits => res!(BAD_REQUEST, text),
86 ManeuverIsDone(..) => res!(BAD_REQUEST, text),
87 ManeuverIsPending(..) => res!(BAD_REQUEST, text),
88 ManeuverIsReturning(..) => res!(BAD_REQUEST, text),
89 ManeuverNotFound(..) => res!(NOT_FOUND, text),
90 MineStatsNotFound(..) => res!(NOT_FOUND, text),
91 MineStatsNotFoundForLevel(..) => res!(NOT_FOUND, text),
92 NotEnoughResourcesInMarketVault => res!(BAD_REQUEST, text),
93 NotWaitingPlayer(..) => res!(BAD_REQUEST, text),
94 OriginIsDestination(..) => res!(BAD_REQUEST, text),
95 PlayerAlreadySpawned(..) => res!(CONFLICT, text),
96 PlayerNotFound(..) => res!(NOT_FOUND, text),
97 PrecursorNotFound(..) => res!(NOT_FOUND, text),
98 ReportNotFound(..) => res!(NOT_FOUND, text),
99 ResourceReceiverIsSender(..) => res!(BAD_REQUEST, text),
100 RoundAlreadyStarted => res!(CONFLICT, text),
101 RoundHasPendingPlayers => res!(BAD_REQUEST, text),
102 RoundNotStarted => res!(BAD_REQUEST, text),
103 StorageStatsNotFound(..) => res!(NOT_FOUND, text),
104 StorageStatsNotFoundForLevel(..) => res!(NOT_FOUND, text),
105 TooManyResources(..) => res!(BAD_REQUEST, text),
106 UnexpectedUnit(..) => res!(BAD_REQUEST, text),
107 WallStatsNotFoundForLevel(..) => res!(NOT_FOUND, text),
108 WorldIsFull => res!(FORBIDDEN, text),
109 }
110}
111
112#[expect(clippy::match_same_arms)]
113fn from_database_err(err: DatabaseError) -> Response {
114 use DatabaseError::*;
115
116 match err {
117 Core(err) => from_core_err(err),
118 Deadpool(..) => res!(INTERNAL_SERVER_ERROR),
119 DeadpoolBuild(..) => res!(INTERNAL_SERVER_ERROR),
120 Diesel(err) => from_diesel_err(&err),
121 DieselConnection(..) => res!(INTERNAL_SERVER_ERROR),
122 GameNotFound(..) => res!(NOT_FOUND, err.to_string()),
123 InvalidPassword => res!(BAD_REQUEST, err.to_string()),
124 InvalidUsername(..) => res!(BAD_REQUEST, err.to_string()),
125 Io(..) => res!(INTERNAL_SERVER_ERROR),
126 Jiff(..) => res!(INTERNAL_SERVER_ERROR),
127 MigrationFailed(..) => res!(INTERNAL_SERVER_ERROR),
128 UserAlreadyExists(..) => res!(CONFLICT, err.to_string()),
129 UserNotFound(..) => res!(NOT_FOUND, err.to_string()),
130 Unknown(..) => res!(INTERNAL_SERVER_ERROR),
131 }
132}
133
134fn from_diesel_err(err: &DieselError) -> Response {
135 if let DieselError::NotFound = &err {
136 res!(NOT_FOUND)
137 } else {
138 res!(INTERNAL_SERVER_ERROR)
139 }
140}
141
142#[expect(clippy::match_same_arms)]
143fn from_server_err(err: Error) -> Response {
144 use Error::*;
145
146 match err {
147 Core(err) => from_core_err(err),
148 Database(err) => from_database_err(err),
149 IncorrectUserCredentials => res!(UNAUTHORIZED, err.to_string()),
150 IncorrectWorldCredentials(..) => res!(UNAUTHORIZED, err.to_string()),
151 Io(..) => res!(INTERNAL_SERVER_ERROR),
152 MaxCharactersExceeded { .. } => res!(BAD_REQUEST, err.to_string()),
153 MissingPassword => res!(BAD_REQUEST, err.to_string()),
154 Unknown(..) => res!(INTERNAL_SERVER_ERROR),
155 WorldLimitReached => res!(FORBIDDEN, err.to_string()),
156 WorldNotFound(..) => res!(NOT_FOUND, err.to_string()),
157 }
158}
159
160pub trait EitherExt<L, R> {
161 fn try_map_left<T, E, F>(self, f: F) -> Either<Response, R>
162 where
163 Self: Sized,
164 L: Try<Output = T, Residual = E>,
165 E: Into<Error>,
166 F: FnOnce(T) -> Response;
167}
168
169impl<L, R> EitherExt<L, R> for Either<L, R> {
170 fn try_map_left<T, E, F>(self, f: F) -> Either<Response, R>
171 where
172 Self: Sized,
173 L: Try<Output = T, Residual = E>,
174 E: Into<Error>,
175 F: FnOnce(T) -> Response,
176 {
177 match self {
178 Self::Left(left) => {
179 match left.branch() {
180 ControlFlow::Continue(value) => Either::Left(f(value)),
181 ControlFlow::Break(err) => Either::Left(from_err(err)),
182 }
183 }
184 Self::Right(right) => Either::Right(right),
185 }
186 }
187}
188
189#[doc(hidden)]
190#[macro_export]
191macro_rules! bail_if_city_is_not_owned_by {
192 ($world:expr, $player:expr, $coord:expr) => {
193 if !$world
194 .city($coord)?
195 .is_owned_by_player_and(|id| $player == id)
196 {
197 return $crate::res!(FORBIDDEN);
198 }
199 };
200}
201
202#[doc(hidden)]
203#[macro_export]
204macro_rules! bail_if_max_chars_exceeded {
205 ($value:expr, $max:expr) => {
206 let current = $value.chars().count();
207 if current > $max {
208 use $crate::error::Error;
209 let err = Error::MaxCharactersExceeded { max: $max, current };
210 return $crate::response::from_err(err);
211 }
212 };
213}
214
215#[doc(hidden)]
216#[macro_export]
217macro_rules! bail_if_player_is_not_pending {
218 ($world:expr, $player:expr) => {
219 if !$world.round().is_waiting_player($player) {
220 use nil_core::error::Error;
221 let err = Error::NotWaitingPlayer($player.clone());
222 return $crate::response::from_err(err);
223 }
224 };
225}
226
227#[doc(hidden)]
228#[macro_export]
229macro_rules! bail_if_player_ne {
230 ($current_player:expr, $player:expr) => {
231 if $current_player != $player {
232 return $crate::res!(FORBIDDEN);
233 }
234 };
235}