Skip to content

Commit

Permalink
Merge ValidationError into HandlerError
Browse files Browse the repository at this point in the history
Instead of handling different types of serializable errors, merge them
all into one.

Also refactor FromRequest for ValidatedJson which was copied from a
ValidatedForm implementation but not changed appropriately.
  • Loading branch information
Aleksbgbg committed Feb 10, 2024
1 parent 3be7196 commit 8fd97a2
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 93 deletions.
1 change: 0 additions & 1 deletion backend-rs/src/controllers.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
pub mod errors;
pub mod user;
pub mod validate;
77 changes: 71 additions & 6 deletions backend-rs/src/controllers/errors.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,46 @@
use crate::models::user;
use axum::extract::rejection::JsonRejection;
use axum::extract::{FromRequest, Request};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::{async_trait, Json};
use convert_case::{Case, Casing};
use lazy_static::lazy_static;
use sea_orm::DbErr;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::json;
use std::collections::HashMap;
use thiserror::Error;
use validator::{Validate, ValidationErrors, ValidationErrorsKind};

lazy_static! {
pub static ref FAILED_VALIDATION: Errors =
static ref FAILED_VALIDATION: Errors =
Errors::generic("Some inputs failed validation.".to_string());
}

#[derive(Serialize, Default, Clone)]
pub struct Errors {
struct Errors {
generic: Vec<String>,
specific: HashMap<String, Vec<String>>,
}

impl Errors {
pub fn generic(value: String) -> Self {
fn generic(value: String) -> Self {
let mut errors = Self::default();
errors.add_generic(value);
errors
}

pub fn add_generic(&mut self, value: String) {
fn add_generic(&mut self, value: String) {
self.generic.push(value);
}

pub fn add_one_specific(&mut self, key: String, value: String) {
fn add_one_specific(&mut self, key: String, value: String) {
self.add_specific(key, vec![value]);
}

pub fn add_specific(&mut self, key: String, value: Vec<String>) {
fn add_specific(&mut self, key: String, value: Vec<String>) {
self.specific.insert(key, value);
}
}
Expand All @@ -47,6 +53,10 @@ impl IntoResponse for Errors {

#[derive(Error, Debug)]
pub enum HandlerError {
#[error(transparent)]
JsonRejection(#[from] JsonRejection),
#[error(transparent)]
Validation(#[from] ValidationErrors),
#[error("Username must not be taken.")]
UsernameTaken,
#[error("Email Address must not be taken.")]
Expand Down Expand Up @@ -77,9 +87,45 @@ impl HandlerError {
}
}

fn format_error_messages(field: &str, errors: ValidationErrorsKind) -> Vec<String> {
let title = field.to_case(Case::Title);

match errors {
ValidationErrorsKind::Field(errors) => errors
.into_iter()
.map(|e| match e.code.as_ref() {
"email" => format!("{} must be a valid email address.", title),
"must_match" => format!("{} must be identical to {}.", title, e.message.unwrap()),
"length" => format!(
"{} must be between {} and {} characters long (currently {}).",
title,
e.params.get("min").unwrap(),
e.params.get("max").unwrap(),
e.params.get("value").unwrap().as_str().unwrap().len(),
),
code => unimplemented!(
"error message is not implemented for message code '{}'",
code
),
})
.collect(),
ValidationErrorsKind::Struct(_) | ValidationErrorsKind::List(_) => {
panic!("unexpected error type")
}
}
}

impl IntoResponse for HandlerError {
fn into_response(self) -> Response {
match self {
HandlerError::JsonRejection(_) => self.into_generic(StatusCode::BAD_REQUEST),
HandlerError::Validation(validation_errors) => (StatusCode::BAD_REQUEST, {
let mut errors = FAILED_VALIDATION.clone();
for (k, v) in validation_errors.into_errors() {
errors.add_specific(k.to_case(Case::Camel), format_error_messages(k, v));
}
errors
}),
HandlerError::UsernameTaken => self.failed_validation(StatusCode::BAD_REQUEST, "username"),
HandlerError::EmailTaken => self.failed_validation(StatusCode::BAD_REQUEST, "emailAddress"),
HandlerError::DecodeJwt(_) => self.into_generic(StatusCode::BAD_REQUEST),
Expand All @@ -93,3 +139,22 @@ impl IntoResponse for HandlerError {
.into_response()
}
}

#[derive(Debug, Clone, Copy, Default)]
pub struct ValidatedJson<T>(pub T);

#[async_trait]
impl<T, S> FromRequest<S> for ValidatedJson<T>
where
T: DeserializeOwned + Validate,
S: Send + Sync,
Json<T>: FromRequest<S, Rejection = JsonRejection>,
{
type Rejection = HandlerError;

async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Json(value) = Json::from_request(req, state).await?;
value.validate()?;
Ok(ValidatedJson(value))
}
}
3 changes: 1 addition & 2 deletions backend-rs/src/controllers/user.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::controllers::errors::HandlerError;
use crate::controllers::validate::ValidatedJson;
use crate::controllers::errors::{HandlerError, ValidatedJson};
use crate::models::id::Id;
use crate::models::user::{self, CreateUser, User};
use crate::AppState;
Expand Down
84 changes: 0 additions & 84 deletions backend-rs/src/controllers/validate.rs

This file was deleted.

0 comments on commit 8fd97a2

Please sign in to comment.