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}