diff --git a/Cargo.lock b/Cargo.lock index 01a2e07..15dbe26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,9 @@ # It is not intended for manual editing. [[package]] name = "abortable_parser" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f520551d986b2be672fac2b1d749874f914ecc03fb347c7b0b5a7e541ffe435" - -[[package]] -name = "ahash" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" +checksum = "bec7b38b411d838e24b7914898b2d3cf3e24adbd81b6edf778e80ea23fe5e9d1" [[package]] name = "autocfg" @@ -18,25 +12,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "bindgen" -version = "0.58.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", -] - [[package]] name = "bitflags" version = "1.2.1" @@ -49,15 +24,6 @@ version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" -[[package]] -name = "cexpr" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -77,17 +43,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "clang-sys" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "dirs-next" version = "2.0.0" @@ -115,18 +70,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fs2" version = "0.4.3" @@ -148,79 +91,20 @@ dependencies = [ "wasi", ] -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashlink" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" -dependencies = [ - "hashbrown", -] - [[package]] name = "kitchen" version = "0.1.0" dependencies = [ - "recipe-store", "recipes", "rustyline", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" -[[package]] -name = "libloading" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19cb1effde5f834799ac5e5ef0e40d45027cd74f271b1de786ba8abb30e2164d" -dependencies = [ - "bindgen", - "cc", - "pkg-config", - "vcpkg", -] - [[package]] name = "log" version = "0.4.14" @@ -257,16 +141,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nom" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" -dependencies = [ - "memchr", - "version_check", -] - [[package]] name = "num-bigint" version = "0.4.0" @@ -309,36 +183,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "pkg-config" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" - -[[package]] -name = "proc-macro2" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" -dependencies = [ - "proc-macro2", -] - [[package]] name = "radix_trie" version = "0.2.1" @@ -349,16 +193,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "recipe-store" -version = "0.1.0" -dependencies = [ - "chrono", - "recipes", - "rusqlite", - "uuid", -] - [[package]] name = "recipes" version = "0.1.0" @@ -388,44 +222,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "regex" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" -dependencies = [ - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" - -[[package]] -name = "rusqlite" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc783b7ddae608338003bac1fa00b6786a75a9675fbd8e87243ecfdea3f6ed2" -dependencies = [ - "bitflags", - "chrono", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "memchr", - "smallvec", - "uuid", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustyline" version = "8.0.0" @@ -455,12 +251,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "shlex" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d" - [[package]] name = "smallvec" version = "1.6.1" @@ -490,12 +280,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" -[[package]] -name = "unicode-xid" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" - [[package]] name = "utf8parse" version = "0.2.0" @@ -511,18 +295,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "vcpkg" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" - -[[package]] -name = "version_check" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" - [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index bb126c6..c1c48ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = [ "recipes", "recipe-store", "kitchen" ] \ No newline at end of file +members = [ "recipes", "kitchen" ] \ No newline at end of file diff --git a/kitchen/Cargo.toml b/kitchen/Cargo.toml index 59c7bbc..fcc09b6 100644 --- a/kitchen/Cargo.toml +++ b/kitchen/Cargo.toml @@ -8,5 +8,4 @@ edition = "2018" [dependencies] recipes = {path = "../recipes" } -recipe-store = {path = "../recipe-store" } rustyline = "~8.0.0" diff --git a/kitchen/src/api.rs b/kitchen/src/api.rs index 612a47a..7fe5621 100644 --- a/kitchen/src/api.rs +++ b/kitchen/src/api.rs @@ -11,23 +11,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use std::convert::From; - -use recipe_store::{RecipeStore, SqliteBackend}; -use recipes::*; - -pub struct Api { - store: SqliteBackend, -} - -impl Api { - pub fn new_recipe_from_str(&self, input: &str) {} - - pub fn new_mealplan_from_str(&self, input: &str) {} -} - -impl From for Api { - fn from(store: SqliteBackend) -> Self { - Api { store } - } -} diff --git a/kitchen/src/cli.rs b/kitchen/src/cli.rs new file mode 100644 index 0000000..a3941ed --- /dev/null +++ b/kitchen/src/cli.rs @@ -0,0 +1,137 @@ +// Copyright 2021 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use std::convert::Into; +use std::convert::TryInto; + +use rustyline::error::ReadlineError; +use rustyline::Editor; + +use recipes::{Ingredient, Recipe, Step}; + +pub enum CliResponse { + Interrupt, + EndOfInput, + ReadItem(T), +} +use CliResponse::*; + +macro_rules! try_cli_resp { + ($res:expr) => { + match $res { + ReadItem(item) => item, + EndOfInput => return Ok(EndOfInput), + Interrupt => return Ok(Interrupt), + } + }; +} + +macro_rules! handle_yes_no { + ($rl:expr, $( $rest:tt )+) => { + match try_cli_resp!(read_prompt($rl, "Y/n: ")?).as_str() { + "y" | "Y" | "yes" | "" => { + $( $rest )* + } + _ => { + break + } + } + }; +} + +fn read_prompt(rl: &mut Editor<()>, prompt: &str) -> Result, String> { + Ok(match rl.readline(prompt) { + Ok(line) => ReadItem(line), + Err(ReadlineError::Interrupted) => Interrupt, + Err(ReadlineError::Eof) => EndOfInput, + Err(e) => return Err(e.to_string()), + }) +} + +fn read_new_ingredient(rl: &mut Editor<()>) -> Result>, String> { + let read_item = try_cli_resp!(read_prompt(rl, "> ")?); + Ok(if read_item.is_empty() { + ReadItem(None) + } else { + ReadItem(Some(Ingredient::parse(&read_item)?)) + }) +} + +fn read_ingredients(rl: &mut Editor<()>) -> Result>, String> { + println!("Enter Ingredients in the following form below: [(modifier)]"); + println!(" or enter an empty line to stop entering ingredients"); + let mut ingredient_list = Vec::new(); + loop { + match read_new_ingredient(rl)? { + Interrupt => break, + EndOfInput => return Ok(EndOfInput), + ReadItem(None) => break, + ReadItem(Some(ingredient)) => ingredient_list.push(ingredient), + } + } + Ok(ReadItem(ingredient_list)) +} + +fn read_new_step(rl: &mut Editor<()>) -> Result, String> { + println!("Enter Recipe Step details below"); + let instructions = try_cli_resp!(read_prompt(rl, "Step Instructions: ")?); + let ingredients = try_cli_resp!(read_ingredients(rl)?); + let mut step = Step::new(None, instructions); + step.add_ingredients(ingredients); + Ok(ReadItem(step)) +} + +fn read_steps(rl: &mut Editor<()>) -> Result>, String> { + let mut steps = Vec::new(); + loop { + println!("Enter a recipe step?"); + handle_yes_no! {rl, + let step = try_cli_resp!(read_new_step(rl)?); + steps.push(step); + }; + } + Ok(ReadItem(steps)) +} + +//pub fn read_new_recipe(rl: &mut Editor<()>) -> Result, String> { +// println!("Enter recipe details below."); +// let title = try_cli_resp!(read_prompt(rl, "Title: ")?); +// let desc = try_cli_resp!(read_prompt(rl, "Description: ")?); +// let steps = try_cli_resp!(read_steps(rl)?); +// let mut recipe = Recipe::new(title, desc); +// recipe.add_steps(steps); +// Ok(ReadItem(recipe)) +//} + +//fn read_loop(rl: &mut Editor<()>, store: S) -> Result, String> { +// loop { +// println!("Enter a recipe?"); +// handle_yes_no! {rl, +// let recipe = try_cli_resp!(read_new_recipe(rl)?); +// // TODO Store this recipe +// store.store_recipe(&recipe)?; +// }; +// } +// Ok(ReadItem(())) +//} + +//pub fn main_impl(factory: Factory) +//where +// Factory: TryInto, +// Err: std::fmt::Debug, +//{ +// let mut rl = Editor::<()>::new(); +// let store = factory.try_into().unwrap(); +// // TODO(jwall): handle history in a cross platform way? +// //read_loop(&mut rl, store).unwrap(); +//} diff --git a/kitchen/src/main.rs b/kitchen/src/main.rs index f68615a..07d8a7e 100644 --- a/kitchen/src/main.rs +++ b/kitchen/src/main.rs @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. mod api; +mod cli; fn main() { - println!("Hello, world!"); + //cli::main_impl(); } diff --git a/models/recipe.als b/models/recipe.als new file mode 100644 index 0000000..85d138e --- /dev/null +++ b/models/recipe.als @@ -0,0 +1,42 @@ +sig Recipe { + , desc: String + , title: String + , ingredients: set Ingredient +} + +sig Cat { + , name: String +} + +abstract sig Unit { } + +sig Tsp extends Unit {} +sig Tbsp extends Unit {} +sig Cup extends Unit {} +sig Pint extends Unit {} +sig Quart extends Unit {} +sig Gallon extends Unit {} + +abstract sig Wgt extends Unit { + +} +sig MilliGram extends Wgt {} +sig Gram extends Wgt {} +sig KiloGram extends Wgt {} + +abstract sig Amt {} +sig Count extends Amt { + , value: Int +} + +sig Frac extends Amt { + , numerator: Int + , denominator: Int +} + +sig Ingredient { + , category: Cat + , name: String + , unit: Unit + , amt: Amt +} \ No newline at end of file diff --git a/recipe-store/Cargo.toml b/recipe-store/Cargo.toml deleted file mode 100644 index ac328b8..0000000 --- a/recipe-store/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "recipe-store" -version = "0.1.0" -authors = ["Jeremy Wall "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -recipes = { path="../recipes" } -chrono = "~0.4" - -[dependencies.rusqlite] -version = "~0.25.0" -features = ["backup", "bundled", "session", "chrono", "uuid"] - -[dependencies.uuid] -version = "~0.8.2" -features = ["v4"] \ No newline at end of file diff --git a/recipe-store/src/lib.rs b/recipe-store/src/lib.rs deleted file mode 100644 index d21b54b..0000000 --- a/recipe-store/src/lib.rs +++ /dev/null @@ -1,386 +0,0 @@ -// Copyright 2021 Jeremy Wall -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -/// Storage backend for recipes. -use std::convert::From; -use std::path::Path; - -use chrono::NaiveDate; -use rusqlite::{params, Connection, Result as SqliteResult, Transaction}; -use uuid::Uuid; - -use recipes::{unit::Measure, Ingredient, Mealplan, Recipe, Step}; - -// TODO Model the error domain of our storage layer. -#[derive(Debug)] -pub struct StorageError { - message: String, -} - -impl From for StorageError { - fn from(e: rusqlite::Error) -> Self { - match e { - rusqlite::Error::SqliteFailure(e, msg) => StorageError { - message: format!("{}: {}", e, msg.unwrap_or_default()), - }, - rusqlite::Error::SqliteSingleThreadedMode => unimplemented!(), - rusqlite::Error::FromSqlConversionFailure(_, _, _) => unimplemented!(), - rusqlite::Error::IntegralValueOutOfRange(_, _) => todo!(), - rusqlite::Error::Utf8Error(_) => todo!(), - rusqlite::Error::NulError(_) => todo!(), - rusqlite::Error::InvalidParameterName(_) => todo!(), - rusqlite::Error::InvalidPath(_) => todo!(), - rusqlite::Error::ExecuteReturnedResults => todo!(), - rusqlite::Error::QueryReturnedNoRows => todo!(), - rusqlite::Error::InvalidColumnIndex(_) => todo!(), - rusqlite::Error::InvalidColumnName(_) => todo!(), - rusqlite::Error::InvalidColumnType(_, _, _) => todo!(), - rusqlite::Error::StatementChangedRows(_) => todo!(), - rusqlite::Error::ToSqlConversionFailure(_) => todo!(), - rusqlite::Error::InvalidQuery => todo!(), - rusqlite::Error::MultipleStatement => todo!(), - rusqlite::Error::InvalidParameterCount(_, _) => todo!(), - _ => todo!(), - } - } -} - -pub enum IterResult { - Some(Entity), - Err(StorageError), - None, -} - -pub trait RecipeStore { - fn store_mealplan(&self, plan: &Mealplan) -> Result<(), StorageError>; - fn fetch_mealplan(&self, mealplan_id: Uuid) -> Result; - fn fetch_mealplans_after_date(&self, date: NaiveDate) -> Result, StorageError>; - fn store_recipe(&self, e: &Recipe) -> Result<(), StorageError>; - fn fetch_all_recipes(&self) -> Result, StorageError>; - fn fetch_recipe_steps(&self, recipe_id: Uuid) -> Result>, StorageError>; - fn fetch_recipe_by_title(&self, k: &str) -> Result, StorageError>; - fn fetch_recipe_by_id(&self, k: Uuid) -> Result, StorageError>; - fn fetch_recipe_ingredients(&self, recipe_id: Uuid) -> Result, StorageError>; -} - -pub struct SqliteBackend { - conn: Connection, -} - -impl SqliteBackend { - pub fn new>(path: P) -> SqliteResult { - Ok(Self { - conn: Connection::open(path)?, - }) - } - - pub fn new_in_memory() -> SqliteResult { - Ok(Self { - conn: Connection::open_in_memory()?, - }) - } - - pub fn get_schema_version(&self) -> SqliteResult> { - let mut stmt = self - .conn - .prepare("SELECT max(version) from schema_version")?; - Ok(stmt.query_row([], |r| r.get(0))?) - } - - pub fn start_transaction<'a>(&'a mut self) -> SqliteResult> { - self.conn.transaction().map(|tx| TxHandle { tx }) - } - - pub fn create_schema(&self) -> SqliteResult<()> { - self.conn.execute( - "CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY )", - [], - )?; - let version = self.get_schema_version()?; - if let None = version { - self.conn - .execute("INSERT INTO schema_version ( version ) values ( 0 )", [])?; - } else { - return Ok(()); - } - - self.conn.execute( - "CREATE TABLE IF NOT EXISTS mealplans ( - id BLOB PRIMARY KEY, - start_date TEXT - )", - [], - )?; - self.conn.execute( - "CREATE TABLE IF NOT EXISTS recipes ( - id BLOB PRIMARY KEY, - title TEXT UNIQUE NOT NULL, - desc TEXT NOT NULL - )", - [], - )?; - self.conn.execute( - "CREATE TABLE IF NOT EXISTS mealplan_recipes ( - plan_id BLOB NOT NULL, - recipe_id BLOB NOT NULL, - recipe_idx INTEGER NOT NULL, - FOREIGN KEY(plan_id) REFERENCES mealplans(id) - FOREIGN KEY(recipe_id) REFERENCES recipes(id) - )", - [], - )?; - self.conn.execute( - "CREATE TABLE IF NOT EXISTS steps ( - recipe_id BLOB NOT NULL, - step_idx INTEGER NOT NULL, - prep_time INTEGER, -- in seconds - instructions TEXT NOT NULL, - FOREIGN KEY(recipe_id) REFERENCES recipes(id) - CONSTRAINT step_key PRIMARY KEY (recipe_id, step_idx) - )", - [], - )?; - self.conn.execute( - "CREATE TABLE IF NOT EXISTS step_ingredients ( - recipe_id BLOB NOT NULL, - step_idx INTEGER NOT NULL, - ingredient_idx INTEGER NOT NULL, - name TEXT NOT NULL, - amt TEXT NOT NULL, - category TEXT NOT NULL, - form TEXT, - FOREIGN KEY(recipe_id, step_idx) REFERENCES steps(recipe_id, step_idx), - CONSTRAINT step_ingredients_key PRIMARY KEY (recipe_id, step_idx, name, form) - )", - [], - )?; - Ok(()) - } -} - -pub struct TxHandle<'conn> { - tx: Transaction<'conn>, -} - -impl<'conn> TxHandle<'conn> { - pub fn serialize_step_stmt_rows( - mut stmt: rusqlite::Statement, - recipe_id: Uuid, - ) -> Result>, StorageError> { - if let Ok(step_iter) = stmt.query_map(params![recipe_id], |row| { - let prep_time: Option = row.get(2)?; - let instructions: String = row.get(3)?; - Ok(Step::new( - prep_time.map(|i| std::time::Duration::from_secs(i as u64)), - instructions, - )) - }) { - let mut steps = Vec::new(); - for step in step_iter { - steps.push(step?); - } - return Ok(Some(steps)); - } - return Ok(None); - } - - fn fill_recipe_steps(&self, recipe: &mut Recipe) -> Result<(), StorageError> { - let stmt = self - .tx - .prepare("SELECT * FROM steps WHERE recipe_id = ?1 ORDER BY step_idx")?; - let steps = Self::serialize_step_stmt_rows(stmt, recipe.id)?; - let mut stmt = self.tx.prepare("SELECT * from step_ingredients WHERE recipe_id = ?1 and step_idx = ?2 ORDER BY ingredient_idx")?; - if let Some(mut steps) = steps { - for (step_idx, mut step) in steps.drain(0..).enumerate() { - // TODO(jwall): Fetch the ingredients. - let ing_iter = stmt.query_map(params![recipe.id, step_idx], |row| { - Self::map_ingredient_row(row) - })?; - for ing in ing_iter { - step.ingredients.push(ing?); - } - recipe.add_step(step); - } - } - Ok(()) - } - - fn fill_plan_recipes(&self, plan: &mut Mealplan) -> Result<(), StorageError> { - let mut stmt = self - .tx - .prepare("SELECT recipe_id from mealplan_recipes where plan_id = ?1")?; - let id_iter = stmt.query_map(params![plan.id], |row| { - let id: Uuid = row.get(0)?; - Ok(id) - })?; - for id in id_iter { - // TODO(jwall): A potential optimzation here is to do this in a single - // select instead of a one at a time. - if let Some(recipe) = self.fetch_recipe_by_id(id?)? { - plan.recipes.push(recipe); - } - } - Ok(()) - } - - fn map_recipe_row(r: &rusqlite::Row) -> Result { - let id: Uuid = r.get(0)?; - let title: String = r.get(1)?; - let desc: String = r.get(2)?; - Ok(Recipe::new_with_id(id, title, desc)) - } - - fn map_ingredient_row(r: &rusqlite::Row) -> Result { - let name: String = r.get(3)?; - let amt: String = r.get(4)?; - let category = r.get(5)?; - let form = r.get(6)?; - Ok(Ingredient::new( - name, - form, - dbg!(Measure::parse(dbg!(&amt)).unwrap()), - category, - )) - } -} - -impl<'conn> RecipeStore for TxHandle<'conn> { - fn fetch_all_recipes(&self) -> Result, StorageError> { - let mut stmt = self.tx.prepare("SELECT * FROM recipes")?; - let recipe_iter = stmt.query_map([], |r| Self::map_recipe_row(r))?; - let mut recipes = Vec::new(); - for next in recipe_iter { - recipes.push(next?); - } - Ok(recipes) - } - - fn store_recipe(&self, recipe: &Recipe) -> Result<(), StorageError> { - // If we don't have a transaction already we should start one. - self.tx.execute( - "INSERT OR REPLACE INTO recipes (id, title, desc) VALUES (?1, ?2, ?3)", - params![recipe.id, recipe.title, recipe.desc], - )?; - for (idx, step) in recipe.steps.iter().enumerate() { - self.tx.execute("INSERT INTO steps (recipe_id, step_idx, prep_time, instructions) VALUES (?1, ?2, ?3, ?4)", - params![recipe.id, dbg!(idx), step.prep_time.map(|v| v.as_secs()) , step.instructions])?; - for (ing_idx, ing) in step.ingredients.iter().enumerate() { - dbg!(self.tx.execute( - "INSERT INTO step_ingredients (recipe_id, step_idx, ingredient_idx, name, amt, category, form) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![recipe.id, dbg!(idx), ing_idx, ing.name, format!("{}", ing.amt), ing.category, ing.form])?); - } - } - Ok(()) - } - - fn store_mealplan(&self, plan: &Mealplan) -> Result<(), StorageError> { - self.tx.execute( - "INSERT OR REPLACE INTO mealplans (id, start_date) VALUES (?1, ?2)", - params![plan.id, plan.start_date], - )?; - for (idx, recipe) in plan.recipes.iter().enumerate() { - self.tx.execute( - "INSERT INTO mealplan_recipes (plan_id, recipe_id, recipe_idx) VALUES (?1, ?2, ?3)", - params![plan.id, recipe.id, idx], - )?; - } - Ok(()) - } - - fn fetch_mealplan(&self, plan_id: Uuid) -> Result { - let mut stmt = self.tx.prepare("SELECT * FROM mealplans WHERE id = ?1")?; - let mut plan = stmt.query_row(params![plan_id], |row| { - let id = row.get(0)?; - let plan = Mealplan::new_id(id); - if let Some(start_date) = dbg!(row.get(1)?) { - Ok(plan.with_start_date(start_date)) - } else { - Ok(plan) - } - })?; - self.fill_plan_recipes(&mut plan)?; - Ok(plan) - } - - fn fetch_mealplans_after_date(&self, date: NaiveDate) -> Result, StorageError> { - let mut stmt = self - .tx - .prepare("SELECT * FROM mealplans WHERE start_date >= ?1 ORDER BY start_date DESC")?; - let plan_iter = stmt.query_map(params![date], |row| { - let id = row.get(0)?; - let plan = Mealplan::new_id(id); - if let Some(start_date) = dbg!(row.get(1)?) { - Ok(plan.with_start_date(start_date)) - } else { - Ok(plan) - } - })?; - let mut plans = Vec::new(); - for plan in plan_iter { - let mut plan = plan?; - self.fill_plan_recipes(&mut plan)?; - plans.push(plan); - } - Ok(plans) - } - - fn fetch_recipe_steps(&self, recipe_id: Uuid) -> Result>, StorageError> { - let stmt = self - .tx - .prepare("SELECT * from steps WHERE recipe_id = ?1 ORDER BY step_idx")?; - Self::serialize_step_stmt_rows(stmt, recipe_id) - } - - fn fetch_recipe_by_id(&self, key: Uuid) -> Result, StorageError> { - let mut stmt = self.tx.prepare("SELECT * FROM recipes WHERE id = ?1")?; - let recipe_iter = stmt.query_map(params![key], |r| Self::map_recipe_row(r))?; - let mut recipe = recipe_iter - .filter(|res| res.is_ok()) // TODO(jwall): What about failures here? - .map(|r| r.unwrap()) - .next(); - // TODO(jwall): abstract this so it's shared between methods. - if let Some(recipe) = recipe.as_mut() { - self.fill_recipe_steps(recipe)?; - } - return Ok(recipe); - } - - fn fetch_recipe_by_title(&self, key: &str) -> Result, StorageError> { - let mut stmt = self.tx.prepare("SELECT * FROM recipes WHERE title = ?1")?; - let recipe_iter = stmt.query_map(params![key], |r| Self::map_recipe_row(r))?; - let mut recipe = recipe_iter - .filter(|res| res.is_ok()) // TODO(jwall): What about failures here? - .map(|r| r.unwrap()) - .next(); - // TODO(jwall): abstract this so it's shared between methods. - if let Some(recipe) = recipe.as_mut() { - self.fill_recipe_steps(recipe)?; - } - return Ok(recipe); - } - - fn fetch_recipe_ingredients(&self, recipe_id: Uuid) -> Result, StorageError> { - let mut stmt = self.tx.prepare( - "SELECT * FROM step_ingredients WHERE recipe_id = ?1 ORDER BY step_idx, ingredient_idx", - )?; - let ing_iter = stmt.query_map(params![recipe_id], |row| Self::map_ingredient_row(row))?; - let mut ingredients = Vec::new(); - for i in ing_iter { - ingredients.push(i?); - } - Ok(ingredients) - } -} - -#[cfg(test)] -mod tests; diff --git a/recipe-store/src/tests.rs b/recipe-store/src/tests.rs deleted file mode 100644 index 8e3b14d..0000000 --- a/recipe-store/src/tests.rs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2021 Jeremy Wall -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -use crate::*; -use std::convert::Into; - -macro_rules! init_sqlite_store { - () => {{ - let in_memory = - SqliteBackend::new_in_memory().expect("We expect in memory connections to succeed"); - in_memory - .create_schema() - .expect("We expect the schema creation to succeed"); - let version = in_memory - .get_schema_version() - .expect("We expect the version fetch to succeed"); - assert!(version.is_some()); - assert_eq!(version.unwrap(), 0); - in_memory - }}; -} - -#[test] -fn test_schema_creation() { - let in_memory = init_sqlite_store!(); - - in_memory - .create_schema() - .expect("create_schema is idempotent"); - let version = in_memory - .get_schema_version() - .expect("We expect the version fetch to succeed"); - assert!(version.is_some()); - assert_eq!(version.unwrap(), 0); -} - -#[test] -fn test_recipe_store_update_roundtrip_full() { - let mut in_memory = init_sqlite_store!(); - let tx = in_memory.start_transaction().unwrap(); - let mut recipe = Recipe::new("my recipe", "my description"); - let mut step1 = Step::new( - Some(std::time::Duration::from_secs(60 * 30)), - "mix thoroughly", - ); - step1.add_ingredients(vec![ - Ingredient::new("flour", None, Measure::cup(1.into()), "dry goods"), - Ingredient::new( - "salt", - Some("Ground".to_owned()), - Measure::tsp(1.into()), - "seasoning", - ), - ]); - let step2 = Step::new(None, "combine ingredients"); - recipe.add_steps(vec![step1.clone(), step2.clone()]); - - tx.store_recipe(&mut recipe) - .expect("We expect the recpe to store successfully"); - - let recipes: Vec = tx - .fetch_all_recipes() - .expect("We expect to get recipes back out"); - assert_eq!(recipes.len(), 1); - let recipe: Option = tx - .fetch_recipe_by_title("my recipe") - .expect("We expect the recipe to come back out"); - assert!(recipe.is_some()); - let recipe = recipe.unwrap(); - assert_eq!(recipe.title, "my recipe"); - assert_eq!(recipe.desc, "my description"); - assert_eq!(recipe.steps.len(), 2); - let step1_got = &recipe.steps[0]; - let step2_got = &recipe.steps[1]; - assert_eq!(step1_got.prep_time, step1.prep_time); - assert_eq!(step1_got.instructions, step1.instructions); - assert_eq!(step1_got.ingredients.len(), step1.ingredients.len()); - assert_eq!(step1_got.ingredients[0], step1.ingredients[0]); - assert_eq!(step1_got.ingredients[1], step1.ingredients[1]); - assert_eq!(step2_got.prep_time, step2.prep_time); - assert_eq!(step2_got.instructions, step2.instructions); - assert_eq!(step2_got.ingredients.len(), step2.ingredients.len()); -} - -#[test] -fn test_fetch_recipe_ingredients() { - let mut in_memory = init_sqlite_store!(); - let tx = in_memory.start_transaction().unwrap(); - let mut recipe = Recipe::new("my recipe", "my description"); - let mut step1 = Step::new( - Some(std::time::Duration::from_secs(60 * 30)), - "mix thoroughly", - ); - step1.add_ingredients(vec![ - Ingredient::new("flour", None, Measure::cup(1.into()), "dry goods"), - Ingredient::new( - "salt", - Some("Ground".to_owned()), - Measure::tsp(1.into()), - "seasoning", - ), - ]); - let step2 = Step::new(None, "combine ingredients"); - recipe.add_steps(vec![step1.clone(), step2.clone()]); - - tx.store_recipe(&mut recipe) - .expect("We expect the recpe to store successfully"); - - let ingredients = tx - .fetch_recipe_ingredients(recipe.id) - .expect("We expect to fetch ingredients for the recipe"); - assert_eq!(ingredients.len(), 2); - assert_eq!(ingredients, step1.ingredients); -} diff --git a/recipes/Cargo.toml b/recipes/Cargo.toml index ac1e4cc..b190790 100644 --- a/recipes/Cargo.toml +++ b/recipes/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -abortable_parser = "~0.2.5" +abortable_parser = "~0.2.6" chrono = "~0.4" [dependencies.uuid] diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index 7167a37..91a9214 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -61,29 +61,37 @@ impl Mealplan { pub struct Recipe { pub id: uuid::Uuid, pub title: String, - pub desc: String, + pub desc: Option, pub steps: Vec, } impl Recipe { - pub fn new>(title: S, desc: S) -> Self { + pub fn new>(title: S, desc: Option) -> Self { Self { id: uuid::Uuid::new_v4(), title: title.into(), - desc: desc.into(), + desc: desc.map(|s| s.into()), steps: Vec::new(), } } - pub fn new_with_id>(id: uuid::Uuid, title: S, desc: S) -> Self { + pub fn new_with_id>(id: uuid::Uuid, title: S, desc: Option) -> Self { Self { id: id, title: title.into(), - desc: desc.into(), + desc: desc.map(|s| s.into()), steps: Vec::new(), } } + pub fn with_steps(mut self, steps: Iter) -> Self + where + Iter: IntoIterator, + { + self.add_steps(steps); + self + } + /// Add steps to the end of the recipe. pub fn add_steps(&mut self, steps: Iter) where