From ccb666f0004b97f22dddd106319172aedecef110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 29 Aug 2023 21:34:00 +0200 Subject: [PATCH] error refactor --- rust/src/main.rs | 477 +++++++++------------------------------ rust/src/models.rs | 420 ++++++---------------------------- rust/src/models/error.rs | 165 ++++++++++++++ 3 files changed, 340 insertions(+), 722 deletions(-) create mode 100644 rust/src/models/error.rs diff --git a/rust/src/main.rs b/rust/src/main.rs index dae51b5..9c6eaea 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -20,10 +20,8 @@ use std::str::FromStr; use serde_variant::to_variant_name; use sqlx::{ - error::DatabaseError, - query, - sqlite::{SqliteConnectOptions, SqliteError, SqlitePoolOptions, SqliteRow}, - Pool, Row, Sqlite, + sqlite::{SqliteConnectOptions, SqlitePoolOptions}, + Pool, Sqlite, }; use maud::Markup; @@ -250,14 +248,7 @@ async fn inventory_active( state.client_state.edit_item = inventory_query.edit_item; state.client_state.active_category_id = Some(id); - let inventory = models::Inventory::load(&state.database_pool) - .await - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - })?; + let inventory = models::Inventory::load(&state.database_pool).await?; let active_category: Option<&models::Category> = state .client_state @@ -296,14 +287,7 @@ async fn inventory_inactive( state.client_state.edit_item = inventory_query.edit_item; state.client_state.active_category_id = None; - let inventory = models::Inventory::load(&state.database_pool) - .await - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - })?; + let inventory = models::Inventory::load(&state.database_pool).await?; Ok(( StatusCode::OK, @@ -318,29 +302,6 @@ async fn inventory_inactive( )) } -// async fn inventory( -// mut state: AppState, -// active_id: Option, -// ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { -// state.client_state.active_category_id = active_id; - -// Ok(( -// StatusCode::OK, -// components::Root::build( -// &components::inventory::Inventory::build(state.client_state, categories).map_err(|e| match e { -// Error::NotFound { description } => { -// (StatusCode::NOT_FOUND, components::ErrorPage::build(&description)) -// } -// _ => ( -// StatusCode::INTERNAL_SERVER_ERROR, -// components::ErrorPage::build(&e.to_string()), -// ), -// })?, -// &TopLevelPage::Inventory, -// ), -// )) -// } - #[derive(Deserialize)] struct NewItem { #[serde(rename = "new-item-name")] @@ -363,14 +324,7 @@ async fn inventory_item_validate_name( State(state): State, Form(new_item): Form, ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { - let exists = models::InventoryItem::name_exists(&state.database_pool, &new_item.name) - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - }) - .await?; + let exists = models::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?; Ok(( StatusCode::OK, @@ -378,6 +332,27 @@ async fn inventory_item_validate_name( )) } +impl From for (StatusCode, Markup) { + fn from(value: models::Error) -> (StatusCode, Markup) { + match value { + models::Error::Database(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + components::ErrorPage::build(&value.to_string()), + ), + models::Error::Query(error) => match error { + models::QueryError::NotFound { description } => ( + StatusCode::NOT_FOUND, + components::ErrorPage::build(&description), + ), + _ => ( + StatusCode::BAD_REQUEST, + components::ErrorPage::build(&error.to_string()), + ), + }, + } + } +} + async fn inventory_item_create( State(state): State, headers: HeaderMap, @@ -396,27 +371,10 @@ async fn inventory_item_create( new_item.category_id, new_item.weight, ) - .map_err(|error| match error { - models::Error::Constraint { description } => ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&description), - ), - _ => ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ), - }) .await?; if is_htmx(&headers) { - let inventory = models::Inventory::load(&state.database_pool) - .await - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - })?; + let inventory = models::Inventory::load(&state.database_pool).await?; // it's impossible to NOT find the item here, as we literally just added // it. but good error handling never hurts @@ -456,18 +414,7 @@ async fn inventory_item_delete( headers: HeaderMap, Path(id): Path, ) -> Result { - let deleted = models::InventoryItem::delete(&state.database_pool, id) - .map_err(|error| match error { - models::Error::Constraint { ref description } => ( - StatusCode::NOT_IMPLEMENTED, - components::ErrorPage::build(description), - ), - _ => ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ), - }) - .await?; + let deleted = models::InventoryItem::delete(&state.database_pool, id).await?; if !deleted { Err(( @@ -520,20 +467,14 @@ async fn inventory_item_edit( &state.database_pool, id, &edit_item.name, - i64::try_from(edit_item.weight).map_err(|e| { + i64::try_from(edit_item.weight).map_err(|error| { ( StatusCode::UNPROCESSABLE_ENTITY, - components::ErrorPage::build(&e.to_string()), + components::ErrorPage::build(&error.to_string()), ) })?, ) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&e.to_string()), - ) - })? + .await? .ok_or(( StatusCode::NOT_FOUND, components::ErrorPage::build(&format!("item with id {id} not found", id = id)), @@ -545,14 +486,11 @@ async fn inventory_item_edit( async fn inventory_item_cancel( State(state): State, Path(id): Path, -) -> Result { - let id = models::Item::find(&state.database_pool, id) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? - .ok_or(( - StatusCode::NOT_FOUND, - format!("item with id {id} not found", id = id), - ))?; +) -> Result { + let id = models::Item::find(&state.database_pool, id).await?.ok_or(( + StatusCode::NOT_FOUND, + components::ErrorPage::build(&format!("item with id {id} not found")), + ))?; Ok(Redirect::to(&format!( "/inventory/category/{id}/", @@ -587,16 +525,6 @@ async fn trip_create( new_trip.date_start, new_trip.date_end, ) - .map_err(|error| match error { - models::Error::TimeParse { description } => ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&description), - ), - _ => ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&error.to_string()), - ), - }) .await?; Ok(Redirect::to(&format!("/trips/{new_id}/"))) @@ -605,14 +533,7 @@ async fn trip_create( async fn trips( State(state): State, ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { - let trips = models::Trip::all(&state.database_pool) - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - }) - .await?; + let trips = models::Trip::all(&state.database_pool).await?; Ok(( StatusCode::OK, @@ -637,45 +558,17 @@ async fn trip( state.client_state.trip_edit_attribute = trip_query.edit; state.client_state.active_category_id = trip_query.category; - let mut trip: models::Trip = models::Trip::find(&state.database_pool, id) - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - }) - .await? - .ok_or(( - StatusCode::NOT_FOUND, - components::ErrorPage::build(&format!("trip with id {} not found", id)), - ))?; + let mut trip: models::Trip = models::Trip::find(&state.database_pool, id).await?.ok_or(( + StatusCode::NOT_FOUND, + components::ErrorPage::build(&format!("trip with id {} not found", id)), + ))?; - trip.load_trips_types(&state.database_pool) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&e.to_string()), - ) - })?; + trip.load_trips_types(&state.database_pool).await?; trip.sync_trip_items_with_inventory(&state.database_pool) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&e.to_string()), - ) - })?; + .await?; - trip.load_categories(&state.database_pool) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&e.to_string()), - ) - })?; + trip.load_categories(&state.database_pool).await?; let active_category: Option<&models::TripCategory> = state .client_state @@ -710,14 +603,7 @@ async fn trip_type_remove( State(state): State, Path((trip_id, type_id)): Path<(Uuid, Uuid)>, ) -> Result { - let found = models::Trip::trip_type_remove(&state.database_pool, trip_id, type_id) - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - }) - .await?; + let found = models::Trip::trip_type_remove(&state.database_pool, trip_id, type_id).await?; if !found { Err(( @@ -735,22 +621,7 @@ async fn trip_type_add( State(state): State, Path((trip_id, type_id)): Path<(Uuid, Uuid)>, ) -> Result { - models::Trip::trip_type_add(&state.database_pool, trip_id, type_id) - .map_err(|error| match error { - models::Error::ReferenceNotFound { description } => ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&description), - ), - models::Error::Duplicate { description } => ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&description), - ), - _ => ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ), - }) - .await?; + models::Trip::trip_type_add(&state.database_pool, trip_id, type_id).await?; Ok(Redirect::to(&format!("/trips/{trip_id}/"))) } @@ -768,12 +639,6 @@ async fn trip_comment_set( ) -> Result { let found = models::Trip::set_comment(&state.database_pool, trip_id, &comment_update.new_comment) - .map_err(|error| { - ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&error.to_string()), - ) - }) .await?; if !found { @@ -811,16 +676,6 @@ async fn trip_edit_attribute( attribute, &trip_update.new_value, ) - .map_err(|error| match error { - models::Error::NotFound { description } => ( - StatusCode::NOT_FOUND, - components::ErrorPage::build(&description), - ), - _ => ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&error.to_string()), - ), - }) .await?; Ok(Redirect::to(&format!("/trips/{trip_id}/"))) @@ -833,19 +688,7 @@ async fn trip_item_set_state( key: models::TripItemStateKey, value: bool, ) -> Result<(), (StatusCode, Markup)> { - models::TripItem::set_state(&state.database_pool, trip_id, item_id, key, value) - .map_err(|error| match error { - models::Error::NotFound { description } => ( - StatusCode::NOT_FOUND, - components::ErrorPage::build(&description), - ), - _ => ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ), - }) - .await?; - + models::TripItem::set_state(&state.database_pool, trip_id, item_id, key, value).await?; Ok(()) } @@ -855,13 +698,7 @@ async fn trip_row( item_id: Uuid, ) -> Result { let item = models::TripItem::find(&state.database_pool, trip_id, item_id) - .await - .map_err(|error| { - ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&error.to_string()), - ) - })? + .await? .ok_or_else(|| { ( StatusCode::NOT_FOUND, @@ -875,23 +712,10 @@ async fn trip_row( let item_row = components::trip::TripItemListRow::build( trip_id, &item, - models::Item::get_category_max_weight(&state.database_pool, item.item.category_id) - .await - .map_err(|error| { - ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&error.to_string()), - ) - })?, + models::Item::get_category_max_weight(&state.database_pool, item.item.category_id).await?, ); let category = models::TripCategory::find(&state.database_pool, trip_id, item.item.category_id) - .map_err(|error| { - ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&error.to_string()), - ) - }) .await? .ok_or_else(|| { ( @@ -915,14 +739,16 @@ async fn trip_item_set_pick( Path((trip_id, item_id)): Path<(Uuid, Uuid)>, headers: HeaderMap, ) -> Result { - Ok(trip_item_set_state( - &state, - trip_id, - item_id, - models::TripItemStateKey::Pick, - true, + Ok::<_, models::Error>( + trip_item_set_state( + &state, + trip_id, + item_id, + models::TripItemStateKey::Pick, + true, + ) + .await?, ) - .await?) .map(|_| -> Result { Ok(Redirect::to( headers @@ -932,12 +758,12 @@ async fn trip_item_set_pick( components::ErrorPage::build("no referer header found"), ))? .to_str() - .map_err(|e| { + .map_err(|error| { ( StatusCode::BAD_REQUEST, components::ErrorPage::build(&format!( "referer could not be converted: {}", - e + error )), ) })?, @@ -974,14 +800,16 @@ async fn trip_item_set_unpick( Path((trip_id, item_id)): Path<(Uuid, Uuid)>, headers: HeaderMap, ) -> Result { - Ok(trip_item_set_state( - &state, - trip_id, - item_id, - models::TripItemStateKey::Pick, - false, + Ok::<_, models::Error>( + trip_item_set_state( + &state, + trip_id, + item_id, + models::TripItemStateKey::Pick, + false, + ) + .await?, ) - .await?) .map(|_| -> Result { Ok(Redirect::to( headers @@ -991,12 +819,12 @@ async fn trip_item_set_unpick( components::ErrorPage::build("no referer header found"), ))? .to_str() - .map_err(|e| { + .map_err(|error| { ( StatusCode::BAD_REQUEST, components::ErrorPage::build(&format!( "referer could not be converted: {}", - e + error )), ) })?, @@ -1033,14 +861,16 @@ async fn trip_item_set_pack( Path((trip_id, item_id)): Path<(Uuid, Uuid)>, headers: HeaderMap, ) -> Result { - Ok(trip_item_set_state( - &state, - trip_id, - item_id, - models::TripItemStateKey::Pack, - true, + Ok::<_, models::Error>( + trip_item_set_state( + &state, + trip_id, + item_id, + models::TripItemStateKey::Pack, + true, + ) + .await?, ) - .await?) .map(|_| -> Result { Ok(Redirect::to( headers @@ -1050,12 +880,12 @@ async fn trip_item_set_pack( components::ErrorPage::build("no referer header found"), ))? .to_str() - .map_err(|e| { + .map_err(|error| { ( StatusCode::BAD_REQUEST, components::ErrorPage::build(&format!( "referer could not be converted: {}", - e + error )), ) })?, @@ -1092,14 +922,16 @@ async fn trip_item_set_unpack( Path((trip_id, item_id)): Path<(Uuid, Uuid)>, headers: HeaderMap, ) -> Result { - Ok(trip_item_set_state( - &state, - trip_id, - item_id, - models::TripItemStateKey::Pack, - false, + Ok::<_, models::Error>( + trip_item_set_state( + &state, + trip_id, + item_id, + models::TripItemStateKey::Pack, + false, + ) + .await?, ) - .await?) .map(|_| -> Result { Ok(Redirect::to( headers @@ -1109,12 +941,12 @@ async fn trip_item_set_unpack( components::ErrorPage::build("no referer header found"), ))? .to_str() - .map_err(|e| { + .map_err(|error| { ( StatusCode::BAD_REQUEST, components::ErrorPage::build(&format!( "referer could not be converted: {}", - e + error )), ) })?, @@ -1151,13 +983,7 @@ async fn trip_total_weight_htmx( Path(trip_id): Path, ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { let total_weight = models::Trip::find_total_picked_weight(&state.database_pool, trip_id) - .await - .map_err(|error| { - ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&error.to_string()), - ) - })? + .await? .ok_or(( StatusCode::NOT_FOUND, components::ErrorPage::build(&format!("trip with id {trip_id} not found")), @@ -1185,18 +1011,7 @@ async fn inventory_category_create( )); } - let _new_id = models::Category::save(&state.database_pool, &new_category.name) - .map_err(|error| match error { - models::Error::Duplicate { description } => ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&description), - ), - _ => ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ), - }) - .await?; + let _new_id = models::Category::save(&state.database_pool, &new_category.name).await?; Ok(Redirect::to("/inventory/")) } @@ -1206,14 +1021,7 @@ async fn trip_state_set( headers: HeaderMap, Path((trip_id, new_state)): Path<(Uuid, models::TripState)>, ) -> Result { - let exists = models::Trip::set_state(&state.database_pool, trip_id, &new_state) - .map_err(|e| { - ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&e.to_string()), - ) - }) - .await?; + let exists = models::Trip::set_state(&state.database_pool, trip_id, &new_state).await?; if !exists { return Err(( @@ -1251,14 +1059,7 @@ async fn trips_types( ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { state.client_state.trip_type_edit = trip_type_query.edit; - let trip_types: Vec = models::TripsType::all(&state.database_pool) - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - }) - .await?; + let trip_types: Vec = models::TripsType::all(&state.database_pool).await?; Ok(( StatusCode::OK, @@ -1286,18 +1087,7 @@ async fn trip_type_create( )); } - let _new_id = models::TripsType::save(&state.database_pool, &new_trip_type.name) - .map_err(|error| match error { - models::Error::Duplicate { description } => ( - StatusCode::BAD_REQUEST, - components::ErrorPage::build(&description), - ), - _ => ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ), - }) - .await?; + let _new_id = models::TripsType::save(&state.database_pool, &new_trip_type.name).await?; Ok(Redirect::to("/trips/types/")) } @@ -1322,12 +1112,6 @@ async fn trips_types_edit_name( let exists = models::TripsType::set_name(&state.database_pool, trip_type_id, &trip_update.new_value) - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - }) .await?; if !exists { @@ -1348,12 +1132,6 @@ async fn inventory_item( Path(id): Path, ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { let item = models::InventoryItem::find(&state.database_pool, id) - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&e.to_string()), - ) - }) .await? .ok_or(( StatusCode::NOT_FOUND, @@ -1374,26 +1152,13 @@ async fn trip_category_select( Path((trip_id, category_id)): Path<(Uuid, Uuid)>, ) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> { let mut trip = models::Trip::find(&state.database_pool, trip_id) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&e.to_string()), - ) - })? + .await? .ok_or(( StatusCode::NOT_FOUND, components::ErrorPage::build(&format!("trip with id {trip_id} not found")), ))?; - trip.load_categories(&state.database_pool) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&e.to_string()), - ) - })?; + trip.load_categories(&state.database_pool).await?; let active_category = trip .categories() @@ -1421,14 +1186,7 @@ async fn inventory_category_select( State(state): State, Path(category_id): Path, ) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> { - let inventory = models::Inventory::load(&state.database_pool) - .await - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - })?; + let inventory = models::Inventory::load(&state.database_pool).await?; let active_category: Option<&models::Category> = Some( inventory @@ -1467,26 +1225,13 @@ async fn trip_packagelist( Path(trip_id): Path, ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { let mut trip = models::Trip::find(&state.database_pool, trip_id) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&e.to_string()), - ) - })? + .await? .ok_or(( StatusCode::NOT_FOUND, components::ErrorPage::build(&format!("trip with id {trip_id} not found")), ))?; - trip.load_categories(&state.database_pool) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&e.to_string()), - ) - })?; + trip.load_categories(&state.database_pool).await?; Ok(( StatusCode::OK, @@ -1511,13 +1256,7 @@ async fn trip_item_packagelist_set_pack_htmx( .await?; let item = models::TripItem::find(&state.database_pool, trip_id, item_id) - .await - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - })? + .await? .ok_or(( StatusCode::NOT_FOUND, components::ErrorPage::build(&format!("an item with id {item_id} does not exist")), @@ -1545,13 +1284,7 @@ async fn trip_item_packagelist_set_unpack_htmx( // note that this cannot fail due to a missing item, as trip_item_set_state would already // return 404. but error handling cannot hurt ;) let item = models::TripItem::find(&state.database_pool, trip_id, item_id) - .await - .map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&error.to_string()), - ) - })? + .await? .ok_or(( StatusCode::NOT_FOUND, components::ErrorPage::build(&format!("an item with id {item_id} does not exist")), diff --git a/rust/src/models.rs b/rust/src/models.rs index 4b5a203..f155eed 100644 --- a/rust/src/models.rs +++ b/rust/src/models.rs @@ -7,108 +7,21 @@ use sqlx::{ Decode, Row, }; use std::convert; -use std::error; use std::fmt; use std::num::TryFromIntError; use std::str::FromStr; use uuid::Uuid; -use sqlx::{error::DatabaseError, sqlite::SqlitePoolOptions}; - use futures::TryFutureExt; use futures::TryStreamExt; -use time::{error::Parse as TimeParse, format_description::FormatItem, macros::format_description}; +use time::{format_description::FormatItem, macros::format_description}; + +mod error; +pub use error::{DatabaseError, Error, QueryError}; pub const DATE_FORMAT: &[FormatItem<'static>] = format_description!("[year]-[month]-[day]"); -pub enum Error { - Sql { description: String }, - Uuid { description: String }, - Enum { description: String }, - Int { description: String }, - Constraint { description: String }, - TimeParse { description: String }, - Duplicate { description: String }, - NotFound { description: String }, - ReferenceNotFound { description: String }, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::Sql { description } => { - write!(f, "SQL error: {description}") - } - Self::Uuid { description } => { - write!(f, "UUID error: {description}") - } - Self::Int { description } => { - write!(f, "Integer error: {description}") - } - Self::Enum { description } => { - write!(f, "Enum error: {description}") - } - Self::TimeParse { description } => { - write!(f, "Date parse error: {description}") - } - Self::Constraint { description } => { - write!(f, "SQL constraint error: {description}") - } - Self::Duplicate { description } => { - write!(f, "Duplicate data entry: {description}") - } - Self::NotFound { description } => { - write!(f, "not found: {description}") - } - Self::ReferenceNotFound { description } => { - write!(f, "SQL foreign key reference was not found: {description}") - } - } - } -} - -impl fmt::Debug for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // defer to Display - write!(f, "SQL error: {self}") - } -} - -impl convert::From for Error { - fn from(value: uuid::Error) -> Self { - Error::Uuid { - description: value.to_string(), - } - } -} - -impl convert::From for Error { - fn from(value: sqlx::Error) -> Self { - Error::Sql { - description: value.to_string(), - } - } -} - -impl convert::From for Error { - fn from(value: TryFromIntError) -> Self { - Error::Int { - description: value.to_string(), - } - } -} - -impl convert::From for Error { - fn from(value: TimeParse) -> Self { - Error::TimeParse { - description: value.to_string(), - } - } -} - -impl error::Error for Error {} - #[derive(sqlx::Type, PartialEq, PartialOrd, Deserialize)] pub enum TripState { Init, @@ -176,9 +89,9 @@ impl std::convert::TryFrom<&str> for TripState { "Review" => Self::Review, "Done" => Self::Done, _ => { - return Err(Error::Enum { + return Err(Error::Database(DatabaseError::Enum { description: format!("{value} is not a valid value for TripState"), - }) + })) } }) } @@ -375,7 +288,7 @@ impl TripItem { ) -> Result, Error> { let item_id_param = item_id.to_string(); let trip_id_param = trip_id.to_string(); - let item: Result, sqlx::Error> = sqlx::query_as!( + let item: Result, Error> = sqlx::query_as!( DbTripsItemsRow, " SELECT @@ -398,12 +311,13 @@ impl TripItem { ) .fetch_one(pool) .map_ok(|row| row.try_into()) - .await; + .await + .map_err(|error| error.into()); match item { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(None), - _ => Err(e.into()), + Err(error) => match error { + Error::Query(QueryError::NotFound { description: _ }) => Ok(None), + _ => Err(error), }, Ok(v) => Ok(Some(v?)), } @@ -427,16 +341,13 @@ impl TripItem { .bind(trip_id.to_string()) .bind(item_id.to_string()) .execute(pool) - .map_err(|error| Error::Sql { - description: error.to_string(), - }) .await?; - (result.rows_affected() != 0) - .then_some(()) - .ok_or_else(|| Error::NotFound { + (result.rows_affected() != 0).then_some(()).ok_or_else(|| { + Error::Query(QueryError::NotFound { description: format!("item {item_id} not found for trip {trip_id}"), }) + }) } } @@ -553,12 +464,13 @@ impl Trip { ) .fetch_one(pool) .map_ok(|row| row.try_into()) + .map_err(|error| error.into()) .await; match trip { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(None), - _ => Err(e.into()), + Err(error) => match error { + Error::Query(QueryError::NotFound { description: _ }) => Ok(None), + _ => Err(error), }, Ok(v) => Ok(Some(v?)), } @@ -603,45 +515,6 @@ impl Trip { type_id_param, ) .execute(pool) - .map_err(|error| match error { - sqlx::Error::Database(ref error) => { - let sqlite_error = error.downcast_ref::(); - if let Some(code) = sqlite_error.code() { - match &*code { - "787" => { - // SQLITE_CONSTRAINT_FOREIGNKEY - Error::ReferenceNotFound { - description: format!("invalid id: {}", code.to_string()), - } - } - "2067" => { - // SQLITE_CONSTRAINT_UNIQUE - Error::Duplicate { - description: format!( - "type {type_id} is already active for trip {id}" - ), - } - } - _ => Error::Sql { - description: format!( - "got error with unknown code: {}", - sqlite_error.to_string() - ), - }, - } - } else { - Error::Sql { - description: format!( - "got error without code: {}", - sqlite_error.to_string() - ), - } - } - } - _ => Error::Sql { - description: format!("got unknown error: {}", error.to_string()), - }, - }) .await?; Ok(()) @@ -700,16 +573,13 @@ impl Trip { .bind(value) .bind(trip_id.to_string()) .execute(pool) - .map_err(|error| Error::Sql { - description: error.to_string(), - }) .await?; - (result.rows_affected() != 0) - .then_some(()) - .ok_or_else(|| Error::NotFound { + (result.rows_affected() != 0).then_some(()).ok_or_else(|| { + Error::Query(QueryError::NotFound { description: format!("trip {trip_id} not found"), }) + }) } pub async fn save( @@ -720,14 +590,8 @@ impl Trip { ) -> Result { let id = Uuid::new_v4(); let id_param = id.to_string(); - let date_start = date_start - .format(DATE_FORMAT) - .map_err(|e| Error::TimeParse { - description: e.to_string(), - })?; - let date_end = date_end.format(DATE_FORMAT).map_err(|e| Error::TimeParse { - description: e.to_string(), - })?; + let date_start = date_start.format(DATE_FORMAT)?; + let date_end = date_end.format(DATE_FORMAT)?; let trip_state = TripState::new(); @@ -743,42 +607,7 @@ impl Trip { trip_state, ) .execute(pool) - .await - .map_err(|e| match e { - sqlx::Error::Database(ref error) => { - let sqlite_error = error.downcast_ref::(); - if let Some(code) = sqlite_error.code() { - match &*code { - // SQLITE_CONSTRAINT_FOREIGNKEY - "787" => Error::Constraint { - description: format!( - "SQLITE_CONSTRAINT_FOREIGNKEY on table without foreignkey?", - ), - }, - // SQLITE_CONSTRAINT_UNIQUE - "2067" => Error::Constraint { - description: format!("trip with name \"{name}\" already exists",), - }, - _ => Error::Sql { - description: format!( - "got error with unknown code: {}", - sqlite_error.to_string() - ), - }, - } - } else { - Error::Sql { - description: format!( - "got error without code: {}", - sqlite_error.to_string() - ), - } - } - } - _ => Error::Sql { - description: format!("got unknown error: {}", e.to_string()), - }, - })?; + .await?; Ok(id) } @@ -806,12 +635,13 @@ impl Trip { ) .fetch_one(pool) .map_ok(|row| row.total_weight.map(|weight| weight as i64)) + .map_err(|error| error.into()) .await; match weight { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(None), - _ => Err(e.into()), + Err(error) => match error { + Error::Query(QueryError::NotFound { description: _ }) => Ok(None), + _ => Err(error.into()), }, Ok(v) => Ok(v), } @@ -1100,39 +930,6 @@ impl TripsType { name, ) .execute(pool) - .map_err(|error| match error { - sqlx::Error::Database(ref error) => { - let sqlite_error = error.downcast_ref::(); - if let Some(code) = sqlite_error.code() { - match &*code { - "2067" => { - // SQLITE_CONSTRAINT_UNIQUE - Error::Duplicate { - description: format!( - "trip type with name \"{name}\" already exists" - ), - } - } - _ => Error::Sql { - description: format!( - "got error with unknown code: {}", - sqlite_error.to_string() - ), - }, - } - } else { - Error::Sql { - description: format!( - "got error without code: {}", - sqlite_error.to_string() - ), - } - } - } - _ => Error::Sql { - description: format!("got unknown error: {}", error.to_string()), - }, - }) .await?; Ok(id) @@ -1200,7 +997,7 @@ impl Category { id: Uuid, ) -> Result, Error> { let id_param = id.to_string(); - let item: Result, sqlx::Error> = sqlx::query_as!( + let item = sqlx::query_as!( DbCategoryRow, "SELECT id, @@ -1212,12 +1009,13 @@ impl Category { ) .fetch_one(pool) .map_ok(|row| row.try_into()) + .map_err(|error| error.into()) .await; match item { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(None), - _ => Err(e.into()), + Err(error) => match error { + Error::Query(QueryError::NotFound { description: _ }) => Ok(None), + _ => Err(error), }, Ok(v) => Ok(Some(v?)), } @@ -1235,39 +1033,6 @@ impl Category { name, ) .execute(pool) - .map_err(|error| match error { - sqlx::Error::Database(ref error) => { - let sqlite_error = error.downcast_ref::(); - if let Some(code) = sqlite_error.code() { - match &*code { - "2067" => { - // SQLITE_CONSTRAINT_UNIQUE - Error::Duplicate { - description: format!( - "inventory item category with name \"{name}\" already exists" - ), - } - } - _ => Error::Sql { - description: format!( - "got error with unknown code: {}", - sqlite_error.to_string() - ), - }, - } - } else { - Error::Sql { - description: format!( - "got error without code: {}", - sqlite_error.to_string() - ), - } - } - } - _ => Error::Sql { - description: format!("got unknown error: {}", error.to_string()), - }, - }) .await?; Ok(id) @@ -1335,7 +1100,7 @@ impl TryFrom for Item { impl Item { pub async fn find(pool: &sqlx::Pool, id: Uuid) -> Result, Error> { let id_param = id.to_string(); - let item: Result, sqlx::Error> = sqlx::query_as!( + let item = sqlx::query_as!( DbInventoryItemsRow, "SELECT id, @@ -1348,13 +1113,14 @@ impl Item { id_param, ) .fetch_one(pool) + .map_err(|error| error.into()) .map_ok(|row| row.try_into()) .await; match item { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(None), - _ => Err(e.into()), + Err(error) => match error { + Error::Query(QueryError::NotFound { description: _ }) => Ok(None), + _ => Err(error), }, Ok(v) => Ok(Some(v?)), } @@ -1367,7 +1133,7 @@ impl Item { weight: i64, ) -> Result, Error> { let id_param = id.to_string(); - let id: Result, sqlx::Error> = sqlx::query!( + let id = sqlx::query!( "UPDATE inventory_items AS item SET name = ?, @@ -1383,15 +1149,16 @@ impl Item { .map_ok(|row| { let id: &str = &row.id.unwrap(); // TODO let uuid: Result = Uuid::try_parse(id); - let uuid: Result = uuid.map_err(|e| e.into()); + let uuid: Result = uuid.map_err(|error| error.into()); uuid }) + .map_err(|error| error.into()) .await; match id { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(None), - _ => Err(e.into()), + Err(error) => match error { + Error::Query(QueryError::NotFound { description: _ }) => Ok(None), + _ => Err(error.into()), }, Ok(v) => Ok(Some(v?)), } @@ -1402,7 +1169,7 @@ impl Item { category_id: Uuid, ) -> Result { let category_id_param = category_id.to_string(); - let weight: Result = sqlx::query!( + let weight = sqlx::query!( " SELECT COALESCE(MAX(i_item.weight), 0) as weight FROM inventory_items_categories as category @@ -1420,9 +1187,9 @@ impl Item { // We can be certain that the row exists, as we COALESCE it row.weight.unwrap() as i64 }) - .await; + .await?; - Ok(weight?) + Ok(weight) } pub async fn _get_category_total_picked_weight( @@ -1430,7 +1197,7 @@ impl Item { category_id: Uuid, ) -> Result { let category_id_param = category_id.to_string(); - let weight: Result = sqlx::query!( + let weight: Result = sqlx::query!( " SELECT COALESCE(SUM(i_item.weight), 0) as weight FROM inventory_items_categories as category @@ -1451,6 +1218,7 @@ impl Item { // We can be certain that the row exists, as we COALESCE it row.weight.unwrap() as i64 }) + .map_err(|error| error.into()) .await; Ok(weight?) @@ -1548,7 +1316,7 @@ impl InventoryItem { pub async fn find(pool: &sqlx::Pool, id: Uuid) -> Result, Error> { let id_param = id.to_string(); - let item: Result, sqlx::Error> = sqlx::query_as!( + let item = sqlx::query_as!( DbInventoryItemRow, "SELECT item.id AS id, @@ -1572,19 +1340,20 @@ impl InventoryItem { ) .fetch_one(pool) .map_ok(|row| row.try_into()) + .map_err(|error| error.into()) .await; match item { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(None), - _ => Err(e.into()), + Err(error) => match error { + Error::Query(QueryError::NotFound { description: _ }) => Ok(None), + _ => Err(error.into()), }, Ok(v) => Ok(Some(v?)), } } pub async fn name_exists(pool: &sqlx::Pool, name: &str) -> Result { - let item: Result<(), sqlx::Error> = sqlx::query!( + let item = sqlx::query!( "SELECT id FROM inventory_items WHERE name = ?", @@ -1592,12 +1361,13 @@ impl InventoryItem { ) .fetch_one(pool) .map_ok(|_row| ()) + .map_err(|error| error.into()) .await; match item { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(false), - _ => Err(e.into()), + Err(error) => match error { + Error::Query(QueryError::NotFound { description: _ }) => Ok(false), + _ => Err(error.into()), }, Ok(_) => Ok(true), } @@ -1611,24 +1381,7 @@ impl InventoryItem { id_param ) .execute(pool) - .map_err(|error| match error { - sqlx::Error::Database(ref error) => { - let sqlite_error = error.downcast_ref::(); - if let Some(code) = sqlite_error.code() { - match &*code { - "787" => { - // SQLITE_CONSTRAINT_FOREIGNKEY - Error::Constraint { description: format!("item {} cannot be deleted because it's on use in trips. instead, archive it", code.to_string()) } - } - _ => - Error::Sql { description: format!("got error with unknown code: {}", sqlite_error.to_string()) } - } - } else { - Error::Constraint { description: format!("got error without code: {}", sqlite_error.to_string()) } - } - } - _ => Error::Constraint { description: format!("got unknown error: {}", error.to_string()) } - }).await?; + .await?; Ok(results.rows_affected() != 0) } @@ -1655,44 +1408,7 @@ impl InventoryItem { category_id_param ) .execute(pool) - .await - .map_err(|e| match e { - sqlx::Error::Database(ref error) => { - let sqlite_error = error.downcast_ref::(); - if let Some(code) = sqlite_error.code() { - match &*code { - // SQLITE_CONSTRAINT_FOREIGNKEY - "787" => Error::Constraint { - description: format!( - "category {category_id} not found", - ), - }, - // SQLITE_CONSTRAINT_UNIQUE - "2067" => Error::Constraint { - description: format!( - "item with name \"{name}\" already exists in category {category_id}", - ), - }, - _ => Error::Sql { - description: format!( - "got error with unknown code: {}", - sqlite_error.to_string() - ), - }, - } - } else { - Error::Sql { - description: format!( - "got error without code: {}", - sqlite_error.to_string() - ), - } - } - } - _ => Error::Sql { - description: format!("got unknown error: {}", e.to_string()), - }, - })?; + .await?; Ok(id) } @@ -1714,15 +1430,19 @@ impl Inventory { .await // we have two error handling lines here. these are distinct errors // this one is the SQL error that may arise during the query - .map_err(|e| Error::Sql { - description: e.to_string(), + .map_err(|error| { + Error::Database(DatabaseError::Sql { + description: error.to_string(), + }) })? .into_iter() .collect::, Error>>() // and this one is the model mapping error that may arise e.g. during // reading of the rows - .map_err(|e| Error::Sql { - description: e.to_string(), + .map_err(|error| { + Error::Database(DatabaseError::Sql { + description: error.to_string(), + }) })?; for category in &mut categories { diff --git a/rust/src/models/error.rs b/rust/src/models/error.rs new file mode 100644 index 0000000..3a28b0e --- /dev/null +++ b/rust/src/models/error.rs @@ -0,0 +1,165 @@ +use std::convert; +use std::fmt; + +use sqlx::error::DatabaseError as _; + +pub enum DatabaseError { + /// Errors we can receive **from** the database that are caused by connection + /// problems or schema problems (e.g. we get a return value that does not fit our enum, + /// or a wrongly formatted date) + Sql { + description: String, + }, + Uuid { + description: String, + }, + Enum { + description: String, + }, + TimeParse { + description: String, + }, +} + +impl fmt::Display for DatabaseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Sql { description } => { + write!(f, "SQL error: {description}") + } + Self::Uuid { description } => { + write!(f, "UUID error: {description}") + } + Self::Enum { description } => { + write!(f, "Enum error: {description}") + } + Self::TimeParse { description } => { + write!(f, "Date parse error: {description}") + } + } + } +} + +pub enum QueryError { + /// Errors that are caused by wrong input data, e.g. ids that cannot be found, or + /// inserts that violate unique constraints + Constraint { + description: String, + }, + Duplicate { + description: String, + }, + NotFound { + description: String, + }, + ReferenceNotFound { + description: String, + }, +} + +impl fmt::Display for QueryError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Constraint { description } => { + write!(f, "SQL constraint error: {description}") + } + Self::Duplicate { description } => { + write!(f, "Duplicate data entry: {description}") + } + Self::NotFound { description } => { + write!(f, "not found: {description}") + } + Self::ReferenceNotFound { description } => { + write!(f, "SQL foreign key reference was not found: {description}") + } + } + } +} + +pub enum Error { + Database(DatabaseError), + Query(QueryError), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Database(error) => write!(f, "{}", error), + Self::Query(error) => write!(f, "{}", error), + } + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // defer to Display + write!(f, "SQL error: {self}") + } +} + +impl convert::From for Error { + fn from(value: uuid::Error) -> Self { + Error::Database(DatabaseError::Uuid { + description: value.to_string(), + }) + } +} + +impl convert::From for Error { + fn from(value: time::error::Format) -> Self { + Error::Database(DatabaseError::TimeParse { + description: value.to_string(), + }) + } +} + +impl convert::From for Error { + fn from(value: sqlx::Error) -> Self { + match value { + sqlx::Error::RowNotFound => Error::Query(QueryError::NotFound { + description: value.to_string(), + }), + sqlx::Error::Database(ref error) => { + let sqlite_error = error.downcast_ref::(); + if let Some(code) = sqlite_error.code() { + match &*code { + // SQLITE_CONSTRAINT_FOREIGNKEY + "787" => Error::Query(QueryError::Constraint { + description: format!("foreign key reference not found"), + }), + // SQLITE_CONSTRAINT_UNIQUE + "2067" => Error::Query(QueryError::Constraint { + description: format!("item with unique constraint already exists",), + }), + _ => Error::Database(DatabaseError::Sql { + description: format!( + "got error with unknown code: {}", + sqlite_error.to_string() + ), + }), + } + } else { + Error::Database(DatabaseError::Sql { + description: format!( + "got error without code: {}", + sqlite_error.to_string() + ), + }) + } + } + _ => Error::Database(DatabaseError::Sql { + description: format!("got unknown error: {}", value.to_string()), + }), + } + } +} + +impl convert::From for Error { + fn from(value: time::error::Parse) -> Self { + Error::Database(DatabaseError::TimeParse { + description: value.to_string(), + }) + } +} + +impl std::error::Error for Error {}