Skip to main content

pipi/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 pipi::{
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 pipi::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<T1: Into<String> + AsRef<str>, T2: Into<String> + AsRef<str>>(
148        error: T1,
149        description: T2,
150    ) -> Self {
151        let description = (!description.as_ref().is_empty()).then(|| description.into());
152        Self {
153            error: Some(error.into()),
154            description,
155            errors: None,
156        }
157    }
158
159    /// Create an `ErrorDetail` with only an error reason and no description.
160    #[must_use]
161    pub fn with_reason<T: Into<String>>(error: T) -> Self {
162        Self {
163            error: Some(error.into()),
164            description: None,
165            errors: None,
166        }
167    }
168}
169
170#[derive(Debug, FromRequest)]
171#[from_request(via(axum::Json), rejection(Error))]
172pub struct Json<T>(pub T);
173
174impl<T: Serialize> IntoResponse for Json<T> {
175    fn into_response(self) -> axum::response::Response {
176        axum::Json(self.0).into_response()
177    }
178}
179
180impl IntoResponse for Error {
181    /// Convert an `Error` into an HTTP response.
182    #[allow(clippy::cognitive_complexity)]
183    fn into_response(self) -> Response {
184        match &self {
185            Self::WithBacktrace {
186                inner,
187                backtrace: _,
188            } => {
189                tracing::error!(
190                error.msg = %inner,
191                error.details = ?inner,
192                "controller_error"
193                );
194            }
195            err => {
196                tracing::error!(
197                error.msg = %err,
198                error.details = ?err,
199                "controller_error"
200                );
201            }
202        }
203
204        let public_facing_error = match self {
205            Self::NotFound => (
206                StatusCode::NOT_FOUND,
207                ErrorDetail::new("not_found", "Resource was not found"),
208            ),
209            Self::Unauthorized(err) => {
210                tracing::warn!(err);
211                (
212                    StatusCode::UNAUTHORIZED,
213                    ErrorDetail::new(
214                        "unauthorized",
215                        "You do not have permission to access this resource",
216                    ),
217                )
218            }
219            Self::CustomError(status_code, data) => (status_code, data),
220            Self::WithBacktrace { inner, backtrace } => {
221                println!("\n{}", inner.to_string().red().underline());
222                backtrace::print_backtrace(&backtrace).unwrap();
223                (
224                    StatusCode::BAD_REQUEST,
225                    ErrorDetail::with_reason("Bad Request"),
226                )
227            }
228            Self::BadRequest(err) => (
229                StatusCode::BAD_REQUEST,
230                ErrorDetail::new("Bad Request", &err),
231            ),
232            Self::JsonRejection(err) => {
233                tracing::debug!(err = err.body_text(), "json rejection");
234                (err.status(), ErrorDetail::with_reason("Bad Request"))
235            }
236
237            Self::Validation(ref errors) => (
238                StatusCode::BAD_REQUEST,
239                ErrorDetail {
240                    error: None,
241                    description: None,
242                    errors: Some(serde_json::to_value(&errors.errors).unwrap_or_default()),
243                },
244            ),
245            _ => (
246                StatusCode::INTERNAL_SERVER_ERROR,
247                ErrorDetail::new("internal_server_error", "Internal Server Error"),
248            ),
249        };
250
251        (public_facing_error.0, Json(public_facing_error.1)).into_response()
252    }
253}