refactor: make the serverside error handling more robust

This commit is contained in:
Matouš Volf
2024-08-18 19:09:40 +02:00
parent a78f6bac94
commit 38be8af169
12 changed files with 301 additions and 43 deletions

View File

@ -10,7 +10,7 @@ pub(crate) fn Home() -> Element {
ProjectForm {
onsubmit: move |title| {
spawn(async move {
let _ = create_project(title).await;
create_project(title).await;
});
}
}

View File

@ -1,13 +1,14 @@
use crate::models::project::NewProject;
use dioxus::core_macro::{component, rsx};
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
#[component]
pub(crate) fn ProjectForm(onsubmit: EventHandler<String>) -> Element {
pub(crate) fn ProjectForm(onsubmit: EventHandler<NewProject>) -> Element {
rsx! {
form {
onsubmit: move |event| {
onsubmit(event.values().get("title").unwrap().as_value());
onsubmit(NewProject::new(event.values().get("title").unwrap().as_value()));
},
input {
r#type: "text",

29
src/errors/error.rs Normal file
View File

@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug)]
pub enum Error {
ServerInternal,
}
// has to be implemented for Dioxus server functions
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::ServerInternal => write!(f, "internal server error"),
}
}
}
// has to be implemented for Dioxus server functions
impl FromStr for Error {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"internal server error" => Error::ServerInternal,
_ => return Err(()),
})
}
}

43
src/errors/error_vec.rs Normal file
View File

@ -0,0 +1,43 @@
use std::fmt::Display;
use std::str::FromStr;
#[derive(Debug)]
pub struct ErrorVec<T> {
errors: Vec<T>,
}
impl<T> From<ErrorVec<T>> for Vec<T> {
fn from(e: ErrorVec<T>) -> Self {
e.errors
}
}
impl<T> From<Vec<T>> for ErrorVec<T> {
fn from(e: Vec<T>) -> Self {
ErrorVec { errors: e }
}
}
// has to be implemented for Dioxus server functions
impl<T: Display> Display for ErrorVec<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("\n")
)
}
}
// has to be implemented for Dioxus server functions
impl<T> FromStr for ErrorVec<T> {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(ErrorVec { errors: Vec::new() })
}
}

3
src/errors/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub(crate) mod error;
pub(crate) mod error_vec;
pub(crate) mod project_create_error;

View File

@ -0,0 +1,50 @@
use crate::errors::error::Error;
use crate::errors::error_vec::ErrorVec;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::FromStr;
use validator::{ValidationErrors, ValidationErrorsKind};
#[derive(Serialize, Deserialize, Debug)]
pub enum ProjectCreateError {
TitleTooShort,
Error(Error),
}
impl From<ValidationErrors> for ErrorVec<ProjectCreateError> {
fn from(e: ValidationErrors) -> Self {
e.errors()
.iter()
.flat_map(|(&field, error_kind)| match field {
"title_length" => match error_kind {
ValidationErrorsKind::Field(validation_errors) => validation_errors
.iter()
.map(|validation_error| match validation_error.code.as_ref() {
"length" => ProjectCreateError::TitleTooShort,
_ => ProjectCreateError::Error(Error::ServerInternal),
})
.collect::<Vec<ProjectCreateError>>(),
_ => panic!("unexpected error kind"),
},
_ => panic!("unexpected field name"),
})
.collect::<Vec<ProjectCreateError>>()
.into()
}
}
// has to be implemented for Dioxus server functions
impl Display for ProjectCreateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", dbg!(self))
}
}
// has to be implemented for Dioxus server functions
impl FromStr for ProjectCreateError {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(ProjectCreateError::Error(Error::ServerInternal))
}
}

View File

@ -1,6 +1,7 @@
#![allow(non_snake_case)]
mod components;
mod errors;
mod models;
mod route;
mod schema;

View File

@ -1,6 +1,10 @@
use crate::schema::projects;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use validator::Validate;
const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255;
#[derive(Queryable, Selectable, Serialize, Deserialize)]
#[diesel(table_name = crate::schema::projects)]
@ -20,8 +24,19 @@ impl Project {
}
}
#[derive(Insertable, Serialize, Deserialize)]
#[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)]
#[diesel(table_name = projects)]
pub struct NewProject<'a> {
pub title: &'a str,
pub struct NewProject {
#[validate(length(
min = "TITLE_LENGTH_MIN",
max = "TITLE_LENGTH_MAX",
code = "title_length"
))]
pub title: String,
}
impl NewProject {
pub fn new(title: String) -> Self {
Self { title }
}
}

View File

@ -3,10 +3,10 @@ use diesel::prelude::*;
use dotenvy::dotenv;
use std::env;
pub(crate) fn establish_database_connection() -> PgConnection {
pub(crate) fn establish_database_connection() -> ConnectionResult<PgConnection> {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let database_url =
env::var("DATABASE_URL").expect("The environment variable DATABASE_URL must be set.");
PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("error connecting to {}", database_url))
}

View File

@ -1,21 +1,33 @@
use crate::errors::error::Error;
use crate::errors::error_vec::ErrorVec;
use crate::errors::project_create_error::ProjectCreateError;
use crate::models::project::{NewProject, Project};
use crate::server::database_connection::establish_database_connection;
use diesel::{RunQueryDsl, SelectableHelper};
use dioxus::prelude::*;
use validator::Validate;
#[server]
pub(crate) async fn create_project(title: String) -> Result<Project, ServerFnError> {
pub(crate) async fn create_project(
new_project: NewProject,
) -> Result<Project, ServerFnError<ErrorVec<ProjectCreateError>>> {
use crate::schema::projects;
let mut connection = establish_database_connection();
new_project
.validate()
.map_err::<ErrorVec<ProjectCreateError>, _>(|errors| errors.into())?;
let new_project = NewProject {
title: title.as_str(),
};
let mut connection =
establish_database_connection().or::<ErrorVec<ProjectCreateError>>(Err(vec![
ProjectCreateError::Error(Error::ServerInternal),
]
.into()))?;
Ok(diesel::insert_into(projects::table)
let new_project = diesel::insert_into(projects::table)
.values(&new_project)
.returning(Project::as_returning())
.get_result(&mut connection)
.expect("error saving a new project"))
.expect("error saving a new project");
Ok(new_project)
}