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}