From fd8ea90f0a7524025297411e06e20390dfc4558b Mon Sep 17 00:00:00 2001 From: Joseph Livesey Date: Sun, 31 Dec 2023 18:47:16 -0500 Subject: [PATCH] fix: make the library more ergonomic --- crates/api/src/lib.rs | 14 ++--- crates/common/src/item.rs | 16 +++++- crates/common/src/items.rs | 84 +++++++++------------------- crates/common/src/lib.rs | 13 ----- crates/common/src/list.rs | 65 ++++++++------------- crates/common/src/recipes.rs | 18 ++---- crates/gust/src/cli.rs | 4 -- crates/gust/src/command.rs | 14 ++--- crates/persistence/src/json/mod.rs | 20 +++---- crates/persistence/src/sqlite/mod.rs | 6 +- crates/persistence/src/store.rs | 5 +- 11 files changed, 93 insertions(+), 166 deletions(-) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 4d18af0..7c6186b 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -238,7 +238,7 @@ mod tests { let api = Api::init(StoreType::SqliteInMem).await.unwrap(); let response = api - .dispatch(ApiCommand::Add(Add::Recipe { recipe: Recipe::new("fluffy american pancakes").unwrap(), ingredients: Ingredients::from_input_string("135g/4¾oz plain flour, 1 tsp baking powder, ½ tsp salt, 2 tbsp caster sugar, 130ml/4½fl oz milk, 1 large egg, lightly beaten, 2 tbsp melted butter (allowed to cool slightly), plus extra for cooking") })) + .dispatch(ApiCommand::Add(Add::Recipe { recipe: Recipe::new("fluffy american pancakes"), ingredients: Ingredients::from_input_string("135g/4¾oz plain flour, 1 tsp baking powder, ½ tsp salt, 2 tbsp caster sugar, 130ml/4½fl oz milk, 1 large egg, lightly beaten, 2 tbsp melted butter (allowed to cool slightly), plus extra for cooking") })) .await .unwrap(); @@ -252,9 +252,9 @@ mod tests { insta::assert_display_snapshot!(response.to_string().trim(), @"fluffy american pancakes"); let response = api - .dispatch(ApiCommand::Read(Read::Recipe( - Recipe::new("fluffy american pancakes").unwrap(), - ))) + .dispatch(ApiCommand::Read(Read::Recipe(Recipe::new( + "fluffy american pancakes", + )))) .await .unwrap(); @@ -286,9 +286,9 @@ mod tests { "###); let response = api - .dispatch(ApiCommand::Delete(Delete::Recipe( - Recipe::new("fluffy american pancakes").unwrap(), - ))) + .dispatch(ApiCommand::Delete(Delete::Recipe(Recipe::new( + "fluffy american pancakes", + )))) .await .unwrap(); diff --git a/crates/common/src/item.rs b/crates/common/src/item.rs index 6c19f92..3c9b344 100644 --- a/crates/common/src/item.rs +++ b/crates/common/src/item.rs @@ -37,8 +37,16 @@ impl Item { self.recipes.as_ref() } - pub fn recipes_mut(&mut self) -> Option<&mut [Recipe]> { - self.recipes.as_deref_mut() + pub fn add_recipe(&mut self, recipe: &str) { + self.recipes + .get_or_insert_with(Vec::new) + .push(recipe.into()); + } + + pub fn delete_recipe(&mut self, name: &str) { + if let Some(vec) = self.recipes.as_mut() { + vec.retain(|x| x.as_str() != name) + } } pub fn with_section(mut self, section: impl Into) -> Self { @@ -108,4 +116,8 @@ impl Section { pub fn as_str(&self) -> &str { &self.0 } + + pub fn contains(&self, s: &Section) -> bool { + self.0.contains(s.as_str()) + } } diff --git a/crates/common/src/items.rs b/crates/common/src/items.rs index 6093151..892dd06 100644 --- a/crates/common/src/items.rs +++ b/crates/common/src/items.rs @@ -1,11 +1,9 @@ -use std::str::FromStr; - use serde::{Deserialize, Serialize}; use crate::{ - item::{Item, Name, Section}, + item::{Item, Section}, recipes::{Ingredients, Recipe}, - Load, ReadError, + Load, }; #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] @@ -51,16 +49,12 @@ impl Items { self.collection.push(item); } - pub fn delete_item(&mut self, name: &str) -> Result<(), ReadError> { - if let Ok(i) = self + pub fn delete_item(&mut self, name: &str) { + self.collection = self .collection - .iter() - .position(|x| x.name() == &Name::from(name)) - .ok_or(ReadError::ItemNotFound) - { - self.collection.remove(i); - } - Ok(()) + .drain(..) + .filter(|item| item.name().as_str() != name) + .collect(); } pub fn items(&self) -> impl Iterator { @@ -68,10 +62,8 @@ impl Items { .iter() .flat_map(|section| { self.collection.iter().filter(|item| { - let Some(item_section) = item.section() else { - return false; - }; - item_section.as_str().contains(section.as_str()) + item.section() + .map_or(false, |item_section| item_section.contains(section)) }) }) .collect::>() @@ -82,59 +74,35 @@ impl Items { self.recipes.iter() } - pub fn add_recipe(&mut self, name: &str, ingredients: &str) -> Result<(), ReadError> { - let recipe = Recipe::from_str(name)?; - - let ingredients = Ingredients::from_input_string(ingredients); - + pub fn add_recipe(&mut self, name: &str, ingredients: &str) { self.collection .iter_mut() - .filter(|x| ingredients.contains(x.name())) - .for_each(|x| match x.recipes_mut() { - Some(recipes) => recipes.to_vec().push(recipe.clone()), - None => { - x.recipes_mut().replace(&mut [recipe.clone()]); - } - }); - - self.recipes.push(recipe); - Ok(()) + .filter(|item| Ingredients::from_input_string(ingredients).contains(item.name())) + .for_each(|item| item.add_recipe(name)); + + self.recipes.push(name.into()); } - pub fn delete_recipe(&mut self, name: &str) -> Result<(), ReadError> { - if let Ok(i) = self + pub fn delete_recipe(&mut self, name: &str) { + self.recipes = self .recipes - .iter() - .position(|recipe| recipe.as_str() == name) - .ok_or(ReadError::ItemNotFound) - { - self.recipes.remove(i); - } + .drain(..) + .filter(|recipe| recipe.as_str() != name) + .collect(); + for item in &mut self.collection { - if let Some(recipes) = item.recipes_mut() { - if let Some(i) = recipes.iter().position(|recipe| recipe.as_str() == name) { - recipes.to_vec().remove(i); - } - } + item.delete_recipe(name); } - Ok(()) } - pub fn recipe_ingredients( - &self, - recipe: &str, - ) -> Result, ReadError> { - let recipe = Recipe::from_str(recipe)?; - Ok(self - .collection + pub fn recipe_ingredients(&self, recipe: &Recipe) -> impl Iterator { + self.collection .iter() .filter(|item| { - let Some(recipes) = &item.recipes() else { - return false; - }; - recipes.contains(&recipe) + item.recipes() + .map_or(false, |recipes| recipes.contains(recipe)) }) .collect::>() - .into_iter()) + .into_iter() } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 2b0badc..254cb71 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -13,21 +13,8 @@ use std::{ }; use serde::Deserialize; -use serde_json::Value; use thiserror::Error; -#[derive(Error, Debug)] -pub enum ReadError { - #[error("Invalid JSON: {0}")] - Json(Value), - - #[error("Item not found")] - ItemNotFound, - - #[error("No groceries library found")] - LibraryNotFound, -} - #[derive(Error, Debug)] pub enum LoadError { #[error("load error: {0}")] diff --git a/crates/common/src/list.rs b/crates/common/src/list.rs index 3e4bfc2..fc8c81f 100644 --- a/crates/common/src/list.rs +++ b/crates/common/src/list.rs @@ -1,11 +1,9 @@ -use std::str::FromStr; - use crate::{ input::user_wants_to_add_item_to_list, - item::{Item, Name, SECTIONS}, + item::{Item, SECTIONS}, items::Items, recipes::Recipe, - Load, ReadError, + Load, }; use serde::{Deserialize, Serialize}; @@ -87,7 +85,7 @@ impl List { } } - pub fn add_groceries(&mut self, groceries: &Items) -> Result<(), ReadError> { + pub fn add_groceries(&mut self, groceries: &Items) { // move everything off list to temp list let list_items: Vec = self.items.drain(..).collect(); let sections = SECTIONS; @@ -99,11 +97,8 @@ impl List { .iter() .filter(|item| item.section().is_some()) .filter(|item| { - if let Some(item_sec) = item.section() { - item_sec.as_str() == section - } else { - false - } + item.section() + .map_or(false, |item_sec| item_sec.as_str() == section) }) .cloned() .collect(); @@ -111,11 +106,9 @@ impl List { let b: Vec = groceries .collection() .filter(|item| { - if let Some(item_sec) = item.section() { + item.section().map_or(false, |item_sec| { item_sec.as_str() == section && !a.contains(item) - } else { - false - } + }) }) .cloned() .collect(); @@ -152,55 +145,41 @@ impl List { } } } - Ok(()) } pub fn add_item(&mut self, item: Item) { self.items.push(item); } - pub fn delete_groceries_item(&mut self, name: &str) -> Result<(), ReadError> { - if let Ok(i) = self + pub fn delete_groceries_item(&mut self, name: &str) { + self.items = self .items - .iter() - .position(|x| x.name() == &Name::from(name)) - .ok_or(ReadError::ItemNotFound) - { - self.items.remove(i); - } - Ok(()) + .drain(..) + .filter(|item| item.name().as_str() != name) + .collect(); } pub fn add_checklist_item(&mut self, item: Item) { self.checklist.push(item); } - pub fn delete_checklist_item(&mut self, name: &str) -> Result<(), ReadError> { - if let Ok(i) = self + pub fn delete_checklist_item(&mut self, name: &str) { + self.checklist = self .checklist - .iter() - .position(|x| x.name() == &Name::from(name)) - .ok_or(ReadError::ItemNotFound) - { - self.checklist.remove(i); - } - Ok(()) + .drain(..) + .filter(|item| item.name().as_str() != name) + .collect(); } pub fn add_recipe(&mut self, recipe: Recipe) { self.recipes.push(recipe); } - pub fn delete_recipe(&mut self, name: &str) -> Result<(), ReadError> { - let recipe = Recipe::from_str(name)?; - if let Ok(index) = self + pub fn delete_recipe(&mut self, name: &str) { + self.recipes = self .recipes - .iter() - .position(|x| x == &recipe) - .ok_or(ReadError::ItemNotFound) - { - self.recipes.remove(index); - } - Ok(()) + .drain(..) + .filter(|recipe| recipe.as_str() != name) + .collect(); } } diff --git a/crates/common/src/recipes.rs b/crates/common/src/recipes.rs index 716a9ff..beff428 100644 --- a/crates/common/src/recipes.rs +++ b/crates/common/src/recipes.rs @@ -1,8 +1,8 @@ -use std::{fmt, ops::Deref, str::FromStr}; +use std::{fmt, ops::Deref}; use serde::{Deserialize, Serialize}; -use crate::{item::Name, ReadError}; +use crate::item::Name; #[derive(Serialize, Deserialize, Clone, Debug, Default, Hash, Eq, PartialEq)] pub struct Recipe(String); @@ -14,8 +14,8 @@ impl fmt::Display for Recipe { } impl Recipe { - pub fn new(s: &str) -> Result { - Self::from_str(s) + pub fn new(s: &str) -> Self { + s.into() } pub fn new_unchecked(s: impl Into) -> Self { @@ -29,15 +29,7 @@ impl Recipe { impl From<&str> for Recipe { fn from(s: &str) -> Self { - Self(s.to_string()) - } -} - -impl FromStr for Recipe { - type Err = ReadError; - - fn from_str(s: &str) -> Result { - Ok(Self(s.trim().to_lowercase())) + Self(s.trim().to_lowercase()) } } diff --git a/crates/gust/src/cli.rs b/crates/gust/src/cli.rs index 674a98b..a511c01 100644 --- a/crates/gust/src/cli.rs +++ b/crates/gust/src/cli.rs @@ -1,6 +1,5 @@ use api::ApiError; use clap::{builder::NonEmptyStringValueParser, Arg, Command, ValueHint}; -use common::ReadError; use thiserror::Error; #[derive(Error, Debug)] @@ -11,9 +10,6 @@ pub enum CliError { #[error("invalid input: {0}")] ParseInputError(String), - #[error("read error: {0}")] - ReadError(#[from] ReadError), - #[error("URL parse error: {0}")] UrlParseError(#[from] url::ParseError), } diff --git a/crates/gust/src/command.rs b/crates/gust/src/command.rs index 8da698c..4292498 100644 --- a/crates/gust/src/command.rs +++ b/crates/gust/src/command.rs @@ -1,9 +1,7 @@ -use std::str::FromStr; - use common::{ commands::{Add, ApiCommand, Delete, Read, Update}, item::{Name, Section}, - recipes::{Ingredients, Recipe}, + recipes::Ingredients, }; use clap::ArgMatches; @@ -31,7 +29,7 @@ impl TryFrom for GustCommand { matches.get_one::("ingredients"), ) { let (recipe, ingredients) = ( - Recipe::from_str(recipe.trim())?, + recipe.as_str().into(), Ingredients::from_input_string(ingredients.trim()), ); @@ -55,7 +53,7 @@ impl TryFrom for GustCommand { )), Some(("list", matches)) => { if let Some(name) = matches.get_one::("recipe") { - Add::list_recipe_from_name(Recipe::from_str(name.trim())?) + Add::list_recipe_from_name(name.as_str().into()) } else if let Some(name) = matches.get_one::("item") { Add::list_item_from_name(Name::from(name.trim())) } else { @@ -68,7 +66,7 @@ impl TryFrom for GustCommand { )), Some(("delete", matches)) => Ok(GustCommand::Delete( if let Some(name) = matches.get_one::("recipe") { - Delete::recipe_from_name(Recipe::from_str(name.trim())?) + Delete::recipe_from_name(name.as_str().into()) } else if let Some(name) = matches.get_one::("item") { Delete::item_from_name(Name::from(name.trim())) } else { @@ -92,7 +90,7 @@ impl TryFrom for GustCommand { } Some(("read", matches)) => Ok(GustCommand::Read( if let Some(name) = matches.get_one::("recipe") { - Read::recipe_from_name(Recipe::from_str(name.trim())?) + Read::recipe_from_name(name.as_str().into()) } else if let Some(name) = matches.get_one::("item") { Read::item_from_name(Name::from(name.trim())) } else { @@ -111,7 +109,7 @@ impl TryFrom for GustCommand { let Some(name) = matches.get_one::("recipe") else { todo!() }; - Update::recipe_from_name(Recipe::from_str(name.trim())?) + Update::recipe_from_name(name.as_str().into()) } Some(("list", matches)) => { let Some(("clear", _)) = matches.subcommand() else { diff --git a/crates/persistence/src/json/mod.rs b/crates/persistence/src/json/mod.rs index f2338b2..38871fb 100644 --- a/crates/persistence/src/json/mod.rs +++ b/crates/persistence/src/json/mod.rs @@ -129,7 +129,7 @@ impl Storage for JsonStore { async fn recipe_ingredients(&self, recipe: &Recipe) -> Result { let items = Items::from_json(&self.items)?; let ingredients: Ingredients = items - .recipe_ingredients(&recipe.to_string())? + .recipe_ingredients(recipe) .map(|item| item.name()) .cloned() .collect(); @@ -367,7 +367,7 @@ pub mod test { "fried eggs for breakfast" ] "###); - items.delete_recipe("oatmeal chocolate chip cookies")?; + items.delete_recipe("oatmeal chocolate chip cookies"); insta::assert_json_snapshot!(items.recipes().collect::>(), @r###" [ "fried eggs for breakfast" @@ -493,7 +493,7 @@ pub mod test { } ] "###); - items.delete_item("eggs")?; + items.delete_item("eggs"); insta::assert_json_snapshot!(items.collection().collect::>(), @r###" [ { @@ -743,7 +743,7 @@ pub mod test { let ingredients = "kumquats, carrots, dried apricots, dried cranberries, chili, onion, garlic, cider vinegar, granulated sugar, honey, kosher salt, cardamom, cloves, coriander, ginger, black peppercorns"; items.add_item(item); - items.add_recipe(recipe, ingredients)?; + items.add_recipe(recipe, ingredients); insta::assert_json_snapshot!(items, @r###" { @@ -1000,7 +1000,7 @@ pub mod test { } ] "###); - shopping_list.delete_groceries_item("kumquats")?; + shopping_list.delete_groceries_item("kumquats"); insta::assert_json_snapshot!(shopping_list.items(), @r###" [ { @@ -1124,7 +1124,7 @@ pub mod test { } #[tokio::test] - async fn test_delete_checklist_item() -> Result<(), Box> { + async fn test_delete_checklist_item() { let mut shopping_list = checklist().await; let item = Item::new("kumquats").with_section("fresh"); shopping_list.add_checklist_item(item); @@ -1137,22 +1137,20 @@ pub mod test { } ] "###); - shopping_list.delete_checklist_item("kumquats")?; + shopping_list.delete_checklist_item("kumquats"); insta::assert_json_snapshot!(shopping_list.checklist(), @"[]"); - Ok(()) } #[tokio::test] - async fn test_delete_recipe_from_list() -> Result<(), Box> { + async fn test_delete_recipe_from_list() { let mut shopping_list = checklist().await; insta::assert_json_snapshot!(shopping_list.recipes().collect::>(), @r#" [ "tomato pasta" ] "#); - shopping_list.delete_recipe("tomato pasta")?; + shopping_list.delete_recipe("tomato pasta"); insta::assert_json_snapshot!(shopping_list.recipes().collect::>(), @"[]"); - Ok(()) } #[tokio::test] diff --git a/crates/persistence/src/sqlite/mod.rs b/crates/persistence/src/sqlite/mod.rs index f3487db..b4d4535 100644 --- a/crates/persistence/src/sqlite/mod.rs +++ b/crates/persistence/src/sqlite/mod.rs @@ -620,7 +620,7 @@ mod tests { let ingredients = Ingredients::from_iter(vec![Name::from("ingredient 1"), Name::from("ingredient 2")]); - let recipe = Recipe::new("test recipe").unwrap(); + let recipe = Recipe::new("test recipe"); store.add_recipe(&recipe, &ingredients).await.unwrap(); store.add_list_recipe(&recipe).await.unwrap(); @@ -663,7 +663,7 @@ mod tests { let ingredients = Ingredients::from_iter(vec![Name::from("ingredient 1"), Name::from("ingredient 2")]); - let recipe = Recipe::new("test recipe").unwrap(); + let recipe = Recipe::new("test recipe"); store.add_recipe(&recipe, &ingredients).await.unwrap(); let StoreResponse::Recipes(recipes) = store.recipes().await.unwrap() else { @@ -711,7 +711,7 @@ mod tests { let ingredients = Ingredients::from_iter(vec![Name::from("ingredient 1"), Name::from("ingredient 2")]); - let recipe = Recipe::new("test recipe").unwrap(); + let recipe = Recipe::new("test recipe"); store.add_recipe(&recipe, &ingredients).await.unwrap(); let StoreResponse::Recipes(recipes) = store.recipes().await.unwrap() else { diff --git a/crates/persistence/src/store.rs b/crates/persistence/src/store.rs index 3668a8a..7530215 100644 --- a/crates/persistence/src/store.rs +++ b/crates/persistence/src/store.rs @@ -5,7 +5,7 @@ use common::{ items::Items, list::List, recipes::{Ingredients, Recipe}, - LoadError, ReadError, + LoadError, }; use futures::FutureExt; use thiserror::Error; @@ -55,9 +55,6 @@ pub enum StoreError { #[error("Parse store type error: {0}")] ParseStoreType(String), - #[error("read error: {0}")] - ReadError(#[from] ReadError), - #[error("error reading/writing file: {0}")] ReadWriteError(#[from] std::io::Error),