dto, entity, error and api response definitions

This commit is contained in:
Thani Sheun 2023-05-03 22:18:00 +02:00
parent 0d53974bc1
commit e932720b2d
14 changed files with 307 additions and 0 deletions

2
src/dto/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod token_dto;
pub mod user_dto;

15
src/dto/token_dto.rs Normal file
View 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
View 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
View file

@ -0,0 +1 @@
pub mod user;

15
src/entity/user.rs Normal file
View 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
View 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
View 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
View 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;

View 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
View 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
View 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()))
}
}

View file

@ -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() {

View 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
View file

@ -0,0 +1 @@
pub(crate) mod api_response;