1use crate::rok_exception;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
7#[non_exhaustive]
8pub enum RokError {
9 #[error("not found")]
10 NotFound,
11
12 #[error("forbidden")]
13 Forbidden,
14
15 #[cfg(feature = "orm")]
16 #[error("database error: {0}")]
17 Orm(sqlx::Error),
18
19 #[error("internal server error: {0}")]
20 Internal(String),
21}
22
23#[cfg(feature = "orm")]
24impl From<sqlx::Error> for RokError {
25 fn from(e: sqlx::Error) -> Self {
26 match e {
27 sqlx::Error::RowNotFound => Self::NotFound,
28 other => Self::Orm(other),
29 }
30 }
31}
32
33impl From<String> for RokError {
34 fn from(s: String) -> Self {
35 Self::Internal(s)
36 }
37}
38
39impl From<&str> for RokError {
40 fn from(s: &str) -> Self {
41 Self::Internal(s.to_string())
42 }
43}
44
45#[cfg(feature = "axum")]
46mod axum_impl {
47 use super::RokError;
48 use axum::{
49 http::StatusCode,
50 response::{IntoResponse, Response},
51 Json,
52 };
53
54 impl IntoResponse for RokError {
55 fn into_response(self) -> Response {
56 match self {
57 RokError::NotFound => {
58 (StatusCode::NOT_FOUND, Json(serde_json::json!({"message": "not found"}))).into_response()
59 }
60 RokError::Forbidden => {
61 (StatusCode::FORBIDDEN, Json(serde_json::json!({"message": "forbidden"}))).into_response()
62 }
63 #[cfg(feature = "orm")]
64 RokError::Orm(e) => {
65 #[cfg(feature = "app")]
66 tracing::error!(error = %e, "Internal server error (ORM)");
67 (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"message": "internal server error"}))).into_response()
68 }
69 #[cfg(feature = "orm")]
70 RokError::Internal(ref msg) => {
71 #[cfg(feature = "app")]
72 tracing::error!(error = %msg, "Internal server error");
73 (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"message": "internal server error"}))).into_response()
74 }
75 #[cfg(not(feature = "orm"))]
76 RokError::Internal(ref msg) => {
77 #[cfg(feature = "app")]
78 tracing::error!(error = %msg, "Internal server error");
79 (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"message": "internal server error"}))).into_response()
80 }
81 }
82 }
83 }
84}
85
86pub trait RokException: std::error::Error + Send + Sync + 'static {
95 fn name(&self) -> &'static str;
97 fn status_code(&self) -> u16;
99 fn self_handled(&self) -> bool {
101 true
102 }
103 fn translation_id(&self) -> Option<&'static str> {
105 None
106 }
107 fn help(&self) -> Option<&'static str> {
109 None
110 }
111}
112
113rok_exception! {
116 pub struct E_HTTP_EXCEPTION {
118 status = 500,
119 self_handled = true,
120 fields: {
121 pub message: String,
122 }
123 }
124}
125
126rok_exception! {
127 pub struct E_HTTP_REQUEST_ABORTED {
129 status = 500,
130 self_handled = true,
131 fields: {
132 pub message: String,
133 }
134 }
135}
136
137rok_exception! {
138 pub struct E_ROUTE_NOT_FOUND {
140 status = 404,
141 self_handled = true,
142 fields: {
143 pub method: String,
144 pub path: String,
145 }
146 }
147}
148
149rok_exception! {
150 pub struct E_METHOD_NOT_ALLOWED {
152 status = 405,
153 self_handled = true,
154 fields: {
155 pub method: String,
156 pub path: String,
157 }
158 }
159}
160
161rok_exception! {
162 pub struct E_CANNOT_LOOKUP_ROUTE {
164 status = 500,
165 self_handled = false,
166 fields: {
167 pub route_name: String,
168 }
169 }
170}
171
172rok_exception! {
173 pub struct E_MISSING_ROUTE_PARAM {
175 status = 500,
176 self_handled = false,
177 fields: {
178 pub param_name: String,
179 }
180 }
181}
182
183rok_exception! {
184 pub struct E_INSECURE_APP_KEY {
186 status = 500,
187 self_handled = false,
188 fields: {
189 pub actual_length: usize,
190 }
191 }
192}
193
194rok_exception! {
195 pub struct E_MISSING_APP_KEY {
197 status = 500,
198 self_handled = false,
199 fields: {
200 }
201 }
202}
203
204rok_exception! {
205 pub struct E_INVALID_ENV_VARIABLES {
207 status = 500,
208 self_handled = false,
209 fields: {
210 pub help: String,
211 }
212 }
213}
214
215rok_exception! {
216 pub struct E_MISSING_CONFIG_KEY {
218 status = 500,
219 self_handled = false,
220 fields: {
221 pub key: String,
222 }
223 }
224}
225
226rok_exception! {
227 pub struct E_CONFIG_PARSE_ERROR {
229 status = 500,
230 self_handled = false,
231 fields: {
232 pub key: String,
233 pub expected: String,
234 }
235 }
236}
237
238rok_exception! {
239 pub struct E_SESSION_NOT_MUTABLE {
241 status = 500,
242 self_handled = false,
243 fields: {
244 }
245 }
246}
247
248rok_exception! {
249 pub struct E_SESSION_NOT_READY {
251 status = 500,
252 self_handled = false,
253 fields: {
254 }
255 }
256}
257
258#[cfg(feature = "axum")]
261impl axum::response::IntoResponse for Box<dyn RokException> {
262 fn into_response(self) -> axum::response::Response {
263 let status = axum::http::StatusCode::from_u16(self.status_code())
264 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
265 let body = serde_json::json!({
266 "error": self.name(),
267 "message": self.to_string(),
268 "statusCode": self.status_code(),
269 });
270 (status, axum::Json(body)).into_response()
271 }
272}
273
274#[macro_export]
299macro_rules! rok_exception {
300 (
302 $(#[$meta:meta])*
303 $vis:vis struct $name:ident {
304 status = $status:expr,
305 self_handled = true,
306 translation = $translation:expr,
307 fields: { $(pub $field:ident: $ty:ty),* $(,)? }
308 }
309 ) => {
310 $(#[$meta])*
311 #[allow(non_camel_case_types)]
312 #[derive(Debug)]
313 $vis struct $name {
314 $(pub $field: $ty,)*
315 }
316 impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
317 impl std::fmt::Display for $name {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 use $crate::RokException;
320 write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
321 }
322 }
323 impl std::error::Error for $name {}
324 impl $crate::error::RokException for $name {
325 fn name(&self) -> &'static str { stringify!($name) }
326 fn status_code(&self) -> u16 { $status }
327 fn self_handled(&self) -> bool { true }
328 fn translation_id(&self) -> Option<&'static str> { Some($translation) }
329 }
330 #[cfg(feature = "axum")]
331 impl axum::response::IntoResponse for $name {
332 fn into_response(self) -> axum::response::Response {
333 let status = axum::http::StatusCode::from_u16($status)
334 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
335 let body = serde_json::json!({
336 "error": stringify!($name),
337 "message": self.to_string(),
338 "statusCode": $status,
339 });
340 (status, axum::Json(body)).into_response()
341 }
342 }
343 };
344
345 (
347 $(#[$meta:meta])*
348 $vis:vis struct $name:ident {
349 status = $status:expr,
350 self_handled = true,
351 fields: { $(pub $field:ident: $ty:ty),* $(,)? }
352 }
353 ) => {
354 $(#[$meta])*
355 #[allow(non_camel_case_types)]
356 #[derive(Debug)]
357 $vis struct $name {
358 $(pub $field: $ty,)*
359 }
360 impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
361 impl std::fmt::Display for $name {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 use $crate::RokException;
364 write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
365 }
366 }
367 impl std::error::Error for $name {}
368 impl $crate::error::RokException for $name {
369 fn name(&self) -> &'static str { stringify!($name) }
370 fn status_code(&self) -> u16 { $status }
371 fn self_handled(&self) -> bool { true }
372 fn translation_id(&self) -> Option<&'static str> { None }
373 }
374 #[cfg(feature = "axum")]
375 impl axum::response::IntoResponse for $name {
376 fn into_response(self) -> axum::response::Response {
377 let status = axum::http::StatusCode::from_u16($status)
378 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
379 let body = serde_json::json!({
380 "error": stringify!($name),
381 "message": self.to_string(),
382 "statusCode": $status,
383 });
384 (status, axum::Json(body)).into_response()
385 }
386 }
387 };
388
389 (
391 $(#[$meta:meta])*
392 $vis:vis struct $name:ident {
393 status = $status:expr,
394 self_handled = false,
395 translation = $translation:expr,
396 fields: { $(pub $field:ident: $ty:ty),* $(,)? }
397 }
398 ) => {
399 $(#[$meta])*
400 #[allow(non_camel_case_types)]
401 #[derive(Debug)]
402 $vis struct $name {
403 $(pub $field: $ty,)*
404 }
405 impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
406 impl std::fmt::Display for $name {
407 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408 use $crate::RokException;
409 write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
410 }
411 }
412 impl std::error::Error for $name {}
413 impl $crate::error::RokException for $name {
414 fn name(&self) -> &'static str { stringify!($name) }
415 fn status_code(&self) -> u16 { $status }
416 fn self_handled(&self) -> bool { false }
417 fn translation_id(&self) -> Option<&'static str> { Some($translation) }
418 }
419 };
420
421 (
423 $(#[$meta:meta])*
424 $vis:vis struct $name:ident {
425 status = $status:expr,
426 self_handled = false,
427 fields: { $(pub $field:ident: $ty:ty),* $(,)? }
428 }
429 ) => {
430 $(#[$meta])*
431 #[allow(non_camel_case_types)]
432 #[derive(Debug)]
433 $vis struct $name {
434 $(pub $field: $ty,)*
435 }
436 impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
437 impl std::fmt::Display for $name {
438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439 use $crate::RokException;
440 write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
441 }
442 }
443 impl std::error::Error for $name {}
444 impl $crate::error::RokException for $name {
445 fn name(&self) -> &'static str { stringify!($name) }
446 fn status_code(&self) -> u16 { $status }
447 fn self_handled(&self) -> bool { false }
448 fn translation_id(&self) -> Option<&'static str> { None }
449 }
450 };
451
452 (
454 $(#[$meta:meta])*
455 $vis:vis struct $name:ident {
456 status = $status:expr,
457 fields: { $(pub $field:ident: $ty:ty),* $(,)? }
458 }
459 ) => {
460 $(#[$meta])*
461 #[allow(non_camel_case_types)]
462 #[derive(Debug)]
463 $vis struct $name {
464 $(pub $field: $ty,)*
465 }
466 impl $name { $vis fn new($($field: $ty),*) -> Self { Self { $($field),* } } }
467 impl std::fmt::Display for $name {
468 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469 use $crate::RokException;
470 write!(f, concat!("{} (", stringify!($name), ")"), self.status_code())
471 }
472 }
473 impl std::error::Error for $name {}
474 impl $crate::error::RokException for $name {
475 fn name(&self) -> &'static str { stringify!($name) }
476 fn status_code(&self) -> u16 { $status }
477 fn self_handled(&self) -> bool { true }
478 fn translation_id(&self) -> Option<&'static str> { None }
479 }
480 #[cfg(feature = "axum")]
481 impl axum::response::IntoResponse for $name {
482 fn into_response(self) -> axum::response::Response {
483 let status = axum::http::StatusCode::from_u16($status)
484 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
485 let body = serde_json::json!({
486 "error": stringify!($name),
487 "message": self.to_string(),
488 "statusCode": $status,
489 });
490 (status, axum::Json(body)).into_response()
491 }
492 }
493 };
494}