diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 535a824..d976aa6 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -626,6 +626,8 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0bab19cef8a7fe1c18a43e881793bfc9d4ea984befec3ae5bd0415abf3ecf00" dependencies = [ + "axum-core", + "http", "itoa", "maud_macros", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ab34402..c34fd62 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -26,6 +26,9 @@ version = "0.3" [dependencies.maud] version = "0.25" +features = [ + "axum", +] [dependencies.uuid] version = "1.3.2" diff --git a/rust/src/components/home.rs b/rust/src/components/home.rs index 9ac220f..2f9b872 100644 --- a/rust/src/components/home.rs +++ b/rust/src/components/home.rs @@ -1,12 +1,10 @@ use maud::{html, Markup}; -pub struct Home { - doc: Markup, -} +pub struct Home; impl Home { - pub fn build() -> Self { - let doc: Markup = html!( + pub fn build() -> Markup { + html!( div id="home" class={"p-8" "max-w-xl"} { p { a href="/inventory/" { "Inventory" } @@ -15,14 +13,6 @@ impl Home { a href="/trips/" { "Trips" } } } - ); - - Self { doc } - } -} - -impl From for Markup { - fn from(val: Home) -> Self { - val.doc + ) } } diff --git a/rust/src/components/inventory.rs b/rust/src/components/inventory.rs index d5dbbfd..bbb5417 100644 --- a/rust/src/components/inventory.rs +++ b/rust/src/components/inventory.rs @@ -4,17 +4,15 @@ use crate::models::*; use crate::ClientState; use uuid::{uuid, Uuid}; -pub struct Inventory { - doc: Markup, -} +pub struct Inventory; impl Inventory { - pub async fn build(state: ClientState, categories: Vec) -> Result { + pub async fn build(state: ClientState, categories: Vec) -> Result { let doc = html!( div id="pkglist-item-manager" { div ."p-8" ."grid" ."grid-cols-4" ."gap-3" { div ."col-span-2" { - (InventoryCategoryList::build(&state, &categories).into_markup()) + (InventoryCategoryList::build(&state, &categories)) } div ."col-span-2" { h1 ."text-2xl" ."mb-5" ."text-center" { "Items" } @@ -22,37 +20,29 @@ impl Inventory { (InventoryItemList::build(&state, categories.iter().find(|category| category.id == active_category_id) .ok_or(Error::NotFoundError { description: format!("no category with id {}", active_category_id) })? .items()) - .into_markup()) + ) } - (InventoryNewItemForm::build(&state, &categories).into_markup()) + (InventoryNewItemForm::build(&state, &categories)) } } } ); - Ok(Self { doc }) + Ok(doc) } } -impl From for Markup { - fn from(val: Inventory) -> Self { - val.doc - } -} - -pub struct InventoryCategoryList { - doc: Markup, -} +pub struct InventoryCategoryList; impl InventoryCategoryList { - pub fn build(state: &ClientState, categories: &Vec) -> Self { + pub fn build(state: &ClientState, categories: &Vec) -> Markup { let biggest_category_weight: u32 = categories .iter() .map(Category::total_weight) .max() .unwrap_or(1); - let doc = html!( + html!( div { h1 ."text-2xl" ."mb-5" ."text-center" { "Categories" } table @@ -146,30 +136,16 @@ impl InventoryCategoryList { } } } - ); - - Self { doc } - } - - fn into_markup(self) -> Markup { - self.doc + ) } } -impl From for Markup { - fn from(val: InventoryCategoryList) -> Self { - val.doc - } -} - -pub struct InventoryItemList { - doc: Markup, -} +pub struct InventoryItemList; impl InventoryItemList { - pub fn build(state: &ClientState, items: &Vec) -> Self { + pub fn build(state: &ClientState, items: &Vec) -> Markup { let biggest_item_weight: u32 = items.iter().map(|item| item.weight).max().unwrap_or(1); - let doc = html!( + html!( div #items { @if items.is_empty() { p ."text-lg" ."text-center" ."py-5" ."text-gray-400" { "[Empty]" } @@ -305,30 +281,15 @@ impl InventoryItemList { } } } - ); - - Self { doc } - } - - fn into_markup(self) -> Markup { - self.doc + ) } } -impl From for Markup { - fn from(val: InventoryItemList) -> Self { - val.doc - } -} - -pub struct InventoryNewItemForm { - doc: Markup, -} +pub struct InventoryNewItemForm; impl InventoryNewItemForm { - pub fn build(state: &ClientState, categories: &Vec) -> Self { - let doc = html!( - + pub fn build(state: &ClientState, categories: &Vec) -> Markup { + html!( form name="new-item" id="new-item" @@ -417,24 +378,6 @@ impl InventoryNewItemForm { } } } - ); - - Self { doc } - } - - fn into_markup(self) -> Markup { - self.doc + ) } } - -impl From for Markup { - fn from(val: InventoryNewItemForm) -> Self { - val.doc - } -} -// impl InventoryItemList { -// pub fn to_string(self) -> String { -// self.doc.into_string() -// } -// } -//ItemList diff --git a/rust/src/components/mod.rs b/rust/src/components/mod.rs index 919b2f3..2d77338 100644 --- a/rust/src/components/mod.rs +++ b/rust/src/components/mod.rs @@ -8,9 +8,7 @@ pub use home::*; pub use inventory::*; pub use trip::*; -pub struct Root { - doc: Markup, -} +pub struct Root; pub enum TopLevelPage { Inventory, @@ -19,8 +17,8 @@ pub enum TopLevelPage { } impl Root { - pub fn build(body: Markup, active_page: &TopLevelPage) -> Self { - let doc = html!( + pub fn build(body: Markup, active_page: &TopLevelPage) -> Markup { + html!( (DOCTYPE) html { head { @@ -61,12 +59,20 @@ impl Root { } } } - ); - - Self { doc } - } - - pub fn into_string(self) -> String { - self.doc.into_string() + ) + } +} + +pub struct ErrorPage; + +impl ErrorPage { + pub fn build(message: &str) -> Markup { + Root::build( + html!( + h1 { "Error" } + p { (message) } + ), + &TopLevelPage::None, + ) } } diff --git a/rust/src/components/trip.rs b/rust/src/components/trip.rs index fafb6be..bdb9d7e 100644 --- a/rust/src/components/trip.rs +++ b/rust/src/components/trip.rs @@ -3,36 +3,24 @@ use crate::models::*; use maud::{html, Markup}; -pub struct TripManager { - doc: Markup, -} +pub struct TripManager; impl TripManager { - pub fn build(trips: Vec) -> Self { - let doc = html!( + pub fn build(trips: Vec) -> Markup { + html!( div ."p-8" { - (TripTable::build(trips).into_markup()) - (NewTrip::build().into_markup()) + (TripTable::build(trips)) + (NewTrip::build()) } - ); - - Self { doc } + ) } } -pub struct TripTable { - doc: Markup, -} - -impl From for Markup { - fn from(val: TripManager) -> Self { - val.doc - } -} +pub struct TripTable; impl TripTable { - pub fn build(trips: Vec) -> Self { - let doc = html!( + pub fn build(trips: Vec) -> Markup { + html!( h1 ."text-2xl" ."mb-5" {"Trips"} table ."table" @@ -83,23 +71,15 @@ impl TripTable { } } } - ); - - Self { doc } - } - - pub fn into_markup(self) -> Markup { - self.doc + ) } } -pub struct NewTrip { - doc: Markup, -} +pub struct NewTrip; impl NewTrip { - pub fn build() -> Self { - let doc = html!( + pub fn build() -> Markup { + html!( form name="new_trip" action="/trip/" @@ -190,48 +170,32 @@ impl NewTrip { {} } } - ); - - Self { doc } - } - - pub fn into_markup(self) -> Markup { - self.doc + ) } } -pub struct Trip { - doc: Markup, -} +pub struct Trip; impl Trip { - pub fn build(trip: &models::Trip) -> Self { - let doc = html!( + pub fn build(trip: &models::Trip) -> Markup { + html!( div ."p-8" { div ."flex" ."flex-row" ."items-center" ."gap-x-3" { h1 ."text-2xl" ."font-semibold"{ (trip.name) } } div ."my-6" { - (TripInfo::build(&trip).into_markup()) + (TripInfo::build(&trip)) } } - ); - - Self { doc } - } - - pub fn into_markup(self) -> Markup { - self.doc + ) } } -pub struct TripInfo { - doc: Markup, -} +pub struct TripInfo; impl TripInfo { - pub fn build(trip: &models::Trip) -> Self { - let doc = html!( + pub fn build(trip: &models::Trip) -> Markup { + html!( table ."table" ."table-auto" @@ -338,12 +302,6 @@ impl TripInfo { } } } - ); - - Self { doc } - } - - pub fn into_markup(self) -> Markup { - self.doc + ) } } diff --git a/rust/src/main.rs b/rust/src/main.rs index f24896f..a28d2f8 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -16,6 +16,8 @@ use sqlx::{ Pool, Row, Sqlite, }; +use maud::Markup; + use serde::Deserialize; use futures::TryFutureExt; @@ -108,10 +110,10 @@ async fn main() -> Result<(), sqlx::Error> { Ok(()) } -async fn root() -> (StatusCode, Html) { +async fn root() -> (StatusCode, Markup) { ( StatusCode::OK, - Html::from(Root::build(Home::build().into(), &TopLevelPage::None).into_string()), + Root::build(Home::build(), &TopLevelPage::None), ) } @@ -130,7 +132,7 @@ async fn inventory_active( Path(id): Path, State(mut state): State, Query(inventory_query): Query, -) -> Result<(StatusCode, Html), (StatusCode, Html)> { +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { state.client_state.edit_item = inventory_query.edit_item; inventory(state, Some(id)).await } @@ -138,7 +140,7 @@ async fn inventory_active( async fn inventory_inactive( State(mut state): State, Query(inventory_query): Query, -) -> Result<(StatusCode, Html), (StatusCode, Html)> { +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { state.client_state.edit_item = inventory_query.edit_item; inventory(state, None).await } @@ -146,7 +148,7 @@ async fn inventory_inactive( async fn inventory( mut state: AppState, active_id: Option, -) -> Result<(StatusCode, Html), (StatusCode, Html)> { +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { state.client_state.active_category_id = active_id; let mut categories = query("SELECT id,name,description FROM inventoryitemcategories") @@ -156,36 +158,50 @@ async fn 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| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))? + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })? .into_iter() .collect::, models::Error>>() // and this one is the model mapping error that may arise e.g. during // reading of the rows - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?; + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; for category in &mut categories { category .populate_items(&state.database_pool) .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?; + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; } Ok(( StatusCode::OK, - Html::from( - Root::build( - Inventory::build(state.client_state, categories) - .await - .map_err(|e| match e { - Error::NotFoundError { description } => { - (StatusCode::NOT_FOUND, Html::from(description)) - } - _ => (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())), - })? - .into(), - &TopLevelPage::Inventory, - ) - .into_string(), + Root::build( + Inventory::build(state.client_state, categories) + .await + .map_err(|e| match e { + Error::NotFoundError { description } => { + (StatusCode::NOT_FOUND, ErrorPage::build(&description)) + } + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ), + })?, + &TopLevelPage::Inventory, ), )) } @@ -306,7 +322,7 @@ async fn inventory_item_delete( // async fn htmx_inventory_category_items( // Path(id): Path, -// ) -> Result<(StatusCode, Html), (StatusCode, Html)> { +// ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { // let pool = SqlitePoolOptions::new() // .max_connections(5) // .connect("sqlite:///home/hannes-private/sync/items/items.sqlite") @@ -452,7 +468,7 @@ async fn trip_create( async fn trips( State(state): State, -) -> Result<(StatusCode, Html), (StatusCode, Html)> { +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { let trips: Vec = query("SELECT * FROM trips") .fetch(&state.database_pool) .map_ok(std::convert::TryInto::try_into) @@ -460,25 +476,33 @@ async fn trips( .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| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))? + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })? .into_iter() .collect::, models::Error>>() // and this one is the model mapping error that may arise e.g. during // reading of the rows - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?; + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; Ok(( StatusCode::OK, - Html::from( - Root::build(TripManager::build(trips).into(), &TopLevelPage::Trips).into_string(), - ), + Root::build(TripManager::build(trips), &TopLevelPage::Trips), )) } async fn trip( Path(id): Path, State(state): State, -) -> Result<(StatusCode, Html), (StatusCode, Html)> { +) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { let mut trip: models::Trip = query("SELECT id,name,start_date,end_date,state,location,temp_min,temp_max FROM trips WHERE id = ?") .bind(id.to_string()) @@ -488,32 +512,31 @@ async fn trip( .map_err(|e: sqlx::Error| match e { sqlx::Error::RowNotFound => ( StatusCode::NOT_FOUND, - Html::from(format!("trip with id {} not found", id)), + ErrorPage::build(&format!("trip with id {} not found", id)), ), - _ => (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())), + _ => (StatusCode::INTERNAL_SERVER_ERROR, ErrorPage::build(&e.to_string())), })? - .map_err(|e: Error| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?; + .map_err(|e: Error| (StatusCode::INTERNAL_SERVER_ERROR, ErrorPage::build(&e.to_string())))?; trip.load_triptypes(&state.database_pool) .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?; + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::build(&e.to_string()), + ) + })?; Ok(( StatusCode::OK, - Html::from( - Root::build( - components::Trip::build(&trip).into_markup(), - &TopLevelPage::Trips, - ) - .into_string(), - ), + Root::build(components::Trip::build(&trip), &TopLevelPage::Trips), )) } async fn trip_type_remove( Path((trip_id, type_id)): Path<(Uuid, Uuid)>, State(state): State, -) -> Result)> { +) -> Result { let results = query( "DELETE FROM trips_to_triptypes AS ttt WHERE ttt.trip_id = ? @@ -524,12 +547,12 @@ async fn trip_type_remove( .bind(type_id.to_string()) .execute(&state.database_pool) .await - .map_err(|e| (StatusCode::BAD_REQUEST, Html::from(e.to_string())))?; + .map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?; if results.rows_affected() == 0 { Err(( StatusCode::NOT_FOUND, - Html::from(format!("type {type_id} is not active for trip {trip_id}")), + ErrorPage::build(&format!("type {type_id} is not active for trip {trip_id}")), )) } else { Ok(Redirect::to(&format!("/trip/{trip_id}/"))) @@ -539,8 +562,8 @@ async fn trip_type_remove( async fn trip_type_add( Path((trip_id, type_id)): Path<(Uuid, Uuid)>, State(state): State, -) -> Result)> { - let results = query( +) -> Result { + query( "INSERT INTO trips_to_triptypes (trip_id, trip_type_id) VALUES (?, ?)", ) @@ -560,21 +583,21 @@ async fn trip_type_add( // TODO: this is not perfect, as both foreign keys // may be responsible for the error. how can we tell // which one? - Html::from(format!("invalid id: {}", code.to_string())), + ErrorPage::build(&format!("invalid id: {}", code.to_string())), ) } "2067" => { // SQLITE_CONSTRAINT_UNIQUE ( StatusCode::BAD_REQUEST, - Html::from(format!( + ErrorPage::build(&format!( "type {type_id} is already active for trip {trip_id}" )), ) } _ => ( StatusCode::INTERNAL_SERVER_ERROR, - Html::from(format!( + ErrorPage::build(&format!( "got error with unknown code: {}", sqlite_error.to_string() )), @@ -583,7 +606,7 @@ async fn trip_type_add( } else { ( StatusCode::INTERNAL_SERVER_ERROR, - Html::from(format!( + ErrorPage::build(&format!( "got error without code: {}", sqlite_error.to_string() )), @@ -592,7 +615,7 @@ async fn trip_type_add( } _ => ( StatusCode::INTERNAL_SERVER_ERROR, - Html::from(format!("got unknown error: {}", e.to_string())), + ErrorPage::build(&format!("got unknown error: {}", e.to_string())), ), })?; diff --git a/rust/src/models.rs b/rust/src/models.rs index 2469142..e630bb4 100644 --- a/rust/src/models.rs +++ b/rust/src/models.rs @@ -19,7 +19,6 @@ pub enum Error { SqlError { description: String }, UuidError { description: String }, NotFoundError { description: String }, - InvalidEnumError { description: String }, } impl fmt::Display for Error { @@ -34,9 +33,6 @@ impl fmt::Display for Error { Self::NotFoundError { description } => { write!(f, "Not found: {description}") } - Self::InvalidEnumError { description } => { - write!(f, "Enum error: {description}") - } } } }