add ssr rust
This commit is contained in:
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "rust/vendor/axohtml"]
|
||||||
|
path = rust/vendor/axohtml
|
||||||
|
url = https://github.com/axodotdev/axohtml
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/hannes/sync/items/items.sqlite
|
|
||||||
1
rust/.gitignore
vendored
Normal file
1
rust/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target/
|
||||||
2012
rust/Cargo.lock
generated
Normal file
2012
rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
rust/Cargo.toml
Normal file
40
rust/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[package]
|
||||||
|
name = "packager"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies.axum]
|
||||||
|
version = "0.6.18"
|
||||||
|
|
||||||
|
[dependencies.tokio]
|
||||||
|
version = "1.28.0"
|
||||||
|
features = ["full"]
|
||||||
|
|
||||||
|
[dependencies.hyper]
|
||||||
|
version = "0.14.26"
|
||||||
|
features = ["full"]
|
||||||
|
|
||||||
|
[dependencies.tower]
|
||||||
|
version = "0.4.13"
|
||||||
|
|
||||||
|
[dependencies.tracing]
|
||||||
|
version = "0.1.37"
|
||||||
|
|
||||||
|
[dependencies.axohtml]
|
||||||
|
path = "./vendor/axohtml/typed-html"
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
version = "1.3.2"
|
||||||
|
features = [
|
||||||
|
"v4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.sqlx]
|
||||||
|
version = "0.6.3"
|
||||||
|
features = ["runtime-tokio-rustls", "sqlite", "macros", "time"]
|
||||||
|
|
||||||
|
[dependencies.futures]
|
||||||
|
version = "0.3.28"
|
||||||
|
|
||||||
|
[dependencies.time]
|
||||||
|
version = "0.3.21"
|
||||||
3
rust/js/app.js
Normal file
3
rust/js/app.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
document.body.addEventListener('htmx:responseError', function(evt) {
|
||||||
|
console.log(evt.detail);
|
||||||
|
});
|
||||||
25
rust/src/components/home.rs
Normal file
25
rust/src/components/home.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use super::Tree;
|
||||||
|
use axohtml::html;
|
||||||
|
|
||||||
|
pub struct Home {
|
||||||
|
doc: Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Home {
|
||||||
|
pub fn build() -> Self {
|
||||||
|
let doc = html!(
|
||||||
|
<div id="home" class=["p-8", "max-w-xl"]>
|
||||||
|
<p><a href="/inventory">"Inventory"</a></p>
|
||||||
|
<p><a href="/trips">"Trips"</a></p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { doc }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Tree> for Home {
|
||||||
|
fn into(self) -> Tree {
|
||||||
|
self.doc
|
||||||
|
}
|
||||||
|
}
|
||||||
227
rust/src/components/inventory.rs
Normal file
227
rust/src/components/inventory.rs
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
use super::Tree;
|
||||||
|
use axohtml::{
|
||||||
|
html, text,
|
||||||
|
types::{Class, SpacedSet},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::models::*;
|
||||||
|
use crate::State;
|
||||||
|
|
||||||
|
pub struct Inventory {
|
||||||
|
doc: Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inventory {
|
||||||
|
pub async fn build(state: State, categories: Vec<Category>) -> Result<Self, Error> {
|
||||||
|
let doc = html!(
|
||||||
|
<div id="pkglist-item-manager">
|
||||||
|
<div class=["p-8", "grid", "grid-cols-4", "gap-3"]>
|
||||||
|
<div class=["col-span-2"]>
|
||||||
|
{<InventoryCategoryList as Into<Tree>>::into(InventoryCategoryList::build(&categories).await?)}
|
||||||
|
</div>
|
||||||
|
{if state.has_active_category { html!(
|
||||||
|
<div class=["col-span-2"]>
|
||||||
|
{<InventoryItemList as Into<Tree>>::into(InventoryItemList::build(categories.iter().find(|category| category.active).unwrap().items()).await?)}
|
||||||
|
</div>
|
||||||
|
)} else {
|
||||||
|
html!(<div></div>)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self { doc })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Tree> for Inventory {
|
||||||
|
fn into(self) -> Tree {
|
||||||
|
self.doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InventoryCategoryList {
|
||||||
|
doc: Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InventoryCategoryList {
|
||||||
|
pub async fn build(categories: &Vec<Category>) -> Result<Self, Error> {
|
||||||
|
let biggest_category_weight: u32 = categories
|
||||||
|
.iter()
|
||||||
|
.map(|category| category.total_weight())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(1);
|
||||||
|
|
||||||
|
let cls_td_active: SpacedSet<Class> =
|
||||||
|
["border", "p-0", "m-0", "font-bold"].try_into().unwrap();
|
||||||
|
let cls_td_inactive: SpacedSet<Class> = ["border", "p-0", "m-0"].try_into().unwrap();
|
||||||
|
|
||||||
|
let cls_tr_active: SpacedSet<Class> = [
|
||||||
|
"h-10",
|
||||||
|
"hover:bg-purple-100",
|
||||||
|
"m-3",
|
||||||
|
"h-full",
|
||||||
|
"outline",
|
||||||
|
"outline-2",
|
||||||
|
"outline-indigo-600",
|
||||||
|
]
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
let cls_tr_inactive: SpacedSet<Class> = ["h-10", "hover:bg-purple-100", "m-3", "h-full"]
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let doc = html!(
|
||||||
|
<div>
|
||||||
|
<h1 class=["text-2xl", "mb-5"]>"Categories"</h1>
|
||||||
|
<table class=[
|
||||||
|
"table",
|
||||||
|
"table-auto",
|
||||||
|
"border-collapse",
|
||||||
|
"border-spacing-0",
|
||||||
|
"border",
|
||||||
|
"w-full",
|
||||||
|
]>
|
||||||
|
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:50%"/>
|
||||||
|
<col style="width:50%"/>
|
||||||
|
</colgroup>
|
||||||
|
<thead class=["bg-gray-200"]>
|
||||||
|
<tr class=["h-10"]>
|
||||||
|
<th class=["border", "p-2"]>"Name"</th>
|
||||||
|
<th class=["border", "p-2"]>"Weight"</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{categories.iter().map(|category| html!(
|
||||||
|
<tr
|
||||||
|
class={if category.active {
|
||||||
|
cls_tr_active.clone()
|
||||||
|
} else {
|
||||||
|
cls_tr_inactive.clone()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class={if category.active {
|
||||||
|
cls_td_active.clone()
|
||||||
|
} else {
|
||||||
|
cls_td_inactive.clone()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
id="select-category"
|
||||||
|
href={
|
||||||
|
format!(
|
||||||
|
"/inventory/category/{id}",
|
||||||
|
id=category.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
class=["inline-block", "p-2", "m-0", "w-full"]
|
||||||
|
>
|
||||||
|
{text!(category.name.clone())}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class=["border", "p-0", "m-0"] style="position:relative;">
|
||||||
|
<a
|
||||||
|
id="select-category"
|
||||||
|
href={
|
||||||
|
format!(
|
||||||
|
"/inventory/category/{id}",
|
||||||
|
id=category.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
class=["inline-block", "p-2", "m-0", "w-full"]
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{text!(category.total_weight().to_string())}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
class=["bg-blue-600", "h-1.5"]
|
||||||
|
style = {
|
||||||
|
format!(
|
||||||
|
"width: {width}%;position:absolute;left:0;bottom:0;right:0;",
|
||||||
|
width=(
|
||||||
|
category.total_weight() as f32
|
||||||
|
/ biggest_category_weight as f32
|
||||||
|
* 100.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
<tr class=["h-10", "hover:bg-purple-200", "bg-gray-300", "font-bold"]>
|
||||||
|
<td class=["border", "p-0", "m-0"]>
|
||||||
|
<p class=["p-2", "m-2"]>"Sum"</p>
|
||||||
|
</td>
|
||||||
|
<td class=["border", "p-0", "m-0"]>
|
||||||
|
<p class=["p-2", "m-2"]>
|
||||||
|
{text!(categories.iter().map(|category| category.total_weight()).sum::<u32>().to_string())}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self { doc })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Tree> for InventoryCategoryList {
|
||||||
|
fn into(self) -> Tree {
|
||||||
|
self.doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InventoryItemList {
|
||||||
|
doc: Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InventoryItemList {
|
||||||
|
pub async fn build(items: &Vec<Item>) -> Result<Self, Error> {
|
||||||
|
let doc = html!(
|
||||||
|
<div>
|
||||||
|
<h1 class=["text-2xl", "mb-5"]>"Categories"</h1>
|
||||||
|
<table class=[
|
||||||
|
"table",
|
||||||
|
"table-auto",
|
||||||
|
"border-collapse",
|
||||||
|
"border-spacing-0",
|
||||||
|
"border",
|
||||||
|
"w-full",
|
||||||
|
]>
|
||||||
|
|
||||||
|
<thead class=["bg-gray-200"]>
|
||||||
|
<tr class=["h-10"]>
|
||||||
|
<th class=["border", "p-2"]>"Name"</th>
|
||||||
|
<th class=["border", "p-2"]>"Weight"</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.iter().map(|item| html!(
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{text!(item.name.clone())}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self { doc })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Tree> for InventoryItemList {
|
||||||
|
fn into(self) -> Tree {
|
||||||
|
self.doc
|
||||||
|
}
|
||||||
|
}
|
||||||
86
rust/src/components/mod.rs
Normal file
86
rust/src/components/mod.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use axohtml::{
|
||||||
|
dom::DOMTree,
|
||||||
|
elements::FlowContent,
|
||||||
|
html,
|
||||||
|
types::{Class, SpacedSet},
|
||||||
|
unsafe_text,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Tree = Box<dyn FlowContent<String>>;
|
||||||
|
|
||||||
|
pub mod home;
|
||||||
|
pub mod inventory;
|
||||||
|
pub mod triplist;
|
||||||
|
|
||||||
|
pub use home::*;
|
||||||
|
pub use inventory::*;
|
||||||
|
pub use triplist::*;
|
||||||
|
|
||||||
|
pub struct Root {
|
||||||
|
doc: DOMTree<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum TopLevelPage {
|
||||||
|
Inventory,
|
||||||
|
Trips,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Root {
|
||||||
|
pub fn build(body: Tree, active_page: TopLevelPage) -> Self {
|
||||||
|
let active_classes: SpacedSet<Class> =
|
||||||
|
["text-lg", "font-bold", "underline"].try_into().unwrap();
|
||||||
|
let inactive_classes: SpacedSet<Class> = ["text-lg"].try_into().unwrap();
|
||||||
|
|
||||||
|
let doc = html!(
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>"Packager"</title>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.7.0"/>
|
||||||
|
<script src="https://cdn.tailwindcss.com"/>
|
||||||
|
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.js" defer="true"/>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"/>
|
||||||
|
<script>{unsafe_text!(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))}</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class=[
|
||||||
|
"bg-gray-200",
|
||||||
|
"p-5",
|
||||||
|
"flex",
|
||||||
|
"flex-row",
|
||||||
|
"flex-nowrap",
|
||||||
|
"justify-between",
|
||||||
|
"items-center",
|
||||||
|
]>
|
||||||
|
<span class=["text-xl", "font-semibold"]>
|
||||||
|
<a href="/">"Packager"</a>
|
||||||
|
</span>
|
||||||
|
<nav class=["grow", "flex", "flex-row", "justify-center", "gap-x-6"]>
|
||||||
|
<a href="/inventory/" class={
|
||||||
|
match active_page {
|
||||||
|
TopLevelPage::Inventory => active_classes.clone(),
|
||||||
|
_ => inactive_classes.clone(),
|
||||||
|
}
|
||||||
|
}>"Inventory"</a>
|
||||||
|
<a href="/trips/" class={
|
||||||
|
match active_page {
|
||||||
|
TopLevelPage::Trips => active_classes,
|
||||||
|
_ => inactive_classes,
|
||||||
|
}
|
||||||
|
}>"Trips"</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
{body}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { doc }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
let mut doc = self.doc.to_string();
|
||||||
|
doc.insert_str(0, "<!DOCTYPE html>\n");
|
||||||
|
doc
|
||||||
|
}
|
||||||
|
}
|
||||||
43
rust/src/components/triplist.rs
Normal file
43
rust/src/components/triplist.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use super::Tree;
|
||||||
|
use crate::models::*;
|
||||||
|
|
||||||
|
use axohtml::{html, text};
|
||||||
|
|
||||||
|
pub struct TripList {
|
||||||
|
doc: Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TripList {
|
||||||
|
pub fn build(package_lists: Vec<Trip>) -> Self {
|
||||||
|
let doc = html!(
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>"ID"</th>
|
||||||
|
<th>"Name"</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
package_lists.into_iter().map(|list| {
|
||||||
|
html!(
|
||||||
|
<tr>
|
||||||
|
<td>{text!(list.id.to_string())}</td>
|
||||||
|
<td>{text!(list.name)}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { doc }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Tree> for TripList {
|
||||||
|
fn into(self) -> Tree {
|
||||||
|
self.doc
|
||||||
|
}
|
||||||
|
}
|
||||||
147
rust/src/main.rs
Normal file
147
rust/src/main.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use axum::{extract::Path, http::StatusCode, response::Html, routing::get, Router};
|
||||||
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
|
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
mod components;
|
||||||
|
mod models;
|
||||||
|
|
||||||
|
use crate::components::*;
|
||||||
|
use crate::models::*;
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
pub has_active_category: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
State {
|
||||||
|
has_active_category: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), sqlx::Error> {
|
||||||
|
// build our application with a route
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(root))
|
||||||
|
.route("/trips/", get(trips))
|
||||||
|
.route("/inventory/", get(inventory_inactive))
|
||||||
|
.route("/inventory/category/:id", get(inventory_active));
|
||||||
|
|
||||||
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||||
|
tracing::debug!("listening on {}", addr);
|
||||||
|
axum::Server::bind(&addr)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn root() -> (StatusCode, Html<String>) {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Html::from(Root::build(Home::build().into(), TopLevelPage::None).to_string()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inventory_active(
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||||
|
inventory(Some(id)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inventory_inactive() -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||||
|
inventory(None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inventory(
|
||||||
|
active_id: Option<String>,
|
||||||
|
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||||
|
let mut state: State = State::new();
|
||||||
|
state.has_active_category = active_id.is_some();
|
||||||
|
|
||||||
|
let active_id = active_id
|
||||||
|
.map(|id| Uuid::try_parse(&id))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| (StatusCode::BAD_REQUEST, Html::from(e.to_string())))?;
|
||||||
|
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect("sqlite:///home/hannes-private/sync/items/items.sqlite")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut categories = sqlx::query("SELECT id,name,description FROM inventoryitemcategories")
|
||||||
|
.fetch(&pool)
|
||||||
|
.map_ok(|row| row.try_into())
|
||||||
|
.try_collect::<Vec<Result<Category, models::Error>>>()
|
||||||
|
.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())))?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<Category>, 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())))?;
|
||||||
|
|
||||||
|
for category in &mut categories {
|
||||||
|
category
|
||||||
|
.populate_items()
|
||||||
|
.await
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?;
|
||||||
|
|
||||||
|
if let Some(active_id) = active_id {
|
||||||
|
if category.id == active_id {
|
||||||
|
category.active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Html::from(
|
||||||
|
Root::build(
|
||||||
|
Inventory::build(state, categories)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?
|
||||||
|
.into(),
|
||||||
|
TopLevelPage::Inventory,
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn trips() -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect("sqlite:///home/hannes-private/sync/items/items.sqlite")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let trips = sqlx::query("SELECT * FROM trips")
|
||||||
|
.fetch(&pool)
|
||||||
|
.map_ok(|row| row.try_into())
|
||||||
|
.try_collect::<Vec<Result<Trip, models::Error>>>()
|
||||||
|
.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())))?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<Trip>, 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())))?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Html::from(Root::build(TripList::build(trips).into(), TopLevelPage::Trips).to_string()),
|
||||||
|
))
|
||||||
|
}
|
||||||
169
rust/src/models.rs
Normal file
169
rust/src/models.rs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
use sqlx::{sqlite::SqliteRow, Row};
|
||||||
|
use std::convert;
|
||||||
|
use std::error;
|
||||||
|
use std::fmt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
|
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
|
||||||
|
pub enum Error {
|
||||||
|
SqlError { description: String },
|
||||||
|
UuidError { description: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::SqlError { description } => {
|
||||||
|
write!(f, "SQL error: {}", description)
|
||||||
|
}
|
||||||
|
Self::UuidError { description } => {
|
||||||
|
write!(f, "UUID error: {}", 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<uuid::Error> for Error {
|
||||||
|
fn from(value: uuid::Error) -> Self {
|
||||||
|
Error::UuidError {
|
||||||
|
description: value.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl convert::From<sqlx::Error> for Error {
|
||||||
|
fn from(value: sqlx::Error) -> Self {
|
||||||
|
Error::SqlError {
|
||||||
|
description: value.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for Error {}
|
||||||
|
|
||||||
|
pub struct Trip {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub start_date: time::Date,
|
||||||
|
pub end_date: time::Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<SqliteRow> for Trip {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
|
||||||
|
let name: &str = row.try_get("name")?;
|
||||||
|
let id: &str = row.try_get("id")?;
|
||||||
|
let start_date: time::Date = row.try_get("start_date")?;
|
||||||
|
let end_date: time::Date = row.try_get("start_date")?;
|
||||||
|
let id: Uuid = Uuid::try_parse(id)?;
|
||||||
|
|
||||||
|
Ok(Trip {
|
||||||
|
name: name.to_string(),
|
||||||
|
id,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Category {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
items: Option<Vec<Item>>,
|
||||||
|
pub active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<SqliteRow> for Category {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
|
||||||
|
let name: &str = row.try_get("name")?;
|
||||||
|
let description: &str = row.try_get("description")?;
|
||||||
|
let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
|
||||||
|
|
||||||
|
Ok(Category {
|
||||||
|
id,
|
||||||
|
name: name.to_string(),
|
||||||
|
description: description.to_string(),
|
||||||
|
items: None,
|
||||||
|
active: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Category {
|
||||||
|
pub fn items(&'a self) -> &'a Vec<Item> {
|
||||||
|
self.items
|
||||||
|
.as_ref()
|
||||||
|
.expect("you need to call populate_items()")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_weight(&self) -> u32 {
|
||||||
|
self.items().iter().map(|item| item.weight).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn populate_items(&'a mut self) -> Result<(), Error> {
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect("sqlite:///home/hannes-private/sync/items/items.sqlite")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let items = sqlx::query(&format!(
|
||||||
|
"SELECT
|
||||||
|
id,name,weight,description,category_id
|
||||||
|
FROM inventoryitems
|
||||||
|
WHERE category_id = '{id}'",
|
||||||
|
id = self.id
|
||||||
|
))
|
||||||
|
.fetch(&pool)
|
||||||
|
.map_ok(|row| row.try_into())
|
||||||
|
.try_collect::<Vec<Result<Item, Error>>>()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<Item>, Error>>()?;
|
||||||
|
|
||||||
|
self.items = Some(items);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Item {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub weight: u32,
|
||||||
|
pub category_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<SqliteRow> for Item {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
|
||||||
|
let name: &str = row.try_get("name")?;
|
||||||
|
let description: &str = row.try_get("description")?;
|
||||||
|
let weight: u32 = row.try_get("weight")?;
|
||||||
|
let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
|
||||||
|
let category_id: Uuid = Uuid::try_parse(row.try_get("category_id")?)?;
|
||||||
|
|
||||||
|
Ok(Item {
|
||||||
|
id,
|
||||||
|
name: name.to_string(),
|
||||||
|
weight,
|
||||||
|
description: description.to_string(),
|
||||||
|
category_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rust/vendor/axohtml
vendored
Submodule
1
rust/vendor/axohtml
vendored
Submodule
Submodule rust/vendor/axohtml added at 1f7cfce85e
Reference in New Issue
Block a user