From 79a0237495cbc7939e7afa481a817c22b24dadca Mon Sep 17 00:00:00 2001 From: Joseph Livesey Date: Wed, 3 Jan 2024 11:25:12 -0500 Subject: [PATCH 1/2] feat: replace JSON store with support for importing from files fix(common): remove unused input prompt functions fix(dockerignore): do not ignore import files fix(gitignore): update import file names fix(persistence): remove dependency on 'question' crate docs: replace migration from JSON store to import from file feat: replace JSON store with support for importing from files fix(common): remove unused methods on 'List' fix(common): remove unused methods on 'Items' fix(common): move 'Section' to separate module --- .dockerignore | 2 + .gitignore | 2 +- Cargo.lock | 1 - crates/api/src/lib.rs | 11 +- crates/common/src/commands.rs | 5 +- crates/common/src/input.rs | 86 +- crates/common/src/item.rs | 52 +- crates/common/src/items.rs | 79 +- crates/common/src/lib.rs | 1 + crates/common/src/list.rs | 139 +-- crates/common/src/section.rs | 26 + crates/gust/src/cli.rs | 10 +- crates/gust/src/command.rs | 9 +- crates/persistence/Cargo.toml | 1 - crates/persistence/src/import_store/mod.rs | 46 + crates/persistence/src/json/mod.rs | 830 ------------------ crates/persistence/src/lib.rs | 2 +- crates/persistence/src/models.rs | 6 +- .../src/{json/migrate.rs => sqlite/import.rs} | 28 +- crates/persistence/src/sqlite/migrations.rs | 16 + crates/persistence/src/sqlite/mod.rs | 44 +- crates/persistence/src/store.rs | 41 +- docs/cli.md | 4 +- docs/database.md | 155 +--- docs/diagrams/design.puml | 4 +- docs/diagrams/design.svg | 30 +- docs/docker.md | 16 +- 27 files changed, 195 insertions(+), 1451 deletions(-) create mode 100644 crates/common/src/section.rs create mode 100644 crates/persistence/src/import_store/mod.rs delete mode 100644 crates/persistence/src/json/mod.rs rename crates/persistence/src/{json/migrate.rs => sqlite/import.rs} (83%) create mode 100644 crates/persistence/src/sqlite/migrations.rs diff --git a/.dockerignore b/.dockerignore index 1a3ffdd..8dfab93 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,5 @@ !Cargo.* !crates !.env +!items.json +!list.json \ No newline at end of file diff --git a/.gitignore b/.gitignore index 409e195..1ae667b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ target/ # These are backup files generated by rustfmt **/*.rs.bk -# Local JSON 'groceries.json' and 'list.json' files +# Local JSON 'items.json' and 'list.json' files *.json /sqlite.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8f10fa4..b513156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1149,7 +1149,6 @@ dependencies = [ "dotenvy", "futures", "insta", - "question", "r2d2", "serde", "serde_derive", diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 6d2301e..3093471 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -2,10 +2,11 @@ use std::fmt::{self, Display}; use common::{ commands::ApiCommand, - item::{Item, Name, Section}, + item::{Item, Name}, items::Items, list::List, recipes::{Ingredients, Recipe}, + section::Section, }; use persistence::store::{Store, StoreDispatch, StoreError, StoreResponse, StoreType}; @@ -120,7 +121,7 @@ pub enum ApiResponse { FetchedRecipe((Recipe, Ingredients)), ItemAlreadyAdded(Name), Items(Items), - JsonToSqlite, + ImportToSqlite, List(List), NothingReturned(ApiCommand), Recipes(Vec), @@ -159,12 +160,12 @@ impl Display for ApiResponse { Self::ItemAlreadyAdded(item) => writeln!(f, "\nitem already added: {item}"), Self::Items(items) => { writeln!(f)?; - for item in items.collection() { + for item in items.collection_iter() { writeln!(f, "{item}")?; } Ok(()) } - Self::JsonToSqlite => writeln!(f, "\nJSON to SQLite data store migration successful"), + Self::ImportToSqlite => writeln!(f, "\nJSON to SQLite data store migration successful"), Self::List(list) => { writeln!(f)?; for item in list.items() { @@ -216,7 +217,7 @@ impl From for ApiResponse { StoreResponse::FetchedRecipe(item) => Self::FetchedRecipe(item), StoreResponse::ItemAlreadyAdded(item) => Self::ItemAlreadyAdded(item), StoreResponse::Items(item) => Self::Items(item), - StoreResponse::JsonToSqlite => Self::JsonToSqlite, + StoreResponse::ImportToSqlite => Self::ImportToSqlite, StoreResponse::List(item) => Self::List(item), StoreResponse::NothingReturned(item) => Self::NothingReturned(item), StoreResponse::Recipes(item) => Self::Recipes(item), diff --git a/crates/common/src/commands.rs b/crates/common/src/commands.rs index 6eb1a2a..29ac20c 100644 --- a/crates/common/src/commands.rs +++ b/crates/common/src/commands.rs @@ -1,8 +1,9 @@ use url::Url; use crate::{ - item::{Name, Section}, + item::Name, recipes::{Ingredients, Recipe}, + section::Section, }; #[derive(Debug)] @@ -10,7 +11,7 @@ pub enum ApiCommand { Add(Add), Delete(Delete), FetchRecipe(Url), - MigrateJsonDbToSqlite, + ImportFromJson, Read(Read), Update(Update), } diff --git a/crates/common/src/input.rs b/crates/common/src/input.rs index a585bba..c28db3e 100644 --- a/crates/common/src/input.rs +++ b/crates/common/src/input.rs @@ -1,37 +1,6 @@ -use crate::{ - item::{Item, SECTIONS}, - recipes::Recipe, -}; +use crate::item::Item; use question::{Answer, Question}; -pub fn user_wants_to_add_item() -> Answer { - Question::new("Add an item to our library?") - .default(question::Answer::NO) - .show_defaults() - .confirm() -} - -pub fn user_wants_to_print_list() -> Answer { - Question::new("Print shopping list?") - .default(question::Answer::NO) - .show_defaults() - .confirm() -} - -pub fn user_wants_to_add_more_recipe_ingredients_to_list() -> Answer { - Question::new("Add more recipe ingredients to our list?") - .default(question::Answer::NO) - .show_defaults() - .confirm() -} - -pub fn user_wants_to_add_items_to_list() -> Answer { - Question::new("Add items to list?") - .default(question::Answer::NO) - .show_defaults() - .confirm() -} - // Returns `None` in case user wishes to skip being asked further. pub fn user_wants_to_add_item_to_list(item: &Item) -> Option { let res = Question::new(&format!( @@ -50,59 +19,6 @@ pub fn user_wants_to_add_item_to_list(item: &Item) -> Option { } } -pub fn user_wants_to_save_list() -> Answer { - Question::new("Save current list?") - .default(question::Answer::NO) - .show_defaults() - .confirm() -} - -// Returns `None` in case user wishes to skip being asked further. -pub fn user_wants_to_add_recipe_to_list(recipe: &Recipe) -> Option { - let res = Question::new(&format!( - "Shall we add {recipe}? (*y*, *n* for next recipe, *s* to skip to end of recipes)", - )) - .acceptable(vec!["y", "n", "s"]) - .until_acceptable() - .default(Answer::RESPONSE("n".to_string())) - .ask(); - - match res { - Some(Answer::RESPONSE(res)) if &res == "y" => Some(true), - Some(Answer::RESPONSE(res)) if &res == "s" => None, - _ => Some(false), - } -} - -pub fn item_from_user() -> String { - let ans = Question::new( - "What is the item?\n\ - e.g. 'bread'", - ) - .ask(); - - if let Some(Answer::RESPONSE(ans)) = ans { - ans - } else { - item_from_user() - } -} - -pub fn section_from_user() -> String { - if let Some(Answer::RESPONSE(ans)) = Question::new( - "What is the section?\n\ - e.g. 'bread'", - ) - .acceptable(SECTIONS.to_vec()) - .until_acceptable() - .ask() - { - ans - } else { - section_from_user() - } -} - pub fn item_matches(item: &Item) -> Answer { Question::new(&format!("is *{item}* a match?")) .default(question::Answer::NO) diff --git a/crates/common/src/item.rs b/crates/common/src/item.rs index 4682831..b3021fc 100644 --- a/crates/common/src/item.rs +++ b/crates/common/src/item.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; -use crate::recipes::Recipe; +use crate::{recipes::Recipe, section::Section}; /// An item used in recipes or bought separately /// @@ -37,25 +37,14 @@ impl Item { self.recipes.as_ref() } - pub fn add_recipe(&mut self, recipe: &str) { - let recipe = recipe.into(); - if let Some(recipes) = &mut self.recipes { - if !recipes.contains(&recipe) { - recipes.push(recipe); - } - } else { - self.recipes = Some(vec![recipe]); - } - } - 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 { - self.section = Some(Section(section.into())); + pub fn with_section(mut self, section: &str) -> Self { + self.section = Some(section.into()); self } @@ -63,10 +52,6 @@ impl Item { self.recipes = Some(recipes.to_vec()); self } - - pub(crate) fn matches(&self, s: impl Into) -> bool { - s.into().split(' ').all(|word| !self.name.0.contains(word)) - } } impl fmt::Display for Item { @@ -101,34 +86,3 @@ impl Name { &self.0 } } - -pub const SECTIONS: [&str; 5] = ["fresh", "pantry", "protein", "dairy", "freezer"]; - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] -pub struct Section(String); - -impl From<&str> for Section { - fn from(value: &str) -> Self { - Self(value.trim().to_lowercase()) - } -} - -impl fmt::Display for Section { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Section { - pub fn new(sec: impl Into) -> Self { - Self(sec.into()) - } - - 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 b5a0288..e26fe29 100644 --- a/crates/common/src/items.rs +++ b/crates/common/src/items.rs @@ -1,10 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - item::{Item, Section}, - recipes::{Ingredients, Recipe}, - Load, -}; +use crate::{item::Item, recipes::Recipe, section::Section, Load}; #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] pub struct Items { @@ -33,84 +29,13 @@ impl Items { Self::default() } - pub fn collection(&self) -> impl Iterator { + pub fn collection_iter(&self) -> impl Iterator { self.collection.iter() } - pub fn get_item_matches(&self, name: &str) -> impl Iterator { - self.collection - .iter() - .filter(|item| item.matches(name)) - .collect::>() - .into_iter() - } - pub fn add_item(&mut self, item: Item) { if !self.collection.iter().any(|i| i.name() == item.name()) { self.collection.push(item); } } - - pub fn delete_item(&mut self, name: &str) { - self.collection = self - .collection - .drain(..) - .filter(|item| item.name().as_str() != name) - .collect(); - } - - pub fn items(&self) -> impl Iterator { - self.sections - .iter() - .flat_map(|section| { - self.collection.iter().filter(|item| { - item.section() - .map_or(false, |item_section| item_section.contains(section)) - }) - }) - .collect::>() - .into_iter() - } - - pub fn recipes(&self) -> impl Iterator { - self.recipes.iter() - } - - pub fn add_recipe(&mut self, name: &str, ingredients: &str) { - let ingredients = Ingredients::from_input_string(ingredients); - - ingredients - .iter() - .for_each(|ingredient| self.add_item(ingredient.into())); - - self.collection - .iter_mut() - .filter(|item| ingredients.contains(item.name())) - .for_each(|item| item.add_recipe(name)); - - self.recipes.push(name.into()); - } - - pub fn delete_recipe(&mut self, name: &str) { - self.recipes = self - .recipes - .drain(..) - .filter(|recipe| recipe.as_str() != name) - .collect(); - - for item in &mut self.collection { - item.delete_recipe(name); - } - } - - pub fn recipe_ingredients(&self, recipe: &Recipe) -> impl Iterator { - self.collection - .iter() - .filter(|item| { - item.recipes() - .map_or(false, |recipes| recipes.contains(recipe)) - }) - .collect::>() - .into_iter() - } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 254cb71..cb65c3d 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -5,6 +5,7 @@ pub mod item; pub mod items; pub mod list; pub mod recipes; +pub mod section; pub mod telemetry; use std::{ diff --git a/crates/common/src/list.rs b/crates/common/src/list.rs index fc8c81f..297566a 100644 --- a/crates/common/src/list.rs +++ b/crates/common/src/list.rs @@ -1,10 +1,4 @@ -use crate::{ - input::user_wants_to_add_item_to_list, - item::{Item, SECTIONS}, - items::Items, - recipes::Recipe, - Load, -}; +use crate::{item::Item, recipes::Recipe, Load}; use serde::{Deserialize, Serialize}; #[derive(Default, Serialize, Deserialize, Debug, Clone)] @@ -44,142 +38,11 @@ impl List { self } - pub fn with_items(mut self, items: Vec) -> Self { - self.items.extend(items); - self - } - - pub fn checklist(&self) -> &Vec { - &self.checklist - } - - pub fn recipes(&self) -> impl Iterator { - self.recipes.iter() - } - pub fn items(&self) -> &Vec { &self.items } - pub fn print(&self) { - if !self.checklist.is_empty() { - println!("Check if we need:"); - - self.checklist.iter().for_each(|item| { - println!("\t{}", item.name()); - }); - } - if !self.recipes.is_empty() { - println!("recipes:"); - - self.recipes.iter().for_each(|recipe| { - println!("\t{recipe}"); - }); - } - if !self.items.is_empty() { - println!("groceries:"); - - self.items.iter().for_each(|item| { - println!("\t{}", item.name()); - }); - } - } - - 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; - let groceries_by_section: Vec> = { - sections - .into_iter() - .map(|section| { - let mut a: Vec = list_items - .iter() - .filter(|item| item.section().is_some()) - .filter(|item| { - item.section() - .map_or(false, |item_sec| item_sec.as_str() == section) - }) - .cloned() - .collect(); - - let b: Vec = groceries - .collection() - .filter(|item| { - item.section().map_or(false, |item_sec| { - item_sec.as_str() == section && !a.contains(item) - }) - }) - .cloned() - .collect(); - a.extend(b); - a - }) - .collect() - }; - for section in groceries_by_section { - if !section.is_empty() { - for item in §ion { - if !self.items.contains(item) { - if let Some(recipes) = &item.recipes() { - if recipes.iter().any(|recipe| self.recipes.contains(recipe)) { - self.add_item(item.clone()); - } - } - } - } - for item in section { - if !self.items.contains(&item) { - let res = user_wants_to_add_item_to_list(&item); - - match res { - Some(true) => { - if !self.items.contains(&item) { - self.add_item(item.clone()); - } - } - Some(false) => continue, - None => break, - } - } - } - } - } - } - pub fn add_item(&mut self, item: Item) { self.items.push(item); } - - pub fn delete_groceries_item(&mut self, name: &str) { - self.items = self - .items - .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) { - self.checklist = self - .checklist - .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) { - self.recipes = self - .recipes - .drain(..) - .filter(|recipe| recipe.as_str() != name) - .collect(); - } } diff --git a/crates/common/src/section.rs b/crates/common/src/section.rs new file mode 100644 index 0000000..101d729 --- /dev/null +++ b/crates/common/src/section.rs @@ -0,0 +1,26 @@ +use core::fmt; + +use serde::{Deserialize, Serialize}; + +pub const SECTIONS: [&str; 5] = ["fresh", "pantry", "protein", "dairy", "freezer"]; + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +pub struct Section(String); + +impl From<&str> for Section { + fn from(value: &str) -> Self { + Self(value.trim().to_lowercase()) + } +} + +impl From for Section { + fn from(value: String) -> Self { + Self(value.trim().to_lowercase()) + } +} + +impl fmt::Display for Section { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/gust/src/cli.rs b/crates/gust/src/cli.rs index a511c01..fe73f37 100644 --- a/crates/gust/src/cli.rs +++ b/crates/gust/src/cli.rs @@ -188,10 +188,10 @@ fn update() -> Command { .subcommand(list().subcommand(refresh_list())) } -fn migrate() -> Command { - Command::new("migrate-json-store") +fn import() -> Command { + Command::new("import") .subcommand_required(false) - .about("migrate JSON store to Sqlite database") + .about("import from 'items.json' and 'list.json' files") } pub fn cli() -> Command { @@ -204,12 +204,12 @@ pub fn cli() -> Command { .subcommand(fetch()) .subcommand(read()) .subcommand(update()) - .subcommand(migrate()) + .subcommand(import()) .arg( Arg::new("store") .long("database") .num_args(1) - .value_parser(["json", "sqlite", "sqlite-inmem"]) + .value_parser(["sqlite", "sqlite-inmem"]) .default_value("sqlite") .help("which database to use"), ) diff --git a/crates/gust/src/command.rs b/crates/gust/src/command.rs index 8b84d69..86a3262 100644 --- a/crates/gust/src/command.rs +++ b/crates/gust/src/command.rs @@ -1,7 +1,8 @@ use common::{ commands::{Add, ApiCommand, Delete, Read, Update}, - item::{Name, Section}, + item::Name, recipes::{Ingredients, Recipe}, + section::Section, }; use clap::ArgMatches; @@ -13,7 +14,7 @@ pub enum GustCommand { Add(Add), Delete(Delete), FetchRecipe(Url), - MigrateJsonDbToSqlite, + ImportFromJson, Read(Read), Update(Update), } @@ -117,7 +118,7 @@ impl TryFrom for GustCommand { } _ => unimplemented!(), })), - Some(("migrate-json-store", _)) => Ok(GustCommand::MigrateJsonDbToSqlite), + Some(("import", _)) => Ok(GustCommand::ImportFromJson), _ => unreachable!(), } } @@ -129,7 +130,7 @@ impl From for ApiCommand { GustCommand::Add(cmd) => Self::Add(cmd), GustCommand::Delete(cmd) => Self::Delete(cmd), GustCommand::FetchRecipe(cmd) => Self::FetchRecipe(cmd), - GustCommand::MigrateJsonDbToSqlite => Self::MigrateJsonDbToSqlite, + GustCommand::ImportFromJson => Self::ImportFromJson, GustCommand::Read(cmd) => Self::Read(cmd), GustCommand::Update(cmd) => Self::Update(cmd), } diff --git a/crates/persistence/Cargo.toml b/crates/persistence/Cargo.toml index 22872cb..dd1564c 100644 --- a/crates/persistence/Cargo.toml +++ b/crates/persistence/Cargo.toml @@ -14,7 +14,6 @@ diesel = { workspace = true } diesel_migrations = { workspace = true } dotenvy = { workspace = true } futures = { workspace = true } -question = { workspace = true } r2d2 = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } diff --git a/crates/persistence/src/import_store/mod.rs b/crates/persistence/src/import_store/mod.rs new file mode 100644 index 0000000..3a5d99d --- /dev/null +++ b/crates/persistence/src/import_store/mod.rs @@ -0,0 +1,46 @@ +use std::{ + fs::{self}, + path::PathBuf, +}; + +use common::{items::Items, list::List, Load}; + +use crate::store::StoreError; + +pub const ITEMS_JSON_PATH: &str = "items.json"; +pub const LIST_JSON_PATH: &str = "list.json"; + +#[derive(Clone)] +pub struct ImportStore { + items: PathBuf, + list: PathBuf, +} + +impl Default for ImportStore { + fn default() -> Self { + Self { + items: PathBuf::from(ITEMS_JSON_PATH), + list: PathBuf::from(LIST_JSON_PATH), + } + } +} + +impl ImportStore { + pub fn items(&self) -> Result { + Ok(Items::from_json(&self.items)?) + } + + pub fn list(&self) -> Result { + Ok(List::from_json(&self.list)?) + } + + pub fn export_items(&self, object: impl serde::Serialize) -> Result<(), StoreError> { + let s = serde_json::to_string(&object)?; + Ok(fs::write(&self.items, s)?) + } + + pub fn export_list(&self, object: impl serde::Serialize) -> Result<(), StoreError> { + let s = serde_json::to_string(&object)?; + Ok(fs::write(&self.list, s)?) + } +} diff --git a/crates/persistence/src/json/mod.rs b/crates/persistence/src/json/mod.rs deleted file mode 100644 index 35e2157..0000000 --- a/crates/persistence/src/json/mod.rs +++ /dev/null @@ -1,830 +0,0 @@ -pub mod migrate; - -use std::{ - collections::HashSet, - fs::{self}, - path::{Path, PathBuf}, -}; - -use common::{ - input::item_matches, - item::{Item, Name}, - items::Items, - list::List, - recipes::{Ingredients, Recipe}, - Load, -}; -use question::Answer; - -use crate::store::{Storage, StoreError, StoreResponse}; - -pub const ITEMS_JSON_PATH: &str = "groceries.json"; - -pub const LIST_JSON_PATH: &str = "list.json"; - -#[derive(Clone)] -pub struct JsonStore { - items: PathBuf, - list: PathBuf, -} - -impl Default for JsonStore { - fn default() -> Self { - Self { - items: PathBuf::from(ITEMS_JSON_PATH), - list: PathBuf::from(LIST_JSON_PATH), - } - } -} - -impl JsonStore { - pub fn new() -> Self { - Self::default() - } - - pub fn with_items_path(mut self, path: &Path) -> Self { - self.items = path.to_path_buf(); - self - } - - pub fn with_list_path(mut self, path: &Path) -> Self { - self.list = path.to_path_buf(); - self - } - - pub fn save_items(&self, object: impl serde::Serialize) -> Result<(), StoreError> { - let s = serde_json::to_string(&object)?; - - Ok(fs::write(&self.items, s)?) - } - // TODO: I don't think it makes much sense to have these saved as separate JSON files. - pub fn save_list(&self, object: impl serde::Serialize) -> Result<(), StoreError> { - let s = serde_json::to_string(&object)?; - - Ok(fs::write(&self.list, s)?) - } -} - -impl Storage for JsonStore { - async fn add_item(&self, item: &Name) -> Result { - let mut groceries = Items::from_json(&self.items)?; - - if groceries - .get_item_matches(item.as_str()) - .any(|item| matches!(item_matches(item), Answer::YES)) - { - eprintln!("Item already in library"); - Ok(StoreResponse::ItemAlreadyAdded(item.clone())) - } else { - let new_item = Item::new(item.as_str()); - groceries.add_item(new_item); - Ok(StoreResponse::AddedItem(item.clone())) - } - } - - async fn add_checklist_item(&self, _item: &Name) -> Result { - todo!() - } - - async fn add_list_item(&self, _item: &Name) -> Result { - todo!() - } - - async fn add_list_recipe(&self, _recipe: &Recipe) -> Result { - todo!() - } - - async fn add_recipe( - &self, - _recipe: &Recipe, - _ingredients: &Ingredients, - ) -> Result { - todo!() - } - - async fn checklist(&self) -> Result { - todo!() - } - - async fn delete_checklist_item(&self, _item: &Name) -> Result { - todo!() - } - - async fn delete_recipe(&self, _recipe: &Recipe) -> Result { - todo!() - } - - async fn items(&self) -> Result { - Ok(StoreResponse::Items(Items::from_json(&self.items)?)) - } - - async fn list(&self) -> Result { - Ok(StoreResponse::List(List::from_json(&self.list)?)) - } - - async fn refresh_list(&self) -> Result { - todo!() - } - - async fn recipe_ingredients(&self, recipe: &Recipe) -> Result { - let items = Items::from_json(&self.items)?; - let ingredients: Ingredients = items - .recipe_ingredients(recipe) - .map(|item| item.name()) - .cloned() - .collect(); - - Ok(StoreResponse::RecipeIngredients(Some(ingredients))) - } - - async fn sections(&self) -> Result { - todo!() - } - - async fn recipes(&self) -> Result { - let mut recipes: HashSet = HashSet::new(); - - { - let groceries = Items::from_json(&self.items)?; - - for item in groceries.collection() { - if let Some(item_recipes) = item.recipes() { - for recipe in item_recipes.iter().cloned() { - recipes.insert(recipe); - } - } - } - - for recipe in groceries.recipes().cloned() { - recipes.insert(recipe); - } - } - - { - let list = List::from_json(&self.list)?; - - for recipe in list.recipes().cloned() { - recipes.insert(recipe); - } - } - Ok(StoreResponse::Recipes(recipes.into_iter().collect())) - } -} - -#[cfg(test)] -pub mod test { - use super::*; - - use assert_fs::prelude::*; - - fn test_json_file() -> Result> { - let file = assert_fs::NamedTempFile::new("test1.json")?; - file.write_str( - r#" - { - "sections": [ - "fresh", - "pantry", - "protein", - "dairy", - "freezer" - ], - "collection": [ - { - "name": "eggs", - "section": "dairy", - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] - } - ], - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] - }"#, - )?; - Ok(file) - } - - async fn items() -> Items { - let file = test_json_file().unwrap(); - let store = JsonStore::new().with_items_path(file.path()); - let StoreResponse::Items(items) = store.items().await.unwrap() else { - todo!() - }; - items - } - - #[test] - fn test_groceries_default() -> Result<(), Box> { - let default_items = Items::default(); - insta::assert_json_snapshot!(default_items, @r#" - { - "sections": [], - "collection": [], - "recipes": [] - } - "#); - Ok(()) - } - - #[tokio::test] - async fn test_save_items() -> Result<(), Box> { - let store = JsonStore::new().with_items_path(&PathBuf::from("test_groceries.json")); - let items = Items::default(); - insta::assert_json_snapshot!(items, @r#" - { - "sections": [], - "collection": [], - "recipes": [] - } - "#); - store.save_items(items)?; - match store.items().await.unwrap() { - StoreResponse::Items(items) => { - insta::assert_json_snapshot!(items, @r#" - { - "sections": [], - "collection": [], - "recipes": [] - } - "#); - } - _ => panic!(), - } - std::fs::remove_file(store.items)?; - Ok(()) - } - - #[tokio::test] - async fn test_delete_recipe() -> Result<(), Box> { - let mut items = items().await; - insta::assert_json_snapshot!(items.recipes().collect::>(), @r###" - [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] - "###); - items.delete_recipe("oatmeal chocolate chip cookies"); - insta::assert_json_snapshot!(items.recipes().collect::>(), @r###" - [ - "fried eggs for breakfast" - ] - "###); - Ok(()) - } - - #[tokio::test] - async fn test_delete_item() -> Result<(), Box> { - let mut items = items().await; - insta::assert_json_snapshot!(items.collection().collect::>(), @r###" - [ - { - "name": "eggs", - "section": "dairy", - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] - } - ] - "###); - items.delete_item("eggs"); - insta::assert_json_snapshot!(items.collection().collect::>(), @"[]"); - Ok(()) - } - - #[tokio::test] - async fn test_groceries() -> Result<(), Box> { - let mut items = items().await; - insta::assert_json_snapshot!(items, @r###" - { - "sections": [ - "fresh", - "pantry", - "protein", - "dairy", - "freezer" - ], - "collection": [ - { - "name": "eggs", - "section": "dairy", - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] - } - ], - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] - } - "###); - - let recipe = "cumquat chutney"; - - let item = Item::new("cumquats") - .with_section("fresh") - .with_recipes(&[Recipe::from(recipe)]); - - let ingredients = "cumquats, 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); - - insta::assert_json_snapshot!(items, @r###" - { - "sections": [ - "fresh", - "pantry", - "protein", - "dairy", - "freezer" - ], - "collection": [ - { - "name": "eggs", - "section": "dairy", - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] - }, - { - "name": "cumquats", - "section": "fresh", - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "carrots", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "dried apricots", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "dried cranberries", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "chili", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "onion", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "garlic", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "cider vinegar", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "granulated sugar", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "honey", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "kosher salt", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "cardamom", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "cloves", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "coriander", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "ginger", - "section": null, - "recipes": [ - "cumquat chutney" - ] - }, - { - "name": "black peppercorns", - "section": null, - "recipes": [ - "cumquat chutney" - ] - } - ], - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast", - "cumquat chutney" - ] - } - "###); - - Ok(()) - } - - #[tokio::test] - async fn test_delete_item_from_list() -> Result<(), Box> { - let file = create_test_checklist_json_file().unwrap(); - let store = JsonStore::new().with_list_path(file.path()); - - let StoreResponse::List(mut shopping_list) = store.list().await.unwrap() else { - todo!() - }; - - let item = Item::new("kumquats").with_section("fresh"); - - shopping_list.add_item(item); - insta::assert_json_snapshot!(shopping_list.items(), @r###" - [ - { - "name": "garlic", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "tomatoes", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "basil", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "pasta", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "olive oil", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "parmigiana", - "section": "dairy", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "kumquats", - "section": "fresh", - "recipes": null - } - ] - "###); - shopping_list.delete_groceries_item("kumquats"); - insta::assert_json_snapshot!(shopping_list.items(), @r###" - [ - { - "name": "garlic", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "tomatoes", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "basil", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "pasta", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "olive oil", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "parmigiana", - "section": "dairy", - "recipes": [ - "tomato pasta" - ] - } - ] - "###); - Ok(()) - } - - fn create_test_checklist_json_file( - ) -> Result> { - let file = assert_fs::NamedTempFile::new("test3.json")?; - file.write_str( - r#" - {"checklist":[],"recipes":["tomato pasta"],"items":[{"name":"garlic","section":"fresh","is_ingredient":true,"recipes":["tomato pasta"]},{"name":"tomatoes","section":"fresh","is_ingredient":true,"recipes":["tomato pasta"]},{"name":"basil","section":"fresh","is_ingredient":true,"recipes":["tomato pasta"]},{"name":"pasta","section":"pantry","is_ingredient":true,"recipes":["tomato pasta"]},{"name":"olive oil","section":"pantry","is_ingredient":true,"recipes":["tomato pasta"]},{"name":"parmigiana","section":"dairy","is_ingredient":true,"recipes":["tomato pasta"]}]} - "# - )?; - Ok(file) - } - - async fn checklist() -> List { - let file = create_test_checklist_json_file().unwrap(); - let store = JsonStore::new().with_list_path(file.path()); - let StoreResponse::List(list) = store.list().await.unwrap() else { - todo!() - }; - list - } - - #[tokio::test] - 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); - insta::assert_json_snapshot!(shopping_list.checklist(), @r###" - [ - { - "name": "kumquats", - "section": "fresh", - "recipes": null - } - ] - "###); - shopping_list.delete_checklist_item("kumquats"); - insta::assert_json_snapshot!(shopping_list.checklist(), @"[]"); - } - - #[tokio::test] - 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"); - insta::assert_json_snapshot!(shopping_list.recipes().collect::>(), @"[]"); - } - - #[tokio::test] - async fn json_from_file() -> Result<(), Box> { - let list = checklist().await; - - insta::assert_json_snapshot!(list, @r###" - { - "checklist": [], - "recipes": [ - "tomato pasta" - ], - "items": [ - { - "name": "garlic", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "tomatoes", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "basil", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "pasta", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "olive oil", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "parmigiana", - "section": "dairy", - "recipes": [ - "tomato pasta" - ] - } - ] - } - "###); - Ok(()) - } - - #[tokio::test] - async fn test_add_groceries_item_and_add_recipe() -> Result<(), Box> { - let mut list = checklist().await; - insta::assert_json_snapshot!(list, @r###" - { - "checklist": [], - "recipes": [ - "tomato pasta" - ], - "items": [ - { - "name": "garlic", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "tomatoes", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "basil", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "pasta", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "olive oil", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "parmigiana", - "section": "dairy", - "recipes": [ - "tomato pasta" - ] - } - ] - } - "###); - - let recipe = Recipe::from("cumquat chutney"); - - let item = Item::new("cumquats") - .with_section("fresh") - .with_recipes(&[recipe.clone()]); - - list.add_item(item); - list.add_recipe(recipe); - insta::assert_json_snapshot!(list, @r###" - { - "checklist": [], - "recipes": [ - "tomato pasta", - "cumquat chutney" - ], - "items": [ - { - "name": "garlic", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "tomatoes", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "basil", - "section": "fresh", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "pasta", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "olive oil", - "section": "pantry", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "parmigiana", - "section": "dairy", - "recipes": [ - "tomato pasta" - ] - }, - { - "name": "cumquats", - "section": "fresh", - "recipes": [ - "cumquat chutney" - ] - } - ] - } - "###); - - Ok(()) - } -} diff --git a/crates/persistence/src/lib.rs b/crates/persistence/src/lib.rs index a582af7..1066268 100644 --- a/crates/persistence/src/lib.rs +++ b/crates/persistence/src/lib.rs @@ -1,4 +1,4 @@ -pub mod json; +pub mod import_store; pub mod models; pub mod schema; pub mod sqlite; diff --git a/crates/persistence/src/models.rs b/crates/persistence/src/models.rs index 87c4a81..dc594d5 100644 --- a/crates/persistence/src/models.rs +++ b/crates/persistence/src/models.rs @@ -77,9 +77,9 @@ impl ItemInfo for Section { } } -impl From
for common::item::Section { - fn from(value: Section) -> common::item::Section { - common::item::Section::new(value.name) +impl From
for common::section::Section { + fn from(value: Section) -> common::section::Section { + value.name.into() } } diff --git a/crates/persistence/src/json/migrate.rs b/crates/persistence/src/sqlite/import.rs similarity index 83% rename from crates/persistence/src/json/migrate.rs rename to crates/persistence/src/sqlite/import.rs index 8b66055..ff936f8 100644 --- a/crates/persistence/src/json/migrate.rs +++ b/crates/persistence/src/sqlite/import.rs @@ -1,4 +1,4 @@ -use common::{item::SECTIONS, items::Items, recipes::Recipe}; +use common::{items::Items, section::SECTIONS}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SqliteConnection}; use crate::{ @@ -24,32 +24,12 @@ pub fn migrate_sections(connection: &mut SqliteConnection) -> Result<(), StoreEr Ok(()) } -pub fn migrate_recipes( - connection: &mut SqliteConnection, - recipes: Vec, -) -> Result<(), StoreError> { - use crate::schema::recipes; - - for recipe in recipes { - let recipe = NewRecipe { - name: &recipe.to_string().to_lowercase(), - }; - - diesel::insert_into(recipes::table) - .values(&recipe) - .on_conflict_do_nothing() - .execute(connection)?; - } - - Ok(()) -} - -pub fn groceries(connection: &mut SqliteConnection, groceries: Items) -> Result<(), StoreError> { +pub fn migrate_items(connection: &mut SqliteConnection, items: Items) -> Result<(), StoreError> { let items_table = schema::items::table; let recipes_table = schema::recipes::table; let sections_table = schema::sections::table; - for item in groceries.collection() { + for item in items.collection_iter() { // add the item to the item table let new_item = NewItem { name: item.name().as_str(), @@ -102,7 +82,7 @@ pub fn groceries(connection: &mut SqliteConnection, groceries: Items) -> Result< } } - if let Some(item_section) = &item.section() { + if let Some(item_section) = item.section() { // log the item_id in items_sections let results = sections_table .filter(schema::sections::dsl::name.eq(item_section.to_string())) diff --git a/crates/persistence/src/sqlite/migrations.rs b/crates/persistence/src/sqlite/migrations.rs new file mode 100644 index 0000000..41e3b83 --- /dev/null +++ b/crates/persistence/src/sqlite/migrations.rs @@ -0,0 +1,16 @@ +use diesel::sqlite::Sqlite; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; + +use crate::store::StoreError; + +pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations"); + +pub fn run_migrations(connection: &mut impl MigrationHarness) -> Result<(), StoreError> { + // This will run the necessary migrations. + // + // See the documentation for `MigrationHarness` for + // all available methods. + connection.run_pending_migrations(MIGRATIONS)?; + + Ok(()) +} diff --git a/crates/persistence/src/sqlite/mod.rs b/crates/persistence/src/sqlite/mod.rs index b4d4535..ca86490 100644 --- a/crates/persistence/src/sqlite/mod.rs +++ b/crates/persistence/src/sqlite/mod.rs @@ -1,3 +1,6 @@ +mod import; +mod migrations; + use std::{env, ops::Deref}; use common::{ @@ -5,11 +8,11 @@ use common::{ list::List, recipes::{Ingredients, Recipe}, }; -use diesel::{prelude::*, r2d2::ConnectionManager, sqlite::Sqlite, SqliteConnection}; -use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use diesel::{prelude::*, r2d2::ConnectionManager, SqliteConnection}; use r2d2::{Pool, PooledConnection}; use crate::{ + import_store::ImportStore, models::{ self, Item, ItemInfo, NewChecklistItem, NewItem, NewItemRecipe, NewListItem, NewListRecipe, NewRecipe, RecipeModel, Section, @@ -18,6 +21,11 @@ use crate::{ store::{Storage, StoreError, StoreResponse}, }; +use self::{ + import::{migrate_items, migrate_sections}, + migrations::run_migrations, +}; + pub struct DbUri(String); impl Default for DbUri { @@ -82,18 +90,6 @@ impl Connection for DatabaseConnector { } } -pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations"); - -fn run_migrations(connection: &mut impl MigrationHarness) -> Result<(), StoreError> { - // This will run the necessary migrations. - // - // See the documentation for `MigrationHarness` for - // all available methods. - connection.run_pending_migrations(MIGRATIONS)?; - - Ok(()) -} - #[derive(Clone)] pub struct SqliteStore { pool: ConnectionPool, @@ -440,6 +436,20 @@ impl Storage for SqliteStore { .await? } + async fn import_from_json(&self) -> Result { + let import_store = ImportStore::default(); + let mut connection = self.connection()?; + let items = import_store.items()?; + tokio::task::spawn_blocking(move || { + connection.immediate_transaction(|connection| { + migrate_sections(connection)?; + migrate_items(connection, items)?; + Ok(StoreResponse::ImportToSqlite) + }) + }) + .await? + } + async fn items(&self) -> Result { use schema::items::dsl::items; @@ -521,7 +531,7 @@ impl Storage for SqliteStore { .load::
(connection)? .into_iter() .map(|sec| sec.name().into()) - .collect(), + .collect::>(), )) }) }) @@ -594,7 +604,9 @@ mod tests { todo!() }; - assert!(items.collection().any(|item| item.name() == &item_name)); + assert!(items + .collection_iter() + .any(|item| item.name() == &item_name)); } #[tokio::test] diff --git a/crates/persistence/src/store.rs b/crates/persistence/src/store.rs index 518e703..80384e9 100644 --- a/crates/persistence/src/store.rs +++ b/crates/persistence/src/store.rs @@ -1,10 +1,11 @@ use common::{ commands::{Add, ApiCommand, Delete, Read, Update}, fetcher::{FetchError, Fetcher}, - item::{Item, Name, Section}, + item::{Item, Name}, items::Items, list::List, recipes::{Ingredients, Recipe}, + section::Section, LoadError, }; use futures::FutureExt; @@ -18,13 +19,7 @@ use url::Url; use std::{error::Error, fmt::Debug, fmt::Display, str::FromStr}; -use crate::{ - json::{ - migrate::{groceries, migrate_recipes, migrate_sections}, - JsonStore, - }, - sqlite::{DbUri, SqliteStore}, -}; +use crate::sqlite::{DbUri, SqliteStore}; #[derive(Error, Debug)] pub enum StoreError { @@ -67,7 +62,6 @@ pub enum StoreError { #[derive(Debug)] pub enum StoreType { - Json, Sqlite, SqliteInMem, } @@ -75,7 +69,6 @@ pub enum StoreType { impl Display for StoreType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - StoreType::Json => write!(f, "json"), StoreType::Sqlite => write!(f, "sqlite"), StoreType::SqliteInMem => write!(f, "sqlite-inmem"), } @@ -87,7 +80,6 @@ impl FromStr for StoreType { fn from_str(s: &str) -> Result { match s { - "json" => Ok(Self::Json), "sqlite" => Ok(Self::Sqlite), "sqlite-inmem" => Ok(Self::SqliteInMem), _ => Err(StoreError::ParseStoreType( @@ -100,7 +92,6 @@ impl FromStr for StoreType { #[derive(Clone)] pub enum Store { - Json(JsonStore), Sqlite(SqliteStore), } @@ -108,7 +99,6 @@ impl Store { pub async fn from_store_type(store_type: StoreType) -> Result { use StoreType::*; match store_type { - Json => Ok(Self::Json(JsonStore::default())), Sqlite => Ok(Self::Sqlite(SqliteStore::new(DbUri::new()).await?)), SqliteInMem => Ok(Self::Sqlite(SqliteStore::new(DbUri::inmem()).await?)), } @@ -149,7 +139,6 @@ impl Store { async fn execute_transaction(&self, command: ApiCommand) -> Result { match self { - Self::Json(store) => store.execute_transaction(command).await, Self::Sqlite(store) => store.execute_transaction(command).await, } } @@ -196,9 +185,9 @@ pub enum StoreResponse { DeletedRecipe(Recipe), DeletedChecklistItem(Name), FetchedRecipe((Recipe, Ingredients)), + ImportToSqlite, ItemAlreadyAdded(Name), Items(Items), - JsonToSqlite, List(List), NothingReturned(ApiCommand), Recipes(Vec), @@ -213,7 +202,7 @@ pub(crate) trait Storage: Send + Sync + 'static { ApiCommand::Add(cmd) => self.add(cmd).await, ApiCommand::Delete(cmd) => self.delete(cmd).await, ApiCommand::FetchRecipe(url) => self.fetch_recipe(url).await, - ApiCommand::MigrateJsonDbToSqlite => self.migrate_json_store_to_sqlite().await, + ApiCommand::ImportFromJson => self.import_from_json().await, ApiCommand::Read(cmd) => self.read(cmd).await, ApiCommand::Update(cmd) => self.update(cmd).await, } @@ -272,25 +261,7 @@ pub(crate) trait Storage: Send + Sync + 'static { Ok(StoreResponse::FetchedRecipe((recipe, ingredients))) } - async fn migrate_json_store_to_sqlite(&self) -> Result { - let sqlite_store = SqliteStore::new(DbUri::new()).await?; - let mut connection = sqlite_store.connection()?; - let StoreResponse::Items(grocery_items) = self.items().await? else { - todo!() - }; - let StoreResponse::Recipes(recipes) = self.recipes().await? else { - todo!() - }; - tokio::task::spawn_blocking(move || { - connection.immediate_transaction(|connection| { - migrate_sections(connection)?; - migrate_recipes(connection, recipes)?; - groceries(connection, grocery_items)?; - Ok(StoreResponse::JsonToSqlite) - }) - }) - .await? - } + async fn import_from_json(&self) -> Result; // Create async fn add_item(&self, item: &Name) -> Result; diff --git a/docs/cli.md b/docs/cli.md index 2f83df0..815c160 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -25,11 +25,11 @@ Commands: delete delete stuff read read stuff update update stuff - migrate-json-store migrate JSON store to Sqlite database + import import from 'items.json' and 'list.json' files help Print this message or the help of the given subcommand(s) Options: - --database which database to use [default: json] [possible values: json, sqlite] + --database which database to use [default: sqlite] [possible values: sqlite, sqlite-inmem] -h, --help Print help ``` diff --git a/docs/database.md b/docs/database.md index 57e24ba..7a7bb44 100644 --- a/docs/database.md +++ b/docs/database.md @@ -3,164 +3,25 @@ ## Contents - [Storage Options](#storage-options) -- [Migrating from JSON to SQLite](#migrating-from-json-to-sqlite) +- [Importing from files](#importing-from-files) ## Storage Options -There are two supported storage options: +Here are the supported storage options: -- [JSON](#json) -- [SQLite](#sqlite) - -### JSON - -To use the application with JSON as the database, use -the `--database` option flag. For example, - -```bash -cargo run -- --database json read recipes -``` - -Here's an example of a very small example JSON store -with just two recipes: - -```json -{ - "sections": [ - "fresh", - "pantry", - "protein", - "dairy", - "freezer" - ], - "collection": [ - { - "name": "eggs", - "section": "dairy", - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] - }, - { - "name": "milk", - "section": "dairy", - "recipes": [] - }, - { - "name": "spinach", - "section": "fresh", - "recipes": [ - "fried eggs for breakfast" - ] - }, - { - "name": "beer", - "section": "dairy", - "recipes": [] - }, - { - "name": "unsalted butter", - "section": "dairy", - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] - }, - { - "name": "bread", - "section": "pantry", - "recipes": [ - "fried eggs for breakfast" - ] - }, - { - "name": "old fashioned rolled oats", - "section": "pantry", - "recipes": [ - "oatmeal chocolate chip cookies" - ] - }, - { - "name": "chocolate chips", - "section": "pantry", - "recipes": [ - "oatmeal chocolate chip cookies" - ] - }, - { - "name": "baking powder", - "section": "pantry", - "recipes": [ - "oatmeal chocolate chip cookies" - ] - }, - { - "name": "baking soda", - "section": "pantry", - "recipes": [ - "oatmeal chocolate chip cookies" - ] - }, - { - "name": "salt", - "section": "pantry", - "recipes": [ - "oatmeal chocolate chip cookies" - ] - }, - { - "name": "white sugar", - "section": "pantry", - "recipes": [ - "oatmeal chocolate chip cookies" - ] - }, - { - "name": "vanilla extract", - "section": "pantry", - "recipes": [ - "oatmeal chocolate chip cookies" - ] - }, - { - "name": "whole-wheat flour", - "section": "pantry", - "recipes": [ - "oatmeal chocolate chip cookies" - ] - }, - { - "name": "1/2 & 1/2", - "section": "dairy", - "recipes": [ - "fried eggs for breakfast" - ] - }, - { - "name": "feta", - "section": "dairy", - "recipes": [ - "fried eggs for breakfast" - ] - } - ], - "recipes": [ - "oatmeal chocolate chip cookies", - "fried eggs for breakfast" - ] -} -``` +- [x] [SQLite](#sqlite) +- [ ] Postgres ### SQLite SQLite is the default storage option. --- -## Migrating from JSON to SQLite +## Importing from files -You can migrate a JSON store to an Sqlite database by running +You can import from JSON files, for now named by default `items.json` and `list.json` +to SQLite by running ```bash -cargo run -- --database sqlite migrate-json-store +cargo run -- --database sqlite import ``` diff --git a/docs/diagrams/design.puml b/docs/diagrams/design.puml index 409be93..ef0761e 100644 --- a/docs/diagrams/design.puml +++ b/docs/diagrams/design.puml @@ -4,11 +4,11 @@ skinparam classFontColor darkSlateGray package "gust" { - [API {\n + Store\n}] *-down-* [Store (JSON | SQLite)] + [API {\n + Store\n}] *-down-* [Store (SQLite)] [CLI] <---> [API {\n + Store\n}] : API commands\n& responses [common types\n and methods] .right. [API {\n + Store\n}] [common types\n and methods] .right. [CLI] - [common types\n and methods] .right. [Store (JSON | SQLite)] + [common types\n and methods] .right. [Store (SQLite)] [API {\n + Store\n}] o-right-o [RecipeFetcher] } interface "\t\t\tcooking sites\n\t\t\t(e.g. NYT Cooking,\n\t\t\tBBC Food)" as ext diff --git a/docs/diagrams/design.svg b/docs/diagrams/design.svg index b52bdaf..3d33d86 100644 --- a/docs/diagrams/design.svg +++ b/docs/diagrams/design.svg @@ -1,29 +1,29 @@ -gustAPI {+ Store}Store (JSON | SQLite)CLIcommon typesand methodsRecipeFetchercooking sites(e.g. NYT Cooking,BBC Food)API {+ Store}Store (SQLite)CLIcommon typesand methodsRecipeFetchercooking sites(e.g. NYT Cooking,BBC Food)API commands& responses