more error refactoring

This commit is contained in:
2023-08-29 21:34:00 +02:00
parent ccb666f000
commit 3a50f3e9f0
7 changed files with 348 additions and 494 deletions

View File

@@ -1,8 +1,8 @@
use maud::{html, Markup, PreEscaped}; use maud::{html, Markup};
use crate::models; use crate::models;
use crate::ClientState; use crate::ClientState;
use uuid::{uuid, Uuid}; use uuid::Uuid;
pub struct Inventory; pub struct Inventory;

View File

@@ -6,7 +6,6 @@ use uuid::Uuid;
use serde_variant::to_variant_name; use serde_variant::to_variant_name;
use crate::ClientState;
pub struct TripManager; pub struct TripManager;
pub mod packagelist; pub mod packagelist;

114
rust/src/error.rs Normal file
View File

@@ -0,0 +1,114 @@
use std::fmt;
use crate::components;
use crate::models;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
pub enum RequestError {
EmptyFormElement { name: String },
RefererNotFound,
RefererInvalid { message: String },
NotFound { message: String },
}
impl fmt::Display for RequestError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::EmptyFormElement { name } => write!(f, "Form element {name} cannot be empty"),
Self::RefererNotFound => write!(f, "Referer header not found"),
Self::RefererInvalid { message } => write!(f, "Referer header invalid: {message}"),
Self::NotFound { message } => write!(f, "Not found: {message}"),
}
}
}
pub enum Error {
Model(models::Error),
Request(RequestError),
}
#[derive(Debug)]
pub enum StartError {
DatabaseInitError { message: String },
DatabaseMigrationError { message: String },
}
impl From<sqlx::Error> for StartError {
fn from(value: sqlx::Error) -> Self {
Self::DatabaseInitError {
message: value.to_string(),
}
}
}
impl From<sqlx::migrate::MigrateError> for StartError {
fn from(value: sqlx::migrate::MigrateError) -> Self {
Self::DatabaseMigrationError {
message: value.to_string(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Model(model_error) => write!(f, "Model error: {model_error}"),
Self::Request(request_error) => write!(f, "Request error: {request_error}"),
}
}
}
impl From<models::Error> for Error {
fn from(value: models::Error) -> Self {
Self::Model(value)
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
match self {
Self::Model(ref model_error) => match model_error {
models::Error::Database(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
components::ErrorPage::build(&format!("{}", self)),
),
models::Error::Query(error) => match error {
models::QueryError::NotFound { description } => (
StatusCode::NOT_FOUND,
components::ErrorPage::build(&description),
),
_ => (
StatusCode::BAD_REQUEST,
components::ErrorPage::build(&format!("{}", error)),
),
},
},
Self::Request(request_error) => match request_error {
RequestError::RefererNotFound => (
StatusCode::BAD_REQUEST,
components::ErrorPage::build("no referer header found"),
),
RequestError::RefererInvalid { message } => (
StatusCode::BAD_REQUEST,
components::ErrorPage::build(&format!(
"referer could not be converted: {}",
message
)),
),
RequestError::EmptyFormElement { name } => (
StatusCode::UNPROCESSABLE_ENTITY,
components::ErrorPage::build(&format!("empty form element: {}", name)),
),
RequestError::NotFound { message } => (
StatusCode::NOT_FOUND,
components::ErrorPage::build(&format!("not found: {}", message)),
),
},
}
.into_response()
}
}

View File

@@ -1,45 +1,35 @@
#![allow(unused_imports)]
use axum::{ use axum::{
extract::{Path, Query, State}, extract::{Path, Query, State},
headers,
headers::Header,
http::{ http::{
header, header,
header::{HeaderMap, HeaderName, HeaderValue}, header::{HeaderMap, HeaderName, HeaderValue},
StatusCode, StatusCode,
}, },
response::{Html, IntoResponse, Redirect, Response}, response::{IntoResponse, Redirect},
routing::{get, post}, routing::{get, post},
Form, Router, Form, Router,
}; };
use maud::html; use maud::html;
use std::str::FromStr;
use serde_variant::to_variant_name;
use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
Pool, Sqlite,
};
use maud::Markup; use maud::Markup;
use serde::Deserialize; use serde::Deserialize;
use futures::TryFutureExt; use uuid::Uuid;
use futures::TryStreamExt;
use uuid::{uuid, Uuid};
use std::net::SocketAddr; use std::net::SocketAddr;
mod components; mod components;
mod error;
mod models; mod models;
mod sqlite;
use error::{Error, RequestError, StartError};
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
database_pool: Pool<Sqlite>, database_pool: sqlite::Pool<sqlite::Sqlite>,
client_state: ClientState, client_state: ClientState,
} }
@@ -124,22 +114,15 @@ impl From<HtmxRequestHeaders> for HeaderName {
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), sqlx::Error> { async fn main() -> Result<(), StartError> {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG) .with_max_level(tracing::Level::DEBUG)
.init(); .init();
let args = Args::parse(); let args = Args::parse();
let database_pool = SqlitePoolOptions::new() let database_pool = sqlite::init_database_pool(&args.database_url).await?;
.max_connections(5) sqlite::migrate(&database_pool).await?;
.connect_with(
SqliteConnectOptions::from_str(&args.database_url)?.pragma("foreign_keys", "1"),
)
.await
.unwrap();
sqlx::migrate!().run(&database_pool).await?;
let state = AppState { let state = AppState {
database_pool, database_pool,
@@ -158,6 +141,14 @@ async fn main() -> Result<(), sqlx::Error> {
.route("/favicon.svg", get(icon_handler)) .route("/favicon.svg", get(icon_handler))
.route("/assets/luggage.svg", get(icon_handler)) .route("/assets/luggage.svg", get(icon_handler))
.route("/", get(root)) .route("/", get(root))
.route(
"/notfound",
get(|| async {
Error::Request(RequestError::NotFound {
message: "hi".to_string(),
})
}),
)
.nest( .nest(
"/trips/", "/trips/",
Router::new() Router::new()
@@ -212,7 +203,7 @@ async fn main() -> Result<(), sqlx::Error> {
.route("/item/:id/edit", post(inventory_item_edit)) .route("/item/:id/edit", post(inventory_item_edit))
.route("/item/name/validate", post(inventory_item_validate_name)), .route("/item/name/validate", post(inventory_item_validate_name)),
) )
.fallback(|| async { (StatusCode::NOT_FOUND, "not found") }) .fallback(|| async { (StatusCode::NOT_FOUND, "no route found") })
.with_state(state); .with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], args.port)); let addr = SocketAddr::from(([127, 0, 0, 1], args.port));
@@ -244,7 +235,7 @@ async fn inventory_active(
State(mut state): State<AppState>, State(mut state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Query(inventory_query): Query<InventoryQuery>, Query(inventory_query): Query<InventoryQuery>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
state.client_state.edit_item = inventory_query.edit_item; state.client_state.edit_item = inventory_query.edit_item;
state.client_state.active_category_id = Some(id); state.client_state.active_category_id = Some(id);
@@ -258,12 +249,9 @@ async fn inventory_active(
.categories .categories
.iter() .iter()
.find(|category| category.id == id) .find(|category| category.id == id)
.ok_or(( .ok_or(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("a category with id {id} does not exist"),
components::ErrorPage::build(&format!( }))
"a category with id {id} does not exist"
)),
))
}) })
.transpose()?; .transpose()?;
@@ -283,7 +271,7 @@ async fn inventory_active(
async fn inventory_inactive( async fn inventory_inactive(
State(mut state): State<AppState>, State(mut state): State<AppState>,
Query(inventory_query): Query<InventoryQuery>, Query(inventory_query): Query<InventoryQuery>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
state.client_state.edit_item = inventory_query.edit_item; state.client_state.edit_item = inventory_query.edit_item;
state.client_state.active_category_id = None; state.client_state.active_category_id = None;
@@ -323,7 +311,7 @@ struct NewItemName {
async fn inventory_item_validate_name( async fn inventory_item_validate_name(
State(state): State<AppState>, State(state): State<AppState>,
Form(new_item): Form<NewItemName>, Form(new_item): Form<NewItemName>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
let exists = models::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?; let exists = models::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?;
Ok(( Ok((
@@ -332,40 +320,18 @@ async fn inventory_item_validate_name(
)) ))
} }
impl From<models::Error> 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( async fn inventory_item_create(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
Form(new_item): Form<NewItem>, Form(new_item): Form<NewItem>,
) -> Result<impl IntoResponse, (StatusCode, Markup)> { ) -> Result<impl IntoResponse, Error> {
if new_item.name.is_empty() { if new_item.name.is_empty() {
return Err(( return Err(Error::Request(RequestError::EmptyFormElement {
StatusCode::UNPROCESSABLE_ENTITY, name: "name".to_string(),
components::ErrorPage::build("name cannot be empty"), }));
));
} }
let new_id = models::InventoryItem::save( let _new_id = models::InventoryItem::save(
&state.database_pool, &state.database_pool,
&new_item.name, &new_item.name,
new_item.category_id, new_item.category_id,
@@ -377,18 +343,13 @@ async fn inventory_item_create(
let inventory = models::Inventory::load(&state.database_pool).await?; let inventory = models::Inventory::load(&state.database_pool).await?;
// it's impossible to NOT find the item here, as we literally just added // it's impossible to NOT find the item here, as we literally just added
// it. but good error handling never hurts // it.
let active_category: Option<&models::Category> = Some( let active_category: Option<&models::Category> = Some(
inventory inventory
.categories .categories
.iter() .iter()
.find(|category| category.id == new_item.category_id) .find(|category| category.id == new_item.category_id)
.ok_or(( .unwrap(),
StatusCode::NOT_FOUND,
components::ErrorPage::build(&format!(
"a category with id {new_id} was inserted but does not exist, this is a bug"
)),
))?,
); );
Ok(( Ok((
@@ -409,37 +370,31 @@ async fn inventory_item_create(
} }
} }
fn get_referer<'a>(headers: &'a HeaderMap) -> Result<&'a str, Error> {
headers
.get("referer")
.ok_or(Error::Request(RequestError::RefererNotFound))?
.to_str()
.map_err(|error| {
Error::Request(RequestError::RefererInvalid {
message: error.to_string(),
})
})
}
async fn inventory_item_delete( async fn inventory_item_delete(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
let deleted = models::InventoryItem::delete(&state.database_pool, id).await?; let deleted = models::InventoryItem::delete(&state.database_pool, id).await?;
if !deleted { if !deleted {
Err(( Err(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("item with id {id} not found"),
components::ErrorPage::build(&format!("item with id {id} not found")), }))
))
} else { } else {
Ok(Redirect::to( Ok(Redirect::to(get_referer(&headers)?))
headers
.get("referer")
.ok_or((
StatusCode::BAD_REQUEST,
components::ErrorPage::build("no referer header found"),
))?
.to_str()
.map_err(|error| {
(
StatusCode::BAD_REQUEST,
components::ErrorPage::build(&format!(
"referer could not be converted: {}",
error
)),
)
})?,
))
} }
} }
@@ -455,30 +410,15 @@ async fn inventory_item_edit(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Form(edit_item): Form<EditItem>, Form(edit_item): Form<EditItem>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
if edit_item.name.is_empty() { if edit_item.name.is_empty() {
return Err(( return Err(Error::Request(RequestError::EmptyFormElement {
StatusCode::UNPROCESSABLE_ENTITY, name: "name".to_string(),
components::ErrorPage::build("name cannot be empty"), }));
));
} }
let id = models::Item::update( let id =
&state.database_pool, models::Item::update(&state.database_pool, id, &edit_item.name, edit_item.weight).await?;
id,
&edit_item.name,
i64::try_from(edit_item.weight).map_err(|error| {
(
StatusCode::UNPROCESSABLE_ENTITY,
components::ErrorPage::build(&error.to_string()),
)
})?,
)
.await?
.ok_or((
StatusCode::NOT_FOUND,
components::ErrorPage::build(&format!("item with id {id} not found", id = id)),
))?;
Ok(Redirect::to(&format!("/inventory/category/{id}/", id = id))) Ok(Redirect::to(&format!("/inventory/category/{id}/", id = id)))
} }
@@ -486,11 +426,12 @@ async fn inventory_item_edit(
async fn inventory_item_cancel( async fn inventory_item_cancel(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
let id = models::Item::find(&state.database_pool, id).await?.ok_or(( let id = models::Item::find(&state.database_pool, id)
StatusCode::NOT_FOUND, .await?
components::ErrorPage::build(&format!("item with id {id} not found")), .ok_or(Error::Request(RequestError::NotFound {
))?; message: format!("item with id {id} not found"),
}))?;
Ok(Redirect::to(&format!( Ok(Redirect::to(&format!(
"/inventory/category/{id}/", "/inventory/category/{id}/",
@@ -511,12 +452,11 @@ struct NewTrip {
async fn trip_create( async fn trip_create(
State(state): State<AppState>, State(state): State<AppState>,
Form(new_trip): Form<NewTrip>, Form(new_trip): Form<NewTrip>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
if new_trip.name.is_empty() { if new_trip.name.is_empty() {
return Err(( return Err(Error::Request(RequestError::EmptyFormElement {
StatusCode::UNPROCESSABLE_ENTITY, name: "name".to_string(),
components::ErrorPage::build("name cannot be empty"), }));
));
} }
let new_id = models::Trip::save( let new_id = models::Trip::save(
@@ -530,9 +470,7 @@ async fn trip_create(
Ok(Redirect::to(&format!("/trips/{new_id}/"))) Ok(Redirect::to(&format!("/trips/{new_id}/")))
} }
async fn trips( async fn trips(State(state): State<AppState>) -> Result<(StatusCode, Markup), Error> {
State(state): State<AppState>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
let trips = models::Trip::all(&state.database_pool).await?; let trips = models::Trip::all(&state.database_pool).await?;
Ok(( Ok((
@@ -554,14 +492,16 @@ async fn trip(
State(mut state): State<AppState>, State(mut state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Query(trip_query): Query<TripQuery>, Query(trip_query): Query<TripQuery>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
state.client_state.trip_edit_attribute = trip_query.edit; state.client_state.trip_edit_attribute = trip_query.edit;
state.client_state.active_category_id = trip_query.category; state.client_state.active_category_id = trip_query.category;
let mut trip: models::Trip = models::Trip::find(&state.database_pool, id).await?.ok_or(( let mut trip: models::Trip =
StatusCode::NOT_FOUND, models::Trip::find(&state.database_pool, id)
components::ErrorPage::build(&format!("trip with id {} not found", id)), .await?
))?; .ok_or(Error::Request(RequestError::NotFound {
message: format!("trip with id {id} not found"),
}))?;
trip.load_trips_types(&state.database_pool).await?; trip.load_trips_types(&state.database_pool).await?;
@@ -577,12 +517,9 @@ async fn trip(
trip.categories() trip.categories()
.iter() .iter()
.find(|category| category.category.id == id) .find(|category| category.category.id == id)
.ok_or(( .ok_or(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("an active category with id {id} does not exist"),
components::ErrorPage::build(&format!( }))
"an active category with id {id} does not exist"
)),
))
}) })
.transpose()?; .transpose()?;
@@ -602,16 +539,13 @@ async fn trip(
async fn trip_type_remove( async fn trip_type_remove(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>, Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
let found = models::Trip::trip_type_remove(&state.database_pool, trip_id, type_id).await?; let found = models::Trip::trip_type_remove(&state.database_pool, trip_id, type_id).await?;
if !found { if !found {
Err(( Err(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("type {type_id} is not active for trip {trip_id}"),
components::ErrorPage::build(&format!( }))
"type {type_id} is not active for trip {trip_id}"
)),
))
} else { } else {
Ok(Redirect::to(&format!("/trips/{trip_id}/"))) Ok(Redirect::to(&format!("/trips/{trip_id}/")))
} }
@@ -620,7 +554,7 @@ async fn trip_type_remove(
async fn trip_type_add( async fn trip_type_add(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, type_id)): Path<(Uuid, Uuid)>, Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
models::Trip::trip_type_add(&state.database_pool, trip_id, type_id).await?; models::Trip::trip_type_add(&state.database_pool, trip_id, type_id).await?;
Ok(Redirect::to(&format!("/trips/{trip_id}/"))) Ok(Redirect::to(&format!("/trips/{trip_id}/")))
@@ -636,16 +570,15 @@ async fn trip_comment_set(
State(state): State<AppState>, State(state): State<AppState>,
Path(trip_id): Path<Uuid>, Path(trip_id): Path<Uuid>,
Form(comment_update): Form<CommentUpdate>, Form(comment_update): Form<CommentUpdate>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
let found = let found =
models::Trip::set_comment(&state.database_pool, trip_id, &comment_update.new_comment) models::Trip::set_comment(&state.database_pool, trip_id, &comment_update.new_comment)
.await?; .await?;
if !found { if !found {
Err(( Err(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("trip with id {trip_id} not found"),
components::ErrorPage::build(&format!("trip with id {id} not found", id = trip_id)), }))
))
} else { } else {
Ok(Redirect::to(&format!("/trips/{id}/", id = trip_id))) Ok(Redirect::to(&format!("/trips/{id}/", id = trip_id)))
} }
@@ -661,13 +594,12 @@ async fn trip_edit_attribute(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, attribute)): Path<(Uuid, models::TripAttribute)>, Path((trip_id, attribute)): Path<(Uuid, models::TripAttribute)>,
Form(trip_update): Form<TripUpdate>, Form(trip_update): Form<TripUpdate>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
if attribute == models::TripAttribute::Name { if attribute == models::TripAttribute::Name {
if trip_update.new_value.is_empty() { if trip_update.new_value.is_empty() {
return Err(( return Err(Error::Request(RequestError::EmptyFormElement {
StatusCode::UNPROCESSABLE_ENTITY, name: "name".to_string(),
components::ErrorPage::build("name cannot be empty"), }));
));
} }
} }
models::Trip::set_attribute( models::Trip::set_attribute(
@@ -687,26 +619,18 @@ async fn trip_item_set_state(
item_id: Uuid, item_id: Uuid,
key: models::TripItemStateKey, key: models::TripItemStateKey,
value: bool, value: bool,
) -> Result<(), (StatusCode, Markup)> { ) -> Result<(), Error> {
models::TripItem::set_state(&state.database_pool, trip_id, item_id, key, value).await?; models::TripItem::set_state(&state.database_pool, trip_id, item_id, key, value).await?;
Ok(()) Ok(())
} }
async fn trip_row( async fn trip_row(state: &AppState, trip_id: Uuid, item_id: Uuid) -> Result<Markup, Error> {
state: &AppState,
trip_id: Uuid,
item_id: Uuid,
) -> Result<Markup, (StatusCode, Markup)> {
let item = models::TripItem::find(&state.database_pool, trip_id, item_id) let item = models::TripItem::find(&state.database_pool, trip_id, item_id)
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
( Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("item with id {item_id} not found for trip {trip_id}"),
components::ErrorPage::build(&format!( })
"item with id {} not found for trip {}",
item_id, trip_id
)),
)
})?; })?;
let item_row = components::trip::TripItemListRow::build( let item_row = components::trip::TripItemListRow::build(
@@ -718,13 +642,9 @@ async fn trip_row(
let category = models::TripCategory::find(&state.database_pool, trip_id, item.item.category_id) let category = models::TripCategory::find(&state.database_pool, trip_id, item.item.category_id)
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
( Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("category with id {} not found", item.item.category_id),
components::ErrorPage::build(&format!( })
"category with id {} not found",
item.item.category_id
)),
)
})?; })?;
// TODO biggest_category_weight? // TODO biggest_category_weight?
@@ -738,8 +658,8 @@ async fn trip_item_set_pick(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
Ok::<_, models::Error>( Ok::<_, Error>(
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -749,32 +669,13 @@ async fn trip_item_set_pick(
) )
.await?, .await?,
) )
.map(|_| -> Result<Redirect, (StatusCode, Markup)> { .map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
Ok(Redirect::to(
headers
.get("referer")
.ok_or((
StatusCode::BAD_REQUEST,
components::ErrorPage::build("no referer header found"),
))?
.to_str()
.map_err(|error| {
(
StatusCode::BAD_REQUEST,
components::ErrorPage::build(&format!(
"referer could not be converted: {}",
error
)),
)
})?,
))
})?
} }
async fn trip_item_set_pick_htmx( async fn trip_item_set_pick_htmx(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, HeaderMap, Markup), Error> {
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -799,8 +700,8 @@ async fn trip_item_set_unpick(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
Ok::<_, models::Error>( Ok::<_, Error>(
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -810,32 +711,13 @@ async fn trip_item_set_unpick(
) )
.await?, .await?,
) )
.map(|_| -> Result<Redirect, (StatusCode, Markup)> { .map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
Ok(Redirect::to(
headers
.get("referer")
.ok_or((
StatusCode::BAD_REQUEST,
components::ErrorPage::build("no referer header found"),
))?
.to_str()
.map_err(|error| {
(
StatusCode::BAD_REQUEST,
components::ErrorPage::build(&format!(
"referer could not be converted: {}",
error
)),
)
})?,
))
})?
} }
async fn trip_item_set_unpick_htmx( async fn trip_item_set_unpick_htmx(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, HeaderMap, Markup), Error> {
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -860,8 +742,8 @@ async fn trip_item_set_pack(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
Ok::<_, models::Error>( Ok::<_, Error>(
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -871,32 +753,13 @@ async fn trip_item_set_pack(
) )
.await?, .await?,
) )
.map(|_| -> Result<Redirect, (StatusCode, Markup)> { .map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
Ok(Redirect::to(
headers
.get("referer")
.ok_or((
StatusCode::BAD_REQUEST,
components::ErrorPage::build("no referer header found"),
))?
.to_str()
.map_err(|error| {
(
StatusCode::BAD_REQUEST,
components::ErrorPage::build(&format!(
"referer could not be converted: {}",
error
)),
)
})?,
))
})?
} }
async fn trip_item_set_pack_htmx( async fn trip_item_set_pack_htmx(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, HeaderMap, Markup), Error> {
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -921,8 +784,8 @@ async fn trip_item_set_unpack(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
Ok::<_, models::Error>( Ok::<_, Error>(
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -932,32 +795,13 @@ async fn trip_item_set_unpack(
) )
.await?, .await?,
) )
.map(|_| -> Result<Redirect, (StatusCode, Markup)> { .map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
Ok(Redirect::to(
headers
.get("referer")
.ok_or((
StatusCode::BAD_REQUEST,
components::ErrorPage::build("no referer header found"),
))?
.to_str()
.map_err(|error| {
(
StatusCode::BAD_REQUEST,
components::ErrorPage::build(&format!(
"referer could not be converted: {}",
error
)),
)
})?,
))
})?
} }
async fn trip_item_set_unpack_htmx( async fn trip_item_set_unpack_htmx(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, HeaderMap, Markup), Error> {
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -981,13 +825,9 @@ async fn trip_item_set_unpack_htmx(
async fn trip_total_weight_htmx( async fn trip_total_weight_htmx(
State(state): State<AppState>, State(state): State<AppState>,
Path(trip_id): Path<Uuid>, Path(trip_id): Path<Uuid>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
let total_weight = models::Trip::find_total_picked_weight(&state.database_pool, trip_id) let total_weight =
.await? models::Trip::find_total_picked_weight(&state.database_pool, trip_id).await?;
.ok_or((
StatusCode::NOT_FOUND,
components::ErrorPage::build(&format!("trip with id {trip_id} not found")),
))?;
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
components::trip::TripInfoTotalWeightRow::build(trip_id, total_weight), components::trip::TripInfoTotalWeightRow::build(trip_id, total_weight),
@@ -1003,12 +843,11 @@ struct NewCategory {
async fn inventory_category_create( async fn inventory_category_create(
State(state): State<AppState>, State(state): State<AppState>,
Form(new_category): Form<NewCategory>, Form(new_category): Form<NewCategory>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
if new_category.name.is_empty() { if new_category.name.is_empty() {
return Err(( return Err(Error::Request(RequestError::EmptyFormElement {
StatusCode::UNPROCESSABLE_ENTITY, name: "name".to_string(),
components::ErrorPage::build("name cannot be empty"), }));
));
} }
let _new_id = models::Category::save(&state.database_pool, &new_category.name).await?; let _new_id = models::Category::save(&state.database_pool, &new_category.name).await?;
@@ -1020,14 +859,13 @@ async fn trip_state_set(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
Path((trip_id, new_state)): Path<(Uuid, models::TripState)>, Path((trip_id, new_state)): Path<(Uuid, models::TripState)>,
) -> Result<impl IntoResponse, (StatusCode, Markup)> { ) -> Result<impl IntoResponse, Error> {
let exists = models::Trip::set_state(&state.database_pool, trip_id, &new_state).await?; let exists = models::Trip::set_state(&state.database_pool, trip_id, &new_state).await?;
if !exists { if !exists {
return Err(( return Err(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("trip with id {trip_id} not found"),
components::ErrorPage::build(&format!("trip with id {id} not found", id = trip_id)), }));
));
} }
if is_htmx(&headers) { if is_htmx(&headers) {
@@ -1056,7 +894,7 @@ struct TripTypeQuery {
async fn trips_types( async fn trips_types(
State(mut state): State<AppState>, State(mut state): State<AppState>,
Query(trip_type_query): Query<TripTypeQuery>, Query(trip_type_query): Query<TripTypeQuery>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
state.client_state.trip_type_edit = trip_type_query.edit; state.client_state.trip_type_edit = trip_type_query.edit;
let trip_types: Vec<models::TripsType> = models::TripsType::all(&state.database_pool).await?; let trip_types: Vec<models::TripsType> = models::TripsType::all(&state.database_pool).await?;
@@ -1079,12 +917,11 @@ struct NewTripType {
async fn trip_type_create( async fn trip_type_create(
State(state): State<AppState>, State(state): State<AppState>,
Form(new_trip_type): Form<NewTripType>, Form(new_trip_type): Form<NewTripType>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
if new_trip_type.name.is_empty() { if new_trip_type.name.is_empty() {
return Err(( return Err(Error::Request(RequestError::EmptyFormElement {
StatusCode::UNPROCESSABLE_ENTITY, name: "name".to_string(),
components::ErrorPage::build("name cannot be empty"), }));
));
} }
let _new_id = models::TripsType::save(&state.database_pool, &new_trip_type.name).await?; let _new_id = models::TripsType::save(&state.database_pool, &new_trip_type.name).await?;
@@ -1102,12 +939,11 @@ async fn trips_types_edit_name(
State(state): State<AppState>, State(state): State<AppState>,
Path(trip_type_id): Path<Uuid>, Path(trip_type_id): Path<Uuid>,
Form(trip_update): Form<TripTypeUpdate>, Form(trip_update): Form<TripTypeUpdate>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, Error> {
if trip_update.new_value.is_empty() { if trip_update.new_value.is_empty() {
return Err(( return Err(Error::Request(RequestError::EmptyFormElement {
StatusCode::UNPROCESSABLE_ENTITY, name: "name".to_string(),
components::ErrorPage::build("name cannot be empty"), }));
));
} }
let exists = let exists =
@@ -1115,13 +951,9 @@ async fn trips_types_edit_name(
.await?; .await?;
if !exists { if !exists {
Err(( return Err(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("trip type with id {trip_type_id} not found"),
components::ErrorPage::build(&format!( }));
"tript type with id {id} not found",
id = trip_type_id
)),
))
} else { } else {
Ok(Redirect::to("/trips/types/")) Ok(Redirect::to("/trips/types/"))
} }
@@ -1130,13 +962,12 @@ async fn trips_types_edit_name(
async fn inventory_item( async fn inventory_item(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
let item = models::InventoryItem::find(&state.database_pool, id) let item = models::InventoryItem::find(&state.database_pool, id)
.await? .await?
.ok_or(( .ok_or(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("inventory item with id {id} not found"),
components::ErrorPage::build(&format!("inventory item with id {id} not found")), }))?;
))?;
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
@@ -1150,13 +981,12 @@ async fn inventory_item(
async fn trip_category_select( async fn trip_category_select(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, category_id)): Path<(Uuid, Uuid)>, Path((trip_id, category_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, HeaderMap, Markup), Error> {
let mut trip = models::Trip::find(&state.database_pool, trip_id) let mut trip = models::Trip::find(&state.database_pool, trip_id)
.await? .await?
.ok_or(( .ok_or(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("trip with id {trip_id} not found"),
components::ErrorPage::build(&format!("trip with id {trip_id} not found")), }))?;
))?;
trip.load_categories(&state.database_pool).await?; trip.load_categories(&state.database_pool).await?;
@@ -1164,10 +994,9 @@ async fn trip_category_select(
.categories() .categories()
.iter() .iter()
.find(|c| c.category.id == category_id) .find(|c| c.category.id == category_id)
.ok_or(( .ok_or(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("category with id {category_id} not found"),
components::ErrorPage::build(&format!("category with id {category_id} not found")), }))?;
))?;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert::<HeaderName>( headers.insert::<HeaderName>(
@@ -1185,7 +1014,7 @@ async fn trip_category_select(
async fn inventory_category_select( async fn inventory_category_select(
State(state): State<AppState>, State(state): State<AppState>,
Path(category_id): Path<Uuid>, Path(category_id): Path<Uuid>,
) -> Result<(StatusCode, HeaderMap, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, HeaderMap, Markup), Error> {
let inventory = models::Inventory::load(&state.database_pool).await?; let inventory = models::Inventory::load(&state.database_pool).await?;
let active_category: Option<&models::Category> = Some( let active_category: Option<&models::Category> = Some(
@@ -1193,12 +1022,9 @@ async fn inventory_category_select(
.categories .categories
.iter() .iter()
.find(|category| category.id == category_id) .find(|category| category.id == category_id)
.ok_or(( .ok_or(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("a category with id {category_id} not found"),
components::ErrorPage::build(&format!( }))?,
"a category with id {category_id} not found"
)),
))?,
); );
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
@@ -1223,13 +1049,12 @@ async fn inventory_category_select(
async fn trip_packagelist( async fn trip_packagelist(
State(state): State<AppState>, State(state): State<AppState>,
Path(trip_id): Path<Uuid>, Path(trip_id): Path<Uuid>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
let mut trip = models::Trip::find(&state.database_pool, trip_id) let mut trip = models::Trip::find(&state.database_pool, trip_id)
.await? .await?
.ok_or(( .ok_or(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("trip with id {trip_id} not found"),
components::ErrorPage::build(&format!("trip with id {trip_id} not found")), }))?;
))?;
trip.load_categories(&state.database_pool).await?; trip.load_categories(&state.database_pool).await?;
@@ -1245,7 +1070,7 @@ async fn trip_packagelist(
async fn trip_item_packagelist_set_pack_htmx( async fn trip_item_packagelist_set_pack_htmx(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -1257,10 +1082,9 @@ async fn trip_item_packagelist_set_pack_htmx(
let item = models::TripItem::find(&state.database_pool, trip_id, item_id) let item = models::TripItem::find(&state.database_pool, trip_id, item_id)
.await? .await?
.ok_or(( .ok_or(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("an item with id {item_id} does not exist"),
components::ErrorPage::build(&format!("an item with id {item_id} does not exist")), }))?;
))?;
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
@@ -1271,7 +1095,7 @@ async fn trip_item_packagelist_set_pack_htmx(
async fn trip_item_packagelist_set_unpack_htmx( async fn trip_item_packagelist_set_unpack_htmx(
State(state): State<AppState>, State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>, Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), Error> {
trip_item_set_state( trip_item_set_state(
&state, &state,
trip_id, trip_id,
@@ -1285,10 +1109,9 @@ async fn trip_item_packagelist_set_unpack_htmx(
// return 404. but error handling cannot hurt ;) // return 404. but error handling cannot hurt ;)
let item = models::TripItem::find(&state.database_pool, trip_id, item_id) let item = models::TripItem::find(&state.database_pool, trip_id, item_id)
.await? .await?
.ok_or(( .ok_or(Error::Request(RequestError::NotFound {
StatusCode::NOT_FOUND, message: format!("an item with id {item_id} does not exist"),
components::ErrorPage::build(&format!("an item with id {item_id} does not exist")), }))?;
))?;
Ok(( Ok((
StatusCode::OK, StatusCode::OK,

View File

@@ -1,15 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_variant::to_variant_name; use serde_variant::to_variant_name;
use sqlx::{
database::Database,
database::HasValueRef,
sqlite::{Sqlite, SqliteRow},
Decode, Row,
};
use std::convert;
use std::fmt; use std::fmt;
use std::num::TryFromIntError;
use std::str::FromStr;
use uuid::Uuid; use uuid::Uuid;
use futures::TryFutureExt; use futures::TryFutureExt;
@@ -288,7 +279,7 @@ impl TripItem {
) -> Result<Option<Self>, Error> { ) -> Result<Option<Self>, Error> {
let item_id_param = item_id.to_string(); let item_id_param = item_id.to_string();
let trip_id_param = trip_id.to_string(); let trip_id_param = trip_id.to_string();
let item: Result<Result<TripItem, Error>, Error> = sqlx::query_as!( sqlx::query_as!(
DbTripsItemsRow, DbTripsItemsRow,
" "
SELECT SELECT
@@ -309,18 +300,10 @@ impl TripItem {
item_id_param, item_id_param,
trip_id_param, trip_id_param,
) )
.fetch_one(pool) .fetch_optional(pool)
.map_ok(|row| row.try_into()) .await?
.await .map(|row| row.try_into())
.map_err(|error| error.into()); .transpose()
match item {
Err(error) => match error {
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
_ => Err(error),
},
Ok(v) => Ok(Some(v?)),
}
} }
pub async fn set_state( pub async fn set_state(
@@ -446,7 +429,7 @@ impl Trip {
trip_id: Uuid, trip_id: Uuid,
) -> Result<Option<Self>, Error> { ) -> Result<Option<Self>, Error> {
let trip_id_param = trip_id.to_string(); let trip_id_param = trip_id.to_string();
let trip = sqlx::query_as!( sqlx::query_as!(
DbTripRow, DbTripRow,
"SELECT "SELECT
id, id,
@@ -462,18 +445,10 @@ impl Trip {
WHERE id = ?", WHERE id = ?",
trip_id_param trip_id_param
) )
.fetch_one(pool) .fetch_optional(pool)
.map_ok(|row| row.try_into()) .await?
.map_err(|error| error.into()) .map(|row| row.try_into())
.await; .transpose()
match trip {
Err(error) => match error {
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
_ => Err(error),
},
Ok(v) => Ok(Some(v?)),
}
} }
pub async fn trip_type_remove( pub async fn trip_type_remove(
@@ -615,7 +590,7 @@ impl Trip {
pub async fn find_total_picked_weight( pub async fn find_total_picked_weight(
pool: &sqlx::Pool<sqlx::Sqlite>, pool: &sqlx::Pool<sqlx::Sqlite>,
trip_id: Uuid, trip_id: Uuid,
) -> Result<Option<i64>, Error> { ) -> Result<i64, Error> {
let trip_id_param = trip_id.to_string(); let trip_id_param = trip_id.to_string();
let weight = sqlx::query_as!( let weight = sqlx::query_as!(
DbTripWeightRow, DbTripWeightRow,
@@ -634,17 +609,10 @@ impl Trip {
trip_id_param trip_id_param
) )
.fetch_one(pool) .fetch_one(pool)
.map_ok(|row| row.total_weight.map(|weight| weight as i64)) .map_ok(|row| row.total_weight.unwrap() as i64)
.map_err(|error| error.into()) .await?;
.await;
match weight { Ok(weight)
Err(error) => match error {
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
_ => Err(error.into()),
},
Ok(v) => Ok(v),
}
} }
pub fn types(&self) -> &Vec<TripType> { pub fn types(&self) -> &Vec<TripType> {
@@ -997,7 +965,7 @@ impl Category {
id: Uuid, id: Uuid,
) -> Result<Option<Category>, Error> { ) -> Result<Option<Category>, Error> {
let id_param = id.to_string(); let id_param = id.to_string();
let item = sqlx::query_as!( sqlx::query_as!(
DbCategoryRow, DbCategoryRow,
"SELECT "SELECT
id, id,
@@ -1007,18 +975,10 @@ impl Category {
WHERE category.id = ?", WHERE category.id = ?",
id_param, id_param,
) )
.fetch_one(pool) .fetch_optional(pool)
.map_ok(|row| row.try_into()) .await?
.map_err(|error| error.into()) .map(|row| row.try_into())
.await; .transpose()
match item {
Err(error) => match error {
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
_ => Err(error),
},
Ok(v) => Ok(Some(v?)),
}
} }
pub async fn save(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<Uuid, Error> { pub async fn save(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<Uuid, Error> {
@@ -1100,7 +1060,7 @@ impl TryFrom<DbInventoryItemsRow> for Item {
impl Item { impl Item {
pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Item>, Error> { pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Item>, Error> {
let id_param = id.to_string(); let id_param = id.to_string();
let item = sqlx::query_as!( sqlx::query_as!(
DbInventoryItemsRow, DbInventoryItemsRow,
"SELECT "SELECT
id, id,
@@ -1112,28 +1072,22 @@ impl Item {
WHERE item.id = ?", WHERE item.id = ?",
id_param, id_param,
) )
.fetch_one(pool) .fetch_optional(pool)
.map_err(|error| error.into()) .await?
.map_ok(|row| row.try_into()) .map(|row| row.try_into())
.await; .transpose()
match item {
Err(error) => match error {
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
_ => Err(error),
},
Ok(v) => Ok(Some(v?)),
}
} }
pub async fn update( pub async fn update(
pool: &sqlx::Pool<sqlx::Sqlite>, pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid, id: Uuid,
name: &str, name: &str,
weight: i64, weight: u32,
) -> Result<Option<Uuid>, Error> { ) -> Result<Uuid, Error> {
let weight = i64::try_from(weight).unwrap();
let id_param = id.to_string(); let id_param = id.to_string();
let id = sqlx::query!( Ok(sqlx::query!(
"UPDATE inventory_items AS item "UPDATE inventory_items AS item
SET SET
name = ?, name = ?,
@@ -1146,22 +1100,8 @@ impl Item {
id_param, id_param,
) )
.fetch_one(pool) .fetch_one(pool)
.map_ok(|row| { .map_ok(|row| Uuid::try_parse(&row.id.unwrap()))
let id: &str = &row.id.unwrap(); // TODO .await??)
let uuid: Result<Uuid, uuid::Error> = Uuid::try_parse(id);
let uuid: Result<Uuid, Error> = uuid.map_err(|error| error.into());
uuid
})
.map_err(|error| error.into())
.await;
match id {
Err(error) => match error {
Error::Query(QueryError::NotFound { description: _ }) => Ok(None),
_ => Err(error.into()),
},
Ok(v) => Ok(Some(v?)),
}
} }
pub async fn get_category_max_weight( pub async fn get_category_max_weight(
@@ -1197,7 +1137,7 @@ impl Item {
category_id: Uuid, category_id: Uuid,
) -> Result<i64, Error> { ) -> Result<i64, Error> {
let category_id_param = category_id.to_string(); let category_id_param = category_id.to_string();
let weight: Result<i64, Error> = sqlx::query!( Ok(sqlx::query!(
" "
SELECT COALESCE(SUM(i_item.weight), 0) as weight SELECT COALESCE(SUM(i_item.weight), 0) as weight
FROM inventory_items_categories as category FROM inventory_items_categories as category
@@ -1218,10 +1158,7 @@ impl Item {
// We can be certain that the row exists, as we COALESCE it // We can be certain that the row exists, as we COALESCE it
row.weight.unwrap() as i64 row.weight.unwrap() as i64
}) })
.map_err(|error| error.into()) .await?)
.await;
Ok(weight?)
} }
} }
@@ -1316,7 +1253,7 @@ impl InventoryItem {
pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Self>, Error> { pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Self>, Error> {
let id_param = id.to_string(); let id_param = id.to_string();
let item = sqlx::query_as!( sqlx::query_as!(
DbInventoryItemRow, DbInventoryItemRow,
"SELECT "SELECT
item.id AS id, item.id AS id,
@@ -1338,39 +1275,23 @@ impl InventoryItem {
WHERE item.id = ?", WHERE item.id = ?",
id_param, id_param,
) )
.fetch_one(pool) .fetch_optional(pool)
.map_ok(|row| row.try_into()) .await?
.map_err(|error| error.into()) .map(|row| row.try_into())
.await; .transpose()
match item {
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<sqlx::Sqlite>, name: &str) -> Result<bool, Error> { pub async fn name_exists(pool: &sqlx::Pool<sqlx::Sqlite>, name: &str) -> Result<bool, Error> {
let item = sqlx::query!( Ok(sqlx::query!(
"SELECT id "SELECT id
FROM inventory_items FROM inventory_items
WHERE name = ?", WHERE name = ?",
name, name,
) )
.fetch_one(pool) .fetch_optional(pool)
.map_ok(|_row| ()) .await?
.map_err(|error| error.into()) .map(|_row| ())
.await; .is_some())
match item {
Err(error) => match error {
Error::Query(QueryError::NotFound { description: _ }) => Ok(false),
_ => Err(error.into()),
},
Ok(_) => Ok(true),
}
} }
pub async fn delete(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<bool, Error> { pub async fn delete(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<bool, Error> {
@@ -1427,23 +1348,9 @@ impl Inventory {
.fetch(pool) .fetch(pool)
.map_ok(|row: DbCategoryRow| row.try_into()) .map_ok(|row: DbCategoryRow| row.try_into())
.try_collect::<Vec<Result<Category, Error>>>() .try_collect::<Vec<Result<Category, Error>>>()
.await .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(|error| {
Error::Database(DatabaseError::Sql {
description: error.to_string(),
})
})?
.into_iter() .into_iter()
.collect::<Result<Vec<Category>, Error>>() .collect::<Result<Vec<Category>, Error>>()?;
// and this one is the model mapping error that may arise e.g. during
// reading of the rows
.map_err(|error| {
Error::Database(DatabaseError::Sql {
description: error.to_string(),
})
})?;
for category in &mut categories { for category in &mut categories {
category.populate_items(pool).await?; category.populate_items(pool).await?;

View File

@@ -1,4 +1,3 @@
use std::convert;
use std::fmt; use std::fmt;
use sqlx::error::DatabaseError as _; use sqlx::error::DatabaseError as _;
@@ -43,9 +42,6 @@ impl fmt::Display for DatabaseError {
pub enum QueryError { pub enum QueryError {
/// Errors that are caused by wrong input data, e.g. ids that cannot be found, or /// Errors that are caused by wrong input data, e.g. ids that cannot be found, or
/// inserts that violate unique constraints /// inserts that violate unique constraints
Constraint {
description: String,
},
Duplicate { Duplicate {
description: String, description: String,
}, },
@@ -60,9 +56,6 @@ pub enum QueryError {
impl fmt::Display for QueryError { impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
Self::Constraint { description } => {
write!(f, "SQL constraint error: {description}")
}
Self::Duplicate { description } => { Self::Duplicate { description } => {
write!(f, "Duplicate data entry: {description}") write!(f, "Duplicate data entry: {description}")
} }
@@ -97,7 +90,7 @@ impl fmt::Debug for Error {
} }
} }
impl convert::From<uuid::Error> for Error { impl From<uuid::Error> for Error {
fn from(value: uuid::Error) -> Self { fn from(value: uuid::Error) -> Self {
Error::Database(DatabaseError::Uuid { Error::Database(DatabaseError::Uuid {
description: value.to_string(), description: value.to_string(),
@@ -105,7 +98,7 @@ impl convert::From<uuid::Error> for Error {
} }
} }
impl convert::From<time::error::Format> for Error { impl From<time::error::Format> for Error {
fn from(value: time::error::Format) -> Self { fn from(value: time::error::Format) -> Self {
Error::Database(DatabaseError::TimeParse { Error::Database(DatabaseError::TimeParse {
description: value.to_string(), description: value.to_string(),
@@ -113,7 +106,7 @@ impl convert::From<time::error::Format> for Error {
} }
} }
impl convert::From<sqlx::Error> for Error { impl From<sqlx::Error> for Error {
fn from(value: sqlx::Error) -> Self { fn from(value: sqlx::Error) -> Self {
match value { match value {
sqlx::Error::RowNotFound => Error::Query(QueryError::NotFound { sqlx::Error::RowNotFound => Error::Query(QueryError::NotFound {
@@ -124,11 +117,11 @@ impl convert::From<sqlx::Error> for Error {
if let Some(code) = sqlite_error.code() { if let Some(code) = sqlite_error.code() {
match &*code { match &*code {
// SQLITE_CONSTRAINT_FOREIGNKEY // SQLITE_CONSTRAINT_FOREIGNKEY
"787" => Error::Query(QueryError::Constraint { "787" => Error::Query(QueryError::ReferenceNotFound {
description: format!("foreign key reference not found"), description: format!("foreign key reference not found"),
}), }),
// SQLITE_CONSTRAINT_UNIQUE // SQLITE_CONSTRAINT_UNIQUE
"2067" => Error::Query(QueryError::Constraint { "2067" => Error::Query(QueryError::Duplicate {
description: format!("item with unique constraint already exists",), description: format!("item with unique constraint already exists",),
}), }),
_ => Error::Database(DatabaseError::Sql { _ => Error::Database(DatabaseError::Sql {
@@ -154,7 +147,7 @@ impl convert::From<sqlx::Error> for Error {
} }
} }
impl convert::From<time::error::Parse> for Error { impl From<time::error::Parse> for Error {
fn from(value: time::error::Parse) -> Self { fn from(value: time::error::Parse) -> Self {
Error::Database(DatabaseError::TimeParse { Error::Database(DatabaseError::TimeParse {
description: value.to_string(), description: value.to_string(),

18
rust/src/sqlite.rs Normal file
View File

@@ -0,0 +1,18 @@
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
pub use sqlx::{Pool, Sqlite};
use std::str::FromStr as _;
use crate::StartError;
pub async fn init_database_pool(url: &str) -> Result<Pool<Sqlite>, StartError> {
Ok(SqlitePoolOptions::new()
.max_connections(5)
.connect_with(SqliteConnectOptions::from_str(url)?.pragma("foreign_keys", "1"))
.await?)
}
pub async fn migrate(pool: &Pool<Sqlite>) -> Result<(), StartError> {
sqlx::migrate!().run(pool).await?;
Ok(())
}