This commit is contained in:
2023-08-29 21:33:59 +02:00
parent 0ddeac69e6
commit 859299f4ce
8 changed files with 142 additions and 221 deletions

2
rust/Cargo.lock generated
View File

@@ -626,6 +626,8 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0bab19cef8a7fe1c18a43e881793bfc9d4ea984befec3ae5bd0415abf3ecf00" checksum = "b0bab19cef8a7fe1c18a43e881793bfc9d4ea984befec3ae5bd0415abf3ecf00"
dependencies = [ dependencies = [
"axum-core",
"http",
"itoa", "itoa",
"maud_macros", "maud_macros",
] ]

View File

@@ -26,6 +26,9 @@ version = "0.3"
[dependencies.maud] [dependencies.maud]
version = "0.25" version = "0.25"
features = [
"axum",
]
[dependencies.uuid] [dependencies.uuid]
version = "1.3.2" version = "1.3.2"

View File

@@ -1,12 +1,10 @@
use maud::{html, Markup}; use maud::{html, Markup};
pub struct Home { pub struct Home;
doc: Markup,
}
impl Home { impl Home {
pub fn build() -> Self { pub fn build() -> Markup {
let doc: Markup = html!( html!(
div id="home" class={"p-8" "max-w-xl"} { div id="home" class={"p-8" "max-w-xl"} {
p { p {
a href="/inventory/" { "Inventory" } a href="/inventory/" { "Inventory" }
@@ -15,14 +13,6 @@ impl Home {
a href="/trips/" { "Trips" } a href="/trips/" { "Trips" }
} }
} }
); )
Self { doc }
}
}
impl From<Home> for Markup {
fn from(val: Home) -> Self {
val.doc
} }
} }

View File

@@ -4,17 +4,15 @@ use crate::models::*;
use crate::ClientState; use crate::ClientState;
use uuid::{uuid, Uuid}; use uuid::{uuid, Uuid};
pub struct Inventory { pub struct Inventory;
doc: Markup,
}
impl Inventory { impl Inventory {
pub async fn build(state: ClientState, categories: Vec<Category>) -> Result<Self, Error> { pub async fn build(state: ClientState, categories: Vec<Category>) -> Result<Markup, Error> {
let doc = html!( let doc = html!(
div id="pkglist-item-manager" { div id="pkglist-item-manager" {
div ."p-8" ."grid" ."grid-cols-4" ."gap-3" { div ."p-8" ."grid" ."grid-cols-4" ."gap-3" {
div ."col-span-2" { div ."col-span-2" {
(InventoryCategoryList::build(&state, &categories).into_markup()) (InventoryCategoryList::build(&state, &categories))
} }
div ."col-span-2" { div ."col-span-2" {
h1 ."text-2xl" ."mb-5" ."text-center" { "Items" } 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) (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) })? .ok_or(Error::NotFoundError { description: format!("no category with id {}", active_category_id) })?
.items()) .items())
.into_markup()) )
} }
(InventoryNewItemForm::build(&state, &categories).into_markup()) (InventoryNewItemForm::build(&state, &categories))
} }
} }
} }
); );
Ok(Self { doc }) Ok(doc)
} }
} }
impl From<Inventory> for Markup { pub struct InventoryCategoryList;
fn from(val: Inventory) -> Self {
val.doc
}
}
pub struct InventoryCategoryList {
doc: Markup,
}
impl InventoryCategoryList { impl InventoryCategoryList {
pub fn build(state: &ClientState, categories: &Vec<Category>) -> Self { pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup {
let biggest_category_weight: u32 = categories let biggest_category_weight: u32 = categories
.iter() .iter()
.map(Category::total_weight) .map(Category::total_weight)
.max() .max()
.unwrap_or(1); .unwrap_or(1);
let doc = html!( html!(
div { div {
h1 ."text-2xl" ."mb-5" ."text-center" { "Categories" } h1 ."text-2xl" ."mb-5" ."text-center" { "Categories" }
table table
@@ -146,30 +136,16 @@ impl InventoryCategoryList {
} }
} }
} }
); )
Self { doc }
}
fn into_markup(self) -> Markup {
self.doc
} }
} }
impl From<InventoryCategoryList> for Markup { pub struct InventoryItemList;
fn from(val: InventoryCategoryList) -> Self {
val.doc
}
}
pub struct InventoryItemList {
doc: Markup,
}
impl InventoryItemList { impl InventoryItemList {
pub fn build(state: &ClientState, items: &Vec<Item>) -> Self { pub fn build(state: &ClientState, items: &Vec<Item>) -> Markup {
let biggest_item_weight: u32 = items.iter().map(|item| item.weight).max().unwrap_or(1); let biggest_item_weight: u32 = items.iter().map(|item| item.weight).max().unwrap_or(1);
let doc = html!( html!(
div #items { div #items {
@if items.is_empty() { @if items.is_empty() {
p ."text-lg" ."text-center" ."py-5" ."text-gray-400" { "[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<InventoryItemList> for Markup { pub struct InventoryNewItemForm;
fn from(val: InventoryItemList) -> Self {
val.doc
}
}
pub struct InventoryNewItemForm {
doc: Markup,
}
impl InventoryNewItemForm { impl InventoryNewItemForm {
pub fn build(state: &ClientState, categories: &Vec<Category>) -> Self { pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup {
let doc = html!( html!(
form form
name="new-item" name="new-item"
id="new-item" id="new-item"
@@ -417,24 +378,6 @@ impl InventoryNewItemForm {
} }
} }
} }
); )
Self { doc }
}
fn into_markup(self) -> Markup {
self.doc
} }
} }
impl From<InventoryNewItemForm> for Markup {
fn from(val: InventoryNewItemForm) -> Self {
val.doc
}
}
// impl InventoryItemList {
// pub fn to_string(self) -> String {
// self.doc.into_string()
// }
// }
//ItemList

View File

@@ -8,9 +8,7 @@ pub use home::*;
pub use inventory::*; pub use inventory::*;
pub use trip::*; pub use trip::*;
pub struct Root { pub struct Root;
doc: Markup,
}
pub enum TopLevelPage { pub enum TopLevelPage {
Inventory, Inventory,
@@ -19,8 +17,8 @@ pub enum TopLevelPage {
} }
impl Root { impl Root {
pub fn build(body: Markup, active_page: &TopLevelPage) -> Self { pub fn build(body: Markup, active_page: &TopLevelPage) -> Markup {
let doc = html!( html!(
(DOCTYPE) (DOCTYPE)
html { html {
head { head {
@@ -61,12 +59,20 @@ impl Root {
} }
} }
} }
); )
}
Self { doc } }
}
pub struct ErrorPage;
pub fn into_string(self) -> String {
self.doc.into_string() impl ErrorPage {
pub fn build(message: &str) -> Markup {
Root::build(
html!(
h1 { "Error" }
p { (message) }
),
&TopLevelPage::None,
)
} }
} }

View File

@@ -3,36 +3,24 @@ use crate::models::*;
use maud::{html, Markup}; use maud::{html, Markup};
pub struct TripManager { pub struct TripManager;
doc: Markup,
}
impl TripManager { impl TripManager {
pub fn build(trips: Vec<models::Trip>) -> Self { pub fn build(trips: Vec<models::Trip>) -> Markup {
let doc = html!( html!(
div ."p-8" { div ."p-8" {
(TripTable::build(trips).into_markup()) (TripTable::build(trips))
(NewTrip::build().into_markup()) (NewTrip::build())
} }
); )
Self { doc }
} }
} }
pub struct TripTable { pub struct TripTable;
doc: Markup,
}
impl From<TripManager> for Markup {
fn from(val: TripManager) -> Self {
val.doc
}
}
impl TripTable { impl TripTable {
pub fn build(trips: Vec<models::Trip>) -> Self { pub fn build(trips: Vec<models::Trip>) -> Markup {
let doc = html!( html!(
h1 ."text-2xl" ."mb-5" {"Trips"} h1 ."text-2xl" ."mb-5" {"Trips"}
table table
."table" ."table"
@@ -83,23 +71,15 @@ impl TripTable {
} }
} }
} }
); )
Self { doc }
}
pub fn into_markup(self) -> Markup {
self.doc
} }
} }
pub struct NewTrip { pub struct NewTrip;
doc: Markup,
}
impl NewTrip { impl NewTrip {
pub fn build() -> Self { pub fn build() -> Markup {
let doc = html!( html!(
form form
name="new_trip" name="new_trip"
action="/trip/" action="/trip/"
@@ -190,48 +170,32 @@ impl NewTrip {
{} {}
} }
} }
); )
Self { doc }
}
pub fn into_markup(self) -> Markup {
self.doc
} }
} }
pub struct Trip { pub struct Trip;
doc: Markup,
}
impl Trip { impl Trip {
pub fn build(trip: &models::Trip) -> Self { pub fn build(trip: &models::Trip) -> Markup {
let doc = html!( html!(
div ."p-8" { div ."p-8" {
div ."flex" ."flex-row" ."items-center" ."gap-x-3" { div ."flex" ."flex-row" ."items-center" ."gap-x-3" {
h1 ."text-2xl" ."font-semibold"{ (trip.name) } h1 ."text-2xl" ."font-semibold"{ (trip.name) }
} }
div ."my-6" { div ."my-6" {
(TripInfo::build(&trip).into_markup()) (TripInfo::build(&trip))
} }
} }
); )
Self { doc }
}
pub fn into_markup(self) -> Markup {
self.doc
} }
} }
pub struct TripInfo { pub struct TripInfo;
doc: Markup,
}
impl TripInfo { impl TripInfo {
pub fn build(trip: &models::Trip) -> Self { pub fn build(trip: &models::Trip) -> Markup {
let doc = html!( html!(
table table
."table" ."table"
."table-auto" ."table-auto"
@@ -338,12 +302,6 @@ impl TripInfo {
} }
} }
} }
); )
Self { doc }
}
pub fn into_markup(self) -> Markup {
self.doc
} }
} }

