From 5a7ea055221cdc10f03a70af3c24bdd2794c6410 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 4 May 2021 18:46:46 -0400 Subject: [PATCH] Start building out the command line API for this --- Cargo.lock | 64 +++++++++++ Cargo.toml | 2 +- kitchen/Cargo.toml | 11 ++ kitchen/src/api.rs | 31 ++++++ kitchen/src/main.rs | 18 +++ recipe-store/Cargo.toml | 7 +- recipe-store/src/lib.rs | 228 +++++++++++++++++++++++++++----------- recipe-store/src/tests.rs | 20 ++-- recipes/Cargo.toml | 5 + recipes/src/lib.rs | 58 ++++++++-- 10 files changed, 361 insertions(+), 83 deletions(-) create mode 100644 kitchen/Cargo.toml create mode 100644 kitchen/src/api.rs create mode 100644 kitchen/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 51151de..0dc0b6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + [[package]] name = "clang-sys" version = "1.2.0" @@ -87,6 +100,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "glob" version = "0.3.0" @@ -111,6 +135,14 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "kitchen" +version = "0.1.0" +dependencies = [ + "recipe-store", + "recipes", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -243,8 +275,10 @@ dependencies = [ name = "recipe-store" version = "0.1.0" dependencies = [ + "chrono", "recipes", "rusqlite", + "uuid", ] [[package]] @@ -252,7 +286,9 @@ name = "recipes" version = "0.1.0" dependencies = [ "abortable_parser", + "chrono", "num-rational", + "uuid", ] [[package]] @@ -277,12 +313,14 @@ 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]] @@ -303,12 +341,32 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.12" @@ -321,6 +379,12 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 738925d..bb126c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = [ "recipes", "recipe-store" ] \ No newline at end of file +members = [ "recipes", "recipe-store", "kitchen" ] \ No newline at end of file diff --git a/kitchen/Cargo.toml b/kitchen/Cargo.toml new file mode 100644 index 0000000..998fa55 --- /dev/null +++ b/kitchen/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "kitchen" +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" } +recipe-store = {path = "../recipe-store" } diff --git a/kitchen/src/api.rs b/kitchen/src/api.rs new file mode 100644 index 0000000..7e19cd2 --- /dev/null +++ b/kitchen/src/api.rs @@ -0,0 +1,31 @@ +// 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::From; + +use recipe_store::{RecipeStore, SqliteBackend}; +use recipes::*; + +pub struct Api { + store: SqliteBackend, +} + +impl Api { + // mealplan +} + +impl From for Api { + fn from(store: SqliteBackend) -> Self { + Api { store } + } +} diff --git a/kitchen/src/main.rs b/kitchen/src/main.rs new file mode 100644 index 0000000..f68615a --- /dev/null +++ b/kitchen/src/main.rs @@ -0,0 +1,18 @@ +// 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. +mod api; + +fn main() { + println!("Hello, world!"); +} diff --git a/recipe-store/Cargo.toml b/recipe-store/Cargo.toml index 19c84a1..ac328b8 100644 --- a/recipe-store/Cargo.toml +++ b/recipe-store/Cargo.toml @@ -8,7 +8,12 @@ edition = "2018" [dependencies] recipes = { path="../recipes" } +chrono = "~0.4" [dependencies.rusqlite] version = "~0.25.0" -features = ["backup", "bundled", "session"] \ No newline at end of file +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 index 00cb372..1e30778 100644 --- a/recipe-store/src/lib.rs +++ b/recipe-store/src/lib.rs @@ -15,9 +15,11 @@ use std::convert::From; use std::path::Path; -use rusqlite::{params, Connection, DropBehavior, Result as SqliteResult, Transaction}; +use chrono::NaiveDate; +use rusqlite::{params, Connection, Result as SqliteResult, Transaction}; +use uuid::Uuid; -use recipes::{unit::Measure, Ingredient, Recipe, Step}; +use recipes::{unit::Measure, Ingredient, Mealplan, Recipe, Step}; // TODO Model the error domain of our storage layer. #[derive(Debug)] @@ -60,11 +62,15 @@ pub enum IterResult { } pub trait RecipeStore { - fn store_recipe(&mut self, e: &mut Recipe) -> Result<(), StorageError>; + 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: i64) -> Result>, StorageError>; - fn fetch_recipe(&mut self, k: &str) -> Result, StorageError>; - fn fetch_recipe_ingredients(&self, recipe_id: i64) -> 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 { @@ -91,11 +97,8 @@ impl SqliteBackend { Ok(stmt.query_row([], |r| r.get(0))?) } - pub fn start_transaction<'a>(&'a mut self) -> SqliteResult> { - self.conn.transaction().map(|mut tx| { - tx.set_drop_behavior(DropBehavior::Commit); - tx - }) + pub fn start_transaction<'a>(&'a mut self) -> SqliteResult> { + self.conn.transaction().map(|tx| TxHandle { tx }) } pub fn create_schema(&self) -> SqliteResult<()> { @@ -111,17 +114,34 @@ impl SqliteBackend { 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 INTEGER PRIMARY KEY, + 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 INTEGER NOT NULL, + recipe_id BLOB NOT NULL, step_idx INTEGER NOT NULL, prep_time INTEGER, -- in seconds instructions TEXT NOT NULL, @@ -132,7 +152,7 @@ impl SqliteBackend { )?; self.conn.execute( "CREATE TABLE IF NOT EXISTS step_ingredients ( - recipe_id INTEGER NOT NULL, + recipe_id BLOB NOT NULL, step_idx INTEGER NOT NULL, ingredient_idx INTEGER NOT NULL, name TEXT NOT NULL, @@ -146,10 +166,16 @@ impl SqliteBackend { )?; Ok(()) } +} +pub struct TxHandle<'conn> { + tx: Transaction<'conn>, +} + +impl<'conn> TxHandle<'conn> { pub fn serialize_step_stmt_rows( mut stmt: rusqlite::Statement, - recipe_id: i64, + recipe_id: Uuid, ) -> Result>, StorageError> { if let Ok(step_iter) = stmt.query_map(params![recipe_id], |row| { let prep_time: Option = row.get(2)?; @@ -168,8 +194,47 @@ impl SqliteBackend { 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: i64 = r.get(0)?; + let id: Uuid = r.get(0)?; let title: String = r.get(1)?; let desc: String = r.get(2)?; Ok(Recipe::new_id(id, title, desc)) @@ -189,9 +254,9 @@ impl SqliteBackend { } } -impl RecipeStore for SqliteBackend { +impl<'conn> RecipeStore for TxHandle<'conn> { fn fetch_all_recipes(&self) -> Result, StorageError> { - let mut stmt = self.conn.prepare("SELECT * FROM recipes")?; + 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 { @@ -200,28 +265,17 @@ impl RecipeStore for SqliteBackend { Ok(recipes) } - fn store_recipe(&mut self, recipe: &mut Recipe) -> Result<(), StorageError> { + fn store_recipe(&self, recipe: &Recipe) -> Result<(), StorageError> { // If we don't have a transaction already we should start one. - let tx = self.start_transaction()?; - if let Some(id) = recipe.id { - tx.execute( - "INSERT OR REPLACE INTO recipes (id, title, desc) VALUES (?1, ?2, ?3)", - params![id, recipe.title, recipe.desc], - )?; - } else { - tx.execute( - "INSERT INTO recipes (title, desc) VALUES (?1, ?2)", - params![recipe.title, recipe.desc], - )?; - let mut stmt = tx.prepare("select id from recipes where title = ?1")?; - let id = stmt.query_row(params![recipe.title], |row| Ok(row.get(0)?))?; - recipe.id = Some(id); - } + 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() { - tx.execute("INSERT INTO steps (recipe_id, step_idx, prep_time, instructions) VALUES (?1, ?2, ?3, ?4)", + 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!(tx.execute( + 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])?); } @@ -229,44 +283,94 @@ impl RecipeStore for SqliteBackend { Ok(()) } - fn fetch_recipe_steps(&self, recipe_id: i64) -> Result>, StorageError> { - let stmt = self - .conn - .prepare("SELECT * from steps WHERE recipe_id = ?1 ORDER BY step_idx")?; - SqliteBackend::serialize_step_stmt_rows(stmt, recipe_id) + 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_recipe(&mut self, key: &str) -> Result, StorageError> { - let tx = self.start_transaction()?; - let mut stmt = tx.prepare("SELECT * FROM recipes WHERE title = ?1")?; + 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() { - // We know the recipe.id has to exist since we filled it when it came from the database. - let stmt = tx.prepare("SELECT * FROM steps WHERE recipe_id = ?1 ORDER BY step_idx")?; - let steps = SqliteBackend::serialize_step_stmt_rows(stmt, recipe.id.unwrap())?; - let mut stmt = 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); - } - } + self.fill_recipe_steps(recipe)?; } return Ok(recipe); } - fn fetch_recipe_ingredients(&self, recipe_id: i64) -> Result, StorageError> { - let mut stmt = self.conn.prepare( + 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))?; diff --git a/recipe-store/src/tests.rs b/recipe-store/src/tests.rs index 250e669..8e3b14d 100644 --- a/recipe-store/src/tests.rs +++ b/recipe-store/src/tests.rs @@ -47,7 +47,7 @@ fn test_schema_creation() { #[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)), @@ -65,17 +65,15 @@ fn test_recipe_store_update_roundtrip_full() { let step2 = Step::new(None, "combine ingredients"); recipe.add_steps(vec![step1.clone(), step2.clone()]); - in_memory - .store_recipe(&mut recipe) + tx.store_recipe(&mut recipe) .expect("We expect the recpe to store successfully"); - assert!(recipe.id.is_some()); - let recipes: Vec = in_memory + let recipes: Vec = tx .fetch_all_recipes() .expect("We expect to get recipes back out"); assert_eq!(recipes.len(), 1); - let recipe: Option = in_memory - .fetch_recipe("my recipe") + 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(); @@ -97,6 +95,7 @@ fn test_recipe_store_update_roundtrip_full() { #[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)), @@ -114,12 +113,11 @@ fn test_fetch_recipe_ingredients() { let step2 = Step::new(None, "combine ingredients"); recipe.add_steps(vec![step1.clone(), step2.clone()]); - in_memory - .store_recipe(&mut recipe) + tx.store_recipe(&mut recipe) .expect("We expect the recpe to store successfully"); - let ingredients = in_memory - .fetch_recipe_ingredients(recipe.id.unwrap()) + 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 deb164d..c809120 100644 --- a/recipes/Cargo.toml +++ b/recipes/Cargo.toml @@ -8,6 +8,11 @@ edition = "2018" [dependencies] abortable_parser = "~0.2.4" +chrono = "~0.4" + +[dependencies.uuid] +version = "~0.8.2" +features = ["v4"] [dependencies.num-rational] version = "~0.4.0" \ No newline at end of file diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index afd25f6..501ef2f 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -15,12 +15,48 @@ pub mod unit; use std::collections::BTreeMap; +use chrono::NaiveDate; +use uuid::{self, Uuid}; + use unit::*; +#[derive(Debug, Clone, PartialEq)] +pub struct Mealplan { + pub id: uuid::Uuid, + pub start_date: Option, + pub recipes: Vec, +} + +impl Mealplan { + pub fn new() -> Self { + Self::new_id(uuid::Uuid::new_v4()) + } + + pub fn new_id(id: Uuid) -> Self { + Self { + id: id, + start_date: None, + recipes: Vec::new(), + } + } + + pub fn with_start_date(mut self, start_date: NaiveDate) -> Self { + self.start_date = Some(start_date); + self + } + + pub fn add_recipes(&mut self, recipes: Iter) + where + Iter: IntoIterator, + { + self.recipes.extend(recipes.into_iter()) + } +} + /// A Recipe with a title, description, and a series of steps. #[derive(Debug, Clone, PartialEq)] pub struct Recipe { - pub id: Option, + pub id: uuid::Uuid, // TODO(jwall): use uuid instead? pub title: String, pub desc: String, pub steps: Vec, @@ -29,16 +65,16 @@ pub struct Recipe { impl Recipe { pub fn new>(title: S, desc: S) -> Self { Self { - id: None, + id: uuid::Uuid::new_v4(), title: title.into(), desc: desc.into(), steps: Vec::new(), } } - pub fn new_id>(id: i64, title: S, desc: S) -> Self { + pub fn new_id>(id: uuid::Uuid, title: S, desc: S) -> Self { Self { - id: Some(id), + id: id, title: title.into(), desc: desc.into(), steps: Vec::new(), @@ -46,7 +82,10 @@ impl Recipe { } /// Add steps to the end of the recipe. - pub fn add_steps(&mut self, steps: Vec) { + pub fn add_steps(&mut self, steps: Iter) + where + Iter: IntoIterator, + { self.steps.extend(steps.into_iter()); } @@ -122,8 +161,11 @@ impl Step { } } - pub fn add_ingredients(&mut self, mut ingredients: Vec) { - self.ingredients.append(&mut ingredients); + pub fn add_ingredients(&mut self, ingredients: Iter) + where + Iter: IntoIterator, + { + self.ingredients.extend(ingredients.into_iter()); } pub fn add_ingredient(&mut self, ingredient: Ingredient) { @@ -140,7 +182,7 @@ pub struct IngredientKey(String, Option, String); /// uniquely identify an ingredient. #[derive(Debug, Clone, PartialEq)] pub struct Ingredient { - pub id: Option, + pub id: Option, // TODO(jwall): use uuid instead? pub name: String, pub form: Option, pub amt: Measure,