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;
85pub mod middleware;
86pub mod monitoring;
87mod routes;
88pub mod views;
89
90/// Create an unauthorized error with a specified message.
91///
92/// This function is used to generate an `Error::Unauthorized` variant with a
93/// custom message.
94///
95/// # Errors
96///
97/// returns unauthorized enum
98///
99/// # Example
100///
101/// ```rust
102/// use loco_rs::prelude::*;
103///
104/// async fn login() -> Result<Response> {
105///     let valid = false;
106///     if !valid {
107///         return unauthorized("unauthorized access");
108///     }
109///     format::json(())
110/// }
111/// ````
112pub fn unauthorized<T: Into<String>, U>(msg: T) -> Result<U> {
113    Err(Error::Unauthorized(msg.into()))
114}
115
116/// Return a bad request with a message
117///
118/// # Errors
119///
120/// This function will return an error result
121pub fn bad_request<T: Into<String>, U>(msg: T) -> Result<U> {
122    Err(Error::BadRequest(msg.into()))
123}
124
125/// return not found status code
126///
127/// # Errors
128/// Currently this function doesn't return any error. this is for feature
129/// functionality
130pub fn not_found<T>() -> Result<T> {
131    Err(Error::NotFound)
132}
133#[derive(Debug, Serialize)]
134/// Structure representing details about an error.
135pub struct ErrorDetail {
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub error: Option<String>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub description: Option<String>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub errors: Option<serde_json::Value>,
142}
143
144impl ErrorDetail {
145    /// Create a new `ErrorDetail` with the specified error and description.
146    #[must_use]
147    pub fn new<T: Into<String> + AsRef<str>>(error: T, description: T) -> Self {
148        let description = (!description.as_ref().is_empty()).then(|| description.into());
149        Self {
150            error: Some(error.into()),
151            description,
152            errors: None,
153        }
154    }
155
156    /// Create an `ErrorDetail` with only an error reason and no description.
157    #[must_use]
158    pub fn with_reason<T: Into<String>>(error: T) -> Self {
159        Self {
160            error: Some(error.into()),
161            description: None,
162            errors: None,
163        }
164    }
165}
166
167#[derive(Debug, FromRequest)]
168#[from_request(via(axum::Json), rejection(Error))]
169pub struct Json<T>(pub T);
170
171impl<T: Serialize> IntoResponse for Json<T> {
172    fn into_response(self) -> axum::response::Response {
173        axum::Json(self.0).into_response()
174    }
175}
176
177impl IntoResponse for Error {
178    /// Convert an `Error` into an HTTP response.
179    #[allow(clippy::cognitive_complexity)]
180    fn into_response(self) -> Response {
181        match &self {
182            Self::WithBacktrace {
183                inner,
184                backtrace: _,
185            } => {
186                tracing::error!(
187                error.msg = %inner,
188                error.details = ?inner,
189                "controller_error"
190                );
191            }
192            err => {
193                tracing::error!(
194                error.msg = %err,
195                error.details = ?err,
196                "controller_error"
197                );
198            }
199        }
200
201        let public_facing_error = match self {
202            Self::NotFound => (
203                StatusCode::NOT_FOUND,
204                ErrorDetail::new("not_found", "Resource was not found"),
205            ),
206            Self::Unauthorized(err) => {
207                tracing::warn!(err);
208                (
209                    StatusCode::UNAUTHORIZED,
210                    ErrorDetail::new(
211                        "unauthorized",
212                        "You do not have permission to access this resource",
213                    ),
214                )
215            }
216            Self::CustomError(status_code, data) => (status_code, data),
217            Self::WithBacktrace { inner, backtrace } => {
218                println!("\n{}", inner.to_string().red().underline());
219                backtrace::print_backtrace(&backtrace).unwrap();
220                (
221                    StatusCode::BAD_REQUEST,
222                    ErrorDetail::with_reason("Bad Request"),
223                )
224            }
225            Self::BadRequest(err) => (
226                StatusCode::BAD_REQUEST,
227                ErrorDetail::new("Bad Request", &err),
228            ),
229            Self::JsonRejection(err) => {
230                tracing::debug!(err = err.body_text(), "json rejection");
231                (err.status(), ErrorDetail::with_reason("Bad Request"))
232            }
233
234            Self::Validation(ref errors) => (
235                StatusCode::BAD_REQUEST,
236                ErrorDetail {
237                    error: None,
238                    description: None,
239                    errors: Some(serde_json::to_value(&errors.errors).unwrap_or_default()),
240                },
241            ),
242            _ => (
243                StatusCode::INTERNAL_SERVER_ERROR,
244                ErrorDetail::new("internal_server_error", "Internal Server Error"),
245            ),
246        };
247
248        (public_facing_error.0, Json(public_facing_error.1)).into_response()
249    }
250}