diff --git a/Cargo.lock b/Cargo.lock index 282fb31..53679ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -94,7 +103,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -178,7 +187,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -469,7 +478,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.74", ] [[package]] @@ -480,7 +489,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -525,7 +534,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -534,7 +543,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" dependencies = [ - "syn", + "syn 2.0.74", ] [[package]] @@ -620,7 +629,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -730,7 +739,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -833,7 +842,7 @@ dependencies = [ "proc-macro2", "quote", "slab", - "syn", + "syn 2.0.74", ] [[package]] @@ -847,7 +856,7 @@ dependencies = [ "krates", "proc-macro2", "quote", - "syn", + "syn 2.0.74", "tracing", ] @@ -923,7 +932,7 @@ dependencies = [ "proc-macro2", "quote", "server_fn_macro", - "syn", + "syn 2.0.74", ] [[package]] @@ -943,7 +952,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -979,7 +988,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -1112,7 +1121,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -1872,7 +1881,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -1923,7 +1932,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.74", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -1983,6 +2016,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2078,7 +2140,7 @@ checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -2122,7 +2184,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -2181,7 +2243,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn", + "syn 2.0.74", "xxhash-rust", ] @@ -2192,7 +2254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e4fd51eb9ee3e7d9fb0febec6cef486dcbc8f7f427591dfcfebee1abe1ad4" dependencies = [ "server_fn_macro", - "syn", + "syn 2.0.74", ] [[package]] @@ -2256,7 +2318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edc90d3e8623d29a664cd8dba5078b600dd203444f00b9739f744e4c6e7aeaf2" dependencies = [ "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -2317,6 +2379,16 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.74" @@ -2357,7 +2429,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -2400,6 +2472,7 @@ dependencies = [ "dioxus-logger", "dotenvy", "serde", + "validator", ] [[package]] @@ -2428,7 +2501,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -2543,7 +2616,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] [[package]] @@ -2692,6 +2765,36 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "validator" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55591299b7007f551ed1eb79a684af7672c19c3193fb9e0a31936987bb2438ec" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2738,7 +2841,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.74", "wasm-bindgen-shared", ] @@ -2772,7 +2875,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2934,5 +3037,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.74", ] diff --git a/Cargo.toml b/Cargo.toml index 825ac02..9eabee1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ dioxus = { version = "0.5", features = ["fullstack", "router"] } dioxus-logger = "0.5.1" dotenvy = "0.15.7" serde = "1.0.208" +validator = { version = "0.18.1", features = ["derive"] } [features] default = [] diff --git a/src/components/home.rs b/src/components/home.rs index 2a4b74f..c2d3ab9 100644 --- a/src/components/home.rs +++ b/src/components/home.rs @@ -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; }); } } diff --git a/src/components/project_form.rs b/src/components/project_form.rs index 628d756..53e30ec 100644 --- a/src/components/project_form.rs +++ b/src/components/project_form.rs @@ -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) -> Element { +pub(crate) fn ProjectForm(onsubmit: EventHandler) -> 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", diff --git a/src/errors/error.rs b/src/errors/error.rs new file mode 100644 index 0000000..dc235c5 --- /dev/null +++ b/src/errors/error.rs @@ -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 { + Ok(match s { + "internal server error" => Error::ServerInternal, + _ => return Err(()), + }) + } +} diff --git a/src/errors/error_vec.rs b/src/errors/error_vec.rs new file mode 100644 index 0000000..557b338 --- /dev/null +++ b/src/errors/error_vec.rs @@ -0,0 +1,43 @@ +use std::fmt::Display; +use std::str::FromStr; + +#[derive(Debug)] +pub struct ErrorVec { + errors: Vec, +} + +impl From> for Vec { + fn from(e: ErrorVec) -> Self { + e.errors + } +} + +impl From> for ErrorVec { + fn from(e: Vec) -> Self { + ErrorVec { errors: e } + } +} + +// has to be implemented for Dioxus server functions +impl Display for ErrorVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.errors + .iter() + .map(|e| e.to_string()) + .collect::>() + .join("\n") + ) + } +} + +// has to be implemented for Dioxus server functions +impl FromStr for ErrorVec { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(ErrorVec { errors: Vec::new() }) + } +} diff --git a/src/errors/mod.rs b/src/errors/mod.rs new file mode 100644 index 0000000..625611e --- /dev/null +++ b/src/errors/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod error; +pub(crate) mod error_vec; +pub(crate) mod project_create_error; diff --git a/src/errors/project_create_error.rs b/src/errors/project_create_error.rs new file mode 100644 index 0000000..99ec5ae --- /dev/null +++ b/src/errors/project_create_error.rs @@ -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 for ErrorVec { + 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::>(), + _ => panic!("unexpected error kind"), + }, + _ => panic!("unexpected field name"), + }) + .collect::>() + .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 { + Ok(ProjectCreateError::Error(Error::ServerInternal)) + } +} diff --git a/src/main.rs b/src/main.rs index ff7692a..95dffb9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![allow(non_snake_case)] mod components; +mod errors; mod models; mod route; mod schema; diff --git a/src/models/project.rs b/src/models/project.rs index c312217..d2a58b8 100644 --- a/src/models/project.rs +++ b/src/models/project.rs @@ -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 } + } } diff --git a/src/server/database_connection.rs b/src/server/database_connection.rs index 717e63e..826f7c8 100644 --- a/src/server/database_connection.rs +++ b/src/server/database_connection.rs @@ -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 { 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)) } diff --git a/src/server/projects.rs b/src/server/projects.rs index 734e281..85bfb1a 100644 --- a/src/server/projects.rs +++ b/src/server/projects.rs @@ -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 { +pub(crate) async fn create_project( + new_project: NewProject, +) -> Result>> { use crate::schema::projects; - let mut connection = establish_database_connection(); + new_project + .validate() + .map_err::, _>(|errors| errors.into())?; - let new_project = NewProject { - title: title.as_str(), - }; + let mut connection = + establish_database_connection().or::>(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) }