loco_rs/controller/
mod.rs

1//! Manage web server routing
2//!
3//! # Example
4//!
5//! This example you can adding custom routes into your application by
6//! implementing routes trait from [`crate::app::Hooks`] and adding your
7//! endpoints to your application
8//!
9//! ```rust
10//! use async_trait::async_trait;
11//! use loco_rs::{
12//!    app::{AppContext, Hooks},
13//!    boot::{create_app, BootResult, StartMode},
14//!    config::Config,
15//!    controller::AppRoutes,
16//!    prelude::*,
17//!    task::Tasks,
18//!    environment::Environment,
19//!    Result,
20//! };
21//! use sea_orm::DatabaseConnection;
22//! use std::path::Path;
23//!
24//! /// this code block should be taken from the sea_orm migration model.
25//! pub struct App;
26//! pub use sea_orm_migration::prelude::*;
27//! pub struct Migrator;
28//! #[async_trait::async_trait]
29//! impl MigratorTrait for Migrator {
30//!     fn migrations() -> Vec<Box<dyn MigrationTrait>> {
31//!         vec![]
32//!     }
33//! }
34//!
35//! #[async_trait]
36//! impl Hooks for App {
37//!
38//!    fn app_name() -> &'static str {
39//!        env!("CARGO_CRATE_NAME")
40//!    }
41//!
42//!     fn routes(ctx: &AppContext) -> AppRoutes {
43//!         AppRoutes::with_default_routes()
44//!             // .add_route(controllers::notes::routes())
45//!     }
46//!     
47//!     async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult>{
48//!          create_app::<Self, Migrator>(mode, environment, config).await
49//!     }
50//!     
51//!     async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {
52//!         Ok(())
53//!     }
54//!
55//!
56//!     fn register_tasks(tasks: &mut Tasks) {}
57//!
58//!     async fn truncate(_ctx: &AppContext) -> Result<()> {
59//!         Ok(())
60//!     }
61//!
62//!     async fn seed(_ctx: &AppContext, base: &Path) -> Result<()> {
63//!         Ok(())
64//!     }
65//! }
66//! ```
67
68pub use app_routes::{AppRoutes, ListRoutes};
69use axum::{
70    extract::FromRequest,
71    http::StatusCode,
72    response::{IntoResponse, Response},
73};
74use colored::Colorize;
75pub use routes::Routes;
76use serde::Serialize;
77
78use crate::{errors::Error, Result};
79
80mod app_routes;
81mod backtrace;
82mod describe;
83pub mod extractor;
84pub mod format;
85#[cfg(feature = "with-db")]
86mod health;
87pub mod middleware;
88mod ping;
89mod routes;
90pub mod views;
91
92/// Create an unauthorized error with a specified message.
93///
94/// This function is used to generate an `Error::Unauthorized` variant with a
95/// custom message.
96///
97/// # Errors
98///
99/// returns unauthorized enum
100///
101/// # Example
102///
103/// ```rust
104/// use loco_rs::prelude::*;
105///
106/// async fn login() -> Result<Response> {
107///     let valid = false;
108///     if !valid {
109///         return unauthorized("unauthorized access");
110///     }
111///     format::json(())
112/// }
113/// ````
114pub fn unauthorized<T: Into<String>, U>(msg: T) -> Result<U> {
115    Err(Error::Unauthorized(msg.into()))
116}
117
118/// Return a bad request with a message
119///
120/// # Errors
121///
122/// This function will return an error result
123pub fn bad_request<T: Into<String>, U>(msg: T) -> Result<U> {
124    Err(Error::BadRequest(msg.into()))
125}
126
127/// return not found status code
128///
129/// # Errors
130/// Currently this function doesn't return any error. this is for feature
131/// functionality
132pub fn not_found<T>() -> Result<T> {
133    Err(Error::NotFound)
134}
135#[derive(Debug, Serialize)]
136/// Structure representing details about an error.
137pub struct ErrorDetail {
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub error: Option<String>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub description: Option<String>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub errors: Option<serde_json::Value>,
144}
145
146impl ErrorDetail {
147    /// Create a new `ErrorDetail` with the specified error and description.
148    #[must_use]
149    pub fn new<T: Into<String> + AsRef<str>>(error: T, description: T) -> Self {
150        let description = (!description.as_ref().is_empty()).then(|| description.into());
151        Self {
152            error: Some(error.into()),
153            description,
154            errors: None,
155        }
156    }
157
158    /// Create an `ErrorDetail` with only an error reason and no description.
159    #[must_use]
160    pub fn with_reason<T: Into<String>>(error: T) -> Self {
161        Self {
162            error: Some(error.into()),
163            description: None,
164            errors: None,
165        }
166    }
167}
168
169#[derive(Debug, FromRequest)]
170#[from_request(via(axum::Json), rejection(Error))]
171pub struct Json<T>(pub T);
172
173impl<T: Serialize> IntoResponse for Json<T> {
174    fn into_response(self) -> axum::response::Response {
175        axum::Json(self.0).into_response()
176    }
177}
178
179impl IntoResponse for Error {
180    /// Convert an `Error` into an HTTP response.
181    #[allow(clippy::cognitive_complexity)]
182    fn into_response(self) -> Response {
183        match &self {
184            Self::WithBacktrace {
185                inner,
186                backtrace: _,
187            } => {
188                tracing::error!(
189                error.msg = %inner,
190                error.details = ?inner,
191                "controller_error"
192                );
193            }
194            err => {
195                tracing::error!(
196                error.msg = %err,
197                error.details = ?err,
198                "controller_error"
199                );
200            }
201        }
202
203        let public_facing_error = match self {
204            Self::NotFound => (
205                StatusCode::NOT_FOUND,
206                ErrorDetail::new("not_found", "Resource was not found"),
207            ),
208            Self::Unauthorized(err) => {
209                tracing::warn!(err);
210                (
211                    StatusCode::UNAUTHORIZED,
212                    ErrorDetail::new(
213                        "unauthorized",
214                        "You do not have permission to access this resource",
215                    ),
216                )
217            }
218            Self::CustomError(status_code, data) => (status_code, data),
219            Self::WithBacktrace { inner, backtrace } => {
220                println!("\n{}", inner.to_string().red().underline());
221                backtrace::print_backtrace(&backtrace).unwrap();
222                (
223                    StatusCode::BAD_REQUEST,
224                    ErrorDetail::with_reason("Bad Request"),
225                )
226            }
227            Self::BadRequest(err) => (
228                StatusCode::BAD_REQUEST,
229                ErrorDetail::new("Bad Request", &err),
230            ),
231            Self::JsonRejection(err) => {
232                tracing::debug!(err = err.body_text(), "json rejection");
233                (err.status(), ErrorDetail::with_reason("Bad Request"))
234            }
235
236            Self::ValidationError(ref errors) => serde_json::to_value(errors).map_or_else(
237                |_| {
238                    (
239                        StatusCode::INTERNAL_SERVER_ERROR,
240                        ErrorDetail::new("internal_server_error", "Internal Server Error"),
241                    )
242                },
243                |errors| {
244                    (
245                        StatusCode::BAD_REQUEST,
246                        ErrorDetail {
247                            error: None,
248                            description: None,
249                            errors: Some(errors),
250                        },
251                    )
252                },
253            ),
254            _ => (
255                StatusCode::INTERNAL_SERVER_ERROR,
256                ErrorDetail::new("internal_server_error", "Internal Server Error"),
257            ),
258        };
259
260        (public_facing_error.0, Json(public_facing_error.1)).into_response()
261    }
262}