Skip to content

Commit

Permalink
feat: replace JSON store with support for importing from files
Browse files Browse the repository at this point in the history
  • Loading branch information
suchapalaver committed Jan 3, 2024
1 parent 61968e7 commit 5e8163f
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 200 deletions.
6 changes: 3 additions & 3 deletions crates/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub enum ApiResponse {
FetchedRecipe((Recipe, Ingredients)),
ItemAlreadyAdded(Name),
Items(Items),
JsonToSqlite,
ImportToSqlite,
List(List),
NothingReturned(ApiCommand),
Recipes(Vec<Recipe>),
Expand Down Expand Up @@ -164,7 +164,7 @@ impl Display for ApiResponse {
}
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() {
Expand Down Expand Up @@ -216,7 +216,7 @@ impl From<StoreResponse> 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),
Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub enum ApiCommand {
Add(Add),
Delete(Delete),
FetchRecipe(Url),
MigrateJsonDbToSqlite,
ImportFromJson,
Read(Read),
Update(Update),
}
Expand Down
10 changes: 5 additions & 5 deletions crates/gust/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"),
)
Expand Down
6 changes: 3 additions & 3 deletions crates/gust/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub enum GustCommand {
Add(Add),
Delete(Delete),
FetchRecipe(Url),
MigrateJsonDbToSqlite,
ImportFromJson,
Read(Read),
Update(Update),
}
Expand Down Expand Up @@ -117,7 +117,7 @@ impl TryFrom<ArgMatches> for GustCommand {
}
_ => unimplemented!(),
})),
Some(("migrate-json-store", _)) => Ok(GustCommand::MigrateJsonDbToSqlite),
Some(("import", _)) => Ok(GustCommand::ImportFromJson),
_ => unreachable!(),
}
}
Expand All @@ -129,7 +129,7 @@ impl From<GustCommand> 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),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,23 @@
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 common::{items::Items, list::List, recipes::Recipe, Load};

use crate::store::{Storage, StoreError, StoreResponse};
use crate::store::StoreError;

pub const ITEMS_JSON_PATH: &str = "store.json";
pub const ITEMS_JSON_PATH: &str = "items.json";
pub const LIST_JSON_PATH: &str = "list.json";

#[derive(Clone)]
pub struct JsonStore {
pub struct ImportStore {
items: PathBuf,
list: PathBuf,
}

impl Default for JsonStore {
impl Default for ImportStore {
fn default() -> Self {
Self {
items: PathBuf::from(ITEMS_JSON_PATH),
Expand All @@ -36,7 +26,7 @@ impl Default for JsonStore {
}
}

impl JsonStore {
impl ImportStore {
pub fn new() -> Self {
Self::default()
}
Expand All @@ -51,122 +41,48 @@ impl JsonStore {
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)?)
pub fn items(&self) -> Result<Items, StoreError> {
Ok(Items::from_json(&self.items)?)
}
// 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)?)
}
}
pub fn recipes(&self) -> Result<Vec<Recipe>, StoreError> {
let mut recipes: HashSet<Recipe> = HashSet::new();

impl Storage for JsonStore {
async fn add_item(&self, item: &Name) -> Result<StoreResponse, StoreError> {
let mut groceries = Items::from_json(&self.items)?;
let 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()))
for item in groceries.collection() {
if let Some(item_recipes) = item.recipes() {
for recipe in item_recipes.iter().cloned() {
recipes.insert(recipe);
}
}
}
}

async fn add_checklist_item(&self, _item: &Name) -> Result<StoreResponse, StoreError> {
todo!()
}

async fn add_list_item(&self, _item: &Name) -> Result<StoreResponse, StoreError> {
todo!()
}

async fn add_list_recipe(&self, _recipe: &Recipe) -> Result<StoreResponse, StoreError> {
todo!()
}

async fn add_recipe(
&self,
_recipe: &Recipe,
_ingredients: &Ingredients,
) -> Result<StoreResponse, StoreError> {
todo!()
}

async fn checklist(&self) -> Result<StoreResponse, StoreError> {
todo!()
}

async fn delete_checklist_item(&self, _item: &Name) -> Result<StoreResponse, StoreError> {
todo!()
}

async fn delete_recipe(&self, _recipe: &Recipe) -> Result<StoreResponse, StoreError> {
todo!()
}
for recipe in groceries.recipes().cloned() {
recipes.insert(recipe);
}

async fn items(&self) -> Result<StoreResponse, StoreError> {
Ok(StoreResponse::Items(Items::from_json(&self.items)?))
}
let list = List::from_json(&self.list)?;

async fn list(&self) -> Result<StoreResponse, StoreError> {
Ok(StoreResponse::List(List::from_json(&self.list)?))
}
for recipe in list.recipes().cloned() {
recipes.insert(recipe);
}

async fn refresh_list(&self) -> Result<StoreResponse, StoreError> {
todo!()
Ok(recipes.into_iter().collect())
}

async fn recipe_ingredients(&self, recipe: &Recipe) -> Result<StoreResponse, StoreError> {
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)))
pub fn list(&self) -> Result<List, StoreError> {
Ok(List::from_json(&self.list)?)
}

async fn sections(&self) -> Result<StoreResponse, StoreError> {
todo!()
pub fn save_items(&self, object: impl serde::Serialize) -> Result<(), StoreError> {
let s = serde_json::to_string(&object)?;
Ok(fs::write(&self.items, s)?)
}

async fn recipes(&self) -> Result<StoreResponse, StoreError> {
let mut recipes: HashSet<Recipe> = 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()))
pub fn save_list(&self, object: impl serde::Serialize) -> Result<(), StoreError> {
let s = serde_json::to_string(&object)?;
Ok(fs::write(&self.list, s)?)
}
}

