mirror of
https://github.com/thanipro/Axum-Rust-Rest-Api-Template
synced 2025-02-16 12:18:25 +00:00
dto, entity, error and api response definitions
This commit is contained in:
parent
0d53974bc1
commit
e932720b2d
14 changed files with 307 additions and 0 deletions
2
src/dto/mod.rs
Normal file
2
src/dto/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod token_dto;
|
||||
pub mod user_dto;
|
15
src/dto/token_dto.rs
Normal file
15
src/dto/token_dto.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TokenReadDto {
|
||||
pub token: String,
|
||||
pub iat: i64,
|
||||
pub exp: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TokenClaimsDto {
|
||||
pub sub: i32,
|
||||
pub email: String,
|
||||
pub iat: i64,
|
||||
pub exp: i64,
|
||||
}
|
63
src/dto/user_dto.rs
Normal file
63
src/dto/user_dto.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use crate::entity::user::User;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Validate)]
|
||||
pub struct UserLoginDto {
|
||||
#[validate(email(message = "Email is not valid"))]
|
||||
pub email: String,
|
||||
#[validate(length(
|
||||
min = 3,
|
||||
max = 20,
|
||||
message = "Password must be between 3 and 20 characters"
|
||||
))]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Validate)]
|
||||
pub struct UserRegisterDto {
|
||||
#[validate(email(message = "Email is not valid"))]
|
||||
pub email: String,
|
||||
#[validate(length(
|
||||
min = 3,
|
||||
max = 20,
|
||||
message = "Password must be between 3 and 20 characters"
|
||||
))]
|
||||
pub password: String,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
#[validate(length(
|
||||
min = 3,
|
||||
max = 20,
|
||||
message = "Username must be between 3 and 20 characters"
|
||||
))]
|
||||
pub user_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct UserReadDto {
|
||||
pub id: i32,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub user_name: String,
|
||||
pub email: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
pub is_active: i8,
|
||||
}
|
||||
|
||||
impl UserReadDto {
|
||||
pub fn from(model: User) -> UserReadDto {
|
||||
Self {
|
||||
id: model.id,
|
||||
first_name: model.first_name,
|
||||
last_name: model.last_name,
|
||||
user_name: model.user_name,
|
||||
email: model.email,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
is_active: model.is_active,
|
||||
}
|
||||
}
|
||||
}
|
1
src/entity/mod.rs
Normal file
1
src/entity/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod user;
|
15
src/entity/user.rs
Normal file
15
src/entity/user.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub user_name: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
pub is_active: i8,
|
||||
}
|
25
src/error/api_error.rs
Normal file
25
src/error/api_error.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use crate::error::db_error::DbError;
|
||||
use crate::error::token_error::TokenError;
|
||||
use crate::error::user_error::UserError;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ApiError {
|
||||
#[error(transparent)]
|
||||
TokenError(#[from] TokenError),
|
||||
#[error(transparent)]
|
||||
UserError(#[from] UserError),
|
||||
#[error(transparent)]
|
||||
DbError(#[from] DbError),
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
ApiError::TokenError(error) => error.into_response(),
|
||||
ApiError::UserError(error) => error.into_response(),
|
||||
ApiError::DbError(error) => error.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
25
src/error/db_error.rs
Normal file
25
src/error/db_error.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use crate::response::api_response::ApiErrorResponse;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DbError {
|
||||
#[error("{0}")]
|
||||
SomethingWentWrong(String),
|
||||
#[error("Duplicate entry exists")]
|
||||
UniqueConstraintViolation(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for DbError {
|
||||
fn into_response(self) -> Response {
|
||||
let status_code = match self {
|
||||
DbError::SomethingWentWrong(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
DbError::UniqueConstraintViolation(_) => StatusCode::CONFLICT,
|
||||
};
|
||||
|
||||
ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string()))
|
||||
}
|
||||
}
|
5
src/error/mod.rs
Normal file
5
src/error/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub(crate) mod api_error;
|
||||
pub(crate) mod db_error;
|
||||
pub(crate) mod request_error;
|
||||
pub(crate) mod token_error;
|
||||
pub(crate) mod user_error;
|
51
src/error/request_error.rs
Normal file
51
src/error/request_error.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use crate::response::api_response::ApiErrorResponse;
|
||||
use async_trait::async_trait;
|
||||
use axum::body::HttpBody;
|
||||
use axum::extract::rejection::JsonRejection;
|
||||
use axum::extract::FromRequest;
|
||||
use axum::http::Request;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::{BoxError, Json};
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RequestError {
|
||||
#[error(transparent)]
|
||||
ValidationError(#[from] validator::ValidationErrors),
|
||||
#[error(transparent)]
|
||||
JsonRejection(#[from] JsonRejection),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ValidatedRequest<T>(pub T);
|
||||
|
||||
#[async_trait]
|
||||
impl<T, S, B> FromRequest<S, B> for ValidatedRequest<T>
|
||||
where
|
||||
T: DeserializeOwned + Validate,
|
||||
S: Send + Sync,
|
||||
B: HttpBody + Send + 'static,
|
||||
B::Data: Send,
|
||||
B::Error: Into<BoxError>,
|
||||
{
|
||||
type Rejection = RequestError;
|
||||
|
||||
async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let Json(value) = Json::<T>::from_request(req, state).await?;
|
||||
value.validate()?;
|
||||
Ok(ValidatedRequest(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for RequestError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
RequestError::ValidationError(_) => {
|
||||
ApiErrorResponse::send(400, Some(self.to_string().replace('\n', ", ")))
|
||||
}
|
||||
RequestError::JsonRejection(_) => ApiErrorResponse::send(400, Some(self.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
31
src/error/token_error.rs
Normal file
31
src/error/token_error.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::response::api_response::ApiErrorResponse;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TokenError {
|
||||
#[error("Invalid token")]
|
||||
InvalidToken(String),
|
||||
#[error("Token has expired")]
|
||||
TokenExpired,
|
||||
#[error("Missing Bearer token")]
|
||||
MissingToken,
|
||||
#[error("Token error: {0}")]
|
||||
TokenCreationError(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for TokenError {
|
||||
fn into_response(self) -> Response {
|
||||
let status_code = match self {
|
||||
TokenError::InvalidToken(_) => StatusCode::UNAUTHORIZED,
|
||||
TokenError::TokenExpired => StatusCode::UNAUTHORIZED,
|
||||
TokenError::MissingToken => StatusCode::UNAUTHORIZED,
|
||||
TokenError::TokenCreationError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string()))
|
||||
}
|
||||
}
|
28
src/error/user_error.rs
Normal file
28
src/error/user_error.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use crate::response::api_response::ApiErrorResponse;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum UserError {
|
||||
#[error("User not found")]
|
||||
UserNotFound,
|
||||
#[error("User already exists")]
|
||||
UserAlreadyExists,
|
||||
#[error("Invalid password")]
|
||||
InvalidPassword,
|
||||
}
|
||||
|
||||
impl IntoResponse for UserError {
|
||||
fn into_response(self) -> Response {
|
||||
let status_code = match self {
|
||||
UserError::UserNotFound => StatusCode::NOT_FOUND,
|
||||
UserError::UserAlreadyExists => StatusCode::BAD_REQUEST,
|
||||
UserError::InvalidPassword => StatusCode::BAD_REQUEST,
|
||||
};
|
||||
|
||||
ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string()))
|
||||
}
|
||||
}
|
|
@ -4,6 +4,10 @@ use crate::config::database::DatabaseTrait;
|
|||
|
||||
mod config;
|
||||
mod routes;
|
||||
mod dto;
|
||||
mod error;
|
||||
mod response;
|
||||
mod entity;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
|
41
src/response/api_response.rs
Normal file
41
src/response/api_response.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct ApiSuccessResponse<T: Serialize> {
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct ApiErrorResponse {
|
||||
message: Option<String>,
|
||||
#[serde(rename = "code")]
|
||||
status: u16,
|
||||
}
|
||||
|
||||
impl<T: Serialize> ApiSuccessResponse<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
pub(crate) fn send(data: T) -> Self {
|
||||
return ApiSuccessResponse { data };
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiErrorResponse {
|
||||
pub(crate) fn send(status: u16, message: Option<String>) -> Response {
|
||||
return ApiErrorResponse { message, status }.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiErrorResponse {
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
Json(self),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
1
src/response/mod.rs
Normal file
1
src/response/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub(crate) mod api_response;
|
Loading…
Add table
Reference in a new issue