View File

@@ -16,6 +16,8 @@ use sqlx::{
Pool, Row, Sqlite, Pool, Row, Sqlite,
}; };
use maud::Markup;
use serde::Deserialize; use serde::Deserialize;
use futures::TryFutureExt; use futures::TryFutureExt;
@@ -108,10 +110,10 @@ async fn main() -> Result<(), sqlx::Error> {
Ok(()) Ok(())
} }
async fn root() -> (StatusCode, Html<String>) { async fn root() -> (StatusCode, Markup) {
( (
StatusCode::OK, 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<Uuid>, Path(id): Path<Uuid>,
State(mut state): State<AppState>, State(mut state): State<AppState>,
Query(inventory_query): Query<InventoryQuery>, Query(inventory_query): Query<InventoryQuery>,
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
state.client_state.edit_item = inventory_query.edit_item; state.client_state.edit_item = inventory_query.edit_item;
inventory(state, Some(id)).await inventory(state, Some(id)).await
} }
@@ -138,7 +140,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, Html<String>), (StatusCode, Html<String>)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
state.client_state.edit_item = inventory_query.edit_item; state.client_state.edit_item = inventory_query.edit_item;
inventory(state, None).await inventory(state, None).await
} }
@@ -146,7 +148,7 @@ async fn inventory_inactive(
async fn inventory( async fn inventory(
mut state: AppState, mut state: AppState,
active_id: Option<Uuid>, active_id: Option<Uuid>,
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
state.client_state.active_category_id = active_id; state.client_state.active_category_id = active_id;
let mut categories = query("SELECT id,name,description FROM inventoryitemcategories") let mut categories = query("SELECT id,name,description FROM inventoryitemcategories")
@@ -156,36 +158,50 @@ async fn inventory(
.await .await
// we have two error handling lines here. these are distinct errors // we have two error handling lines here. these are distinct errors
// this one is the SQL error that may arise during the query // 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() .into_iter()
.collect::<Result<Vec<Category>, models::Error>>() .collect::<Result<Vec<Category>, models::Error>>()
// and this one is the model mapping error that may arise e.g. during // and this one is the model mapping error that may arise e.g. during
// reading of the rows // 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 { for category in &mut categories {
category category
.populate_items(&state.database_pool) .populate_items(&state.database_pool)
.await .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(( Ok((
StatusCode::OK, StatusCode::OK,
Html::from( Root::build(
Root::build( Inventory::build(state.client_state, categories)
Inventory::build(state.client_state, categories) .await
.await .map_err(|e| match e {
.map_err(|e| match e { Error::NotFoundError { description } => {
Error::NotFoundError { description } => { (StatusCode::NOT_FOUND, ErrorPage::build(&description))
(StatusCode::NOT_FOUND, Html::from(description)) }
} _ => (
_ => (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())), StatusCode::INTERNAL_SERVER_ERROR,
})? ErrorPage::build(&e.to_string()),
.into(), ),
&TopLevelPage::Inventory, })?,
) &TopLevelPage::Inventory,
.into_string(),
), ),
)) ))
} }
@@ -306,7 +322,7 @@ async fn inventory_item_delete(
// async fn htmx_inventory_category_items( // async fn htmx_inventory_category_items(
// Path(id): Path<String>, // Path(id): Path<String>,
// ) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { // ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
// let pool = SqlitePoolOptions::new() // let pool = SqlitePoolOptions::new()
// .max_connections(5) // .max_connections(5)
// .connect("sqlite:///home/hannes-private/sync/items/items.sqlite") // .connect("sqlite:///home/hannes-private/sync/items/items.sqlite")
@@ -452,7 +468,7 @@ async fn trip_create(
async fn trips( async fn trips(
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
let trips: Vec<models::Trip> = query("SELECT * FROM trips") let trips: Vec<models::Trip> = query("SELECT * FROM trips")
.fetch(&state.database_pool) .fetch(&state.database_pool)
.map_ok(std::convert::TryInto::try_into) .map_ok(std::convert::TryInto::try_into)
@@ -460,25 +476,33 @@ async fn trips(
.await .await
// we have two error handling lines here. these are distinct errors // we have two error handling lines here. these are distinct errors
// this one is the SQL error that may arise during the query // 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() .into_iter()
.collect::<Result<Vec<models::Trip>, models::Error>>() .collect::<Result<Vec<models::Trip>, models::Error>>()
// and this one is the model mapping error that may arise e.g. during // and this one is the model mapping error that may arise e.g. during
// reading of the rows // 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(( Ok((
StatusCode::OK, StatusCode::OK,
Html::from( Root::build(TripManager::build(trips), &TopLevelPage::Trips),
Root::build(TripManager::build(trips).into(), &TopLevelPage::Trips).into_string(),
),
)) ))
} }
async fn trip( async fn trip(
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
let mut trip: models::Trip = let mut trip: models::Trip =
query("SELECT id,name,start_date,end_date,state,location,temp_min,temp_max FROM trips WHERE id = ?") query("SELECT id,name,start_date,end_date,state,location,temp_min,temp_max FROM trips WHERE id = ?")
.bind(id.to_string()) .bind(id.to_string())
@@ -488,32 +512,31 @@ async fn trip(
.map_err(|e: sqlx::Error| match e { .map_err(|e: sqlx::Error| match e {
sqlx::Error::RowNotFound => ( sqlx::Error::RowNotFound => (
StatusCode::NOT_FOUND, 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) trip.load_triptypes(&state.database_pool)
.await .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(( Ok((
StatusCode::OK, StatusCode::OK,
Html::from( Root::build(components::Trip::build(&trip), &TopLevelPage::Trips),
Root::build(
components::Trip::build(&trip).into_markup(),
&TopLevelPage::Trips,
)
.into_string(),
),
)) ))
} }
async fn trip_type_remove( async fn trip_type_remove(
Path((trip_id, type_id)): Path<(Uuid, Uuid)>, Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<Redirect, (StatusCode, Html<String>)> { ) -> Result<Redirect, (StatusCode, Markup)> {
let results = query( let results = query(
"DELETE FROM trips_to_triptypes AS ttt "DELETE FROM trips_to_triptypes AS ttt
WHERE ttt.trip_id = ? WHERE ttt.trip_id = ?
@@ -524,12 +547,12 @@ async fn trip_type_remove(
.bind(type_id.to_string()) .bind(type_id.to_string())
.execute(&state.database_pool) .execute(&state.database_pool)
.await .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 { if results.rows_affected() == 0 {
Err(( Err((
StatusCode::NOT_FOUND, 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 { } else {
Ok(Redirect::to(&format!("/trip/{trip_id}/"))) Ok(Redirect::to(&format!("/trip/{trip_id}/")))
@@ -539,8 +562,8 @@ async fn trip_type_remove(
async fn trip_type_add( async fn trip_type_add(
Path((trip_id, type_id)): Path<(Uuid, Uuid)>, Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<Redirect, (StatusCode, Html<String>)> { ) -> Result<Redirect, (StatusCode, Markup)> {
let results = query( query(
"INSERT INTO trips_to_triptypes "INSERT INTO trips_to_triptypes
(trip_id, trip_type_id) VALUES (?, ?)", (trip_id, trip_type_id) VALUES (?, ?)",
) )
@@ -560,21 +583,21 @@ async fn trip_type_add(
// TODO: this is not perfect, as both foreign keys // TODO: this is not perfect, as both foreign keys
// may be responsible for the error. how can we tell // may be responsible for the error. how can we tell
// which one? // which one?
Html::from(format!("invalid id: {}", code.to_string())), ErrorPage::build(&format!("invalid id: {}", code.to_string())),
) )
} }
"2067" => { "2067" => {
// SQLITE_CONSTRAINT_UNIQUE // SQLITE_CONSTRAINT_UNIQUE
( (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
Html::from(format!( ErrorPage::build(&format!(
"type {type_id} is already active for trip {trip_id}" "type {type_id} is already active for trip {trip_id}"
)), )),
) )
} }
_ => ( _ => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Html::from(format!( ErrorPage::build(&format!(
"got error with unknown code: {}", "got error with unknown code: {}",
sqlite_error.to_string() sqlite_error.to_string()
)), )),
@@ -583,7 +606,7 @@ async fn trip_type_add(
} else { } else {
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Html::from(format!( ErrorPage::build(&format!(
"got error without code: {}", "got error without code: {}",
sqlite_error.to_string() sqlite_error.to_string()
)), )),
@@ -592,7 +615,7 @@ async fn trip_type_add(
} }
_ => ( _ => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Html::from(format!("got unknown error: {}", e.to_string())), ErrorPage::build(&format!("got unknown error: {}", e.to_string())),
), ),
})?; })?;

View File

@@ -19,7 +19,6 @@ pub enum Error {
SqlError { description: String }, SqlError { description: String },
UuidError { description: String }, UuidError { description: String },
NotFoundError { description: String }, NotFoundError { description: String },
InvalidEnumError { description: String },
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@@ -34,9 +33,6 @@ impl fmt::Display for Error {
Self::NotFoundError { description } => { Self::NotFoundError { description } => {
write!(f, "Not found: {description}") write!(f, "Not found: {description}")
} }
Self::InvalidEnumError { description } => {
write!(f, "Enum error: {description}")
}
} }
} }
} }