Expand All @@ -175,6 +91,7 @@ pub mod test {
use super::*;

use assert_fs::prelude::*;
use common::{item::Item, recipes::Recipe};

fn test_json_file() -> Result<assert_fs::NamedTempFile, Box<dyn std::error::Error>> {
let file = assert_fs::NamedTempFile::new("test1.json")?;
Expand Down Expand Up @@ -209,11 +126,8 @@ pub mod test {

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
let store = ImportStore::new().with_items_path(file.path());
store.items().unwrap()
}

#[test]
Expand All @@ -231,7 +145,7 @@ pub mod test {

#[tokio::test]
async fn test_save_items() -> Result<(), Box<dyn std::error::Error>> {
let store = JsonStore::new().with_items_path(&PathBuf::from("test_groceries.json"));
let store = ImportStore::new().with_items_path(&PathBuf::from("test_groceries.json"));
let items = Items::default();
insta::assert_json_snapshot!(items, @r#"
{
Expand All @@ -241,18 +155,13 @@ pub mod test {
}
"#);
store.save_items(items)?;
match store.items().await.unwrap() {
StoreResponse::Items(items) => {
insta::assert_json_snapshot!(items, @r#"
insta::assert_json_snapshot!(store.items().unwrap(), @r#"
{
"sections": [],
"collection": [],
"recipes": []
}
"#);
}
_ => panic!(),
}
std::fs::remove_file(store.items)?;
Ok(())
}
Expand Down Expand Up @@ -480,11 +389,9 @@ pub mod test {
#[tokio::test]
async fn test_delete_item_from_list() -> Result<(), Box<dyn std::error::Error>> {
let file = create_test_checklist_json_file().unwrap();
let store = JsonStore::new().with_list_path(file.path());
let store = ImportStore::new().with_list_path(file.path());

let StoreResponse::List(mut shopping_list) = store.list().await.unwrap() else {
todo!()
};
let mut shopping_list = store.list().unwrap();

let item = Item::new("kumquats").with_section("fresh");

Expand Down Expand Up @@ -603,11 +510,8 @@ pub mod test {

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
let store = ImportStore::new().with_list_path(file.path());
store.list().unwrap()
}

#[tokio::test]
Expand Down
2 changes: 1 addition & 1 deletion crates/persistence/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub mod json;
pub mod import_store;
pub mod models;
pub mod schema;
pub mod sqlite;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ pub fn migrate_recipes(
Ok(())
}

pub fn groceries(connection: &mut SqliteConnection, groceries: Items) -> Result<(), StoreError> {
pub fn migrate_items(
connection: &mut SqliteConnection,
groceries: Items,
) -> Result<(), StoreError> {
let items_table = schema::items::table;
let recipes_table = schema::recipes::table;
let sections_table = schema::sections::table;
Expand Down
Loading

0 comments on commit 5e8163f

Please sign in to comment.