Start building out the command line API for this

This commit is contained in:
Jeremy Wall 2021-05-04 18:46:46 -04:00
parent 32bb68b911
commit 5a7ea05522
10 changed files with 361 additions and 83 deletions

64
Cargo.lock generated
View File

@ -64,6 +64,19 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "clang-sys" name = "clang-sys"
version = "1.2.0" version = "1.2.0"
@ -87,6 +100,17 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 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]] [[package]]
name = "glob" name = "glob"
version = "0.3.0" version = "0.3.0"
@ -111,6 +135,14 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "kitchen"
version = "0.1.0"
dependencies = [
"recipe-store",
"recipes",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -243,8 +275,10 @@ dependencies = [
name = "recipe-store" name = "recipe-store"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"recipes", "recipes",
"rusqlite", "rusqlite",
"uuid",
] ]
[[package]] [[package]]
@ -252,7 +286,9 @@ name = "recipes"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"abortable_parser", "abortable_parser",
"chrono",
"num-rational", "num-rational",
"uuid",
] ]
[[package]] [[package]]
@ -277,12 +313,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc783b7ddae608338003bac1fa00b6786a75a9675fbd8e87243ecfdea3f6ed2" checksum = "fbc783b7ddae608338003bac1fa00b6786a75a9675fbd8e87243ecfdea3f6ed2"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"chrono",
"fallible-iterator", "fallible-iterator",
"fallible-streaming-iterator", "fallible-streaming-iterator",
"hashlink", "hashlink",
"libsqlite3-sys", "libsqlite3-sys",
"memchr", "memchr",
"smallvec", "smallvec",
"uuid",
] ]
[[package]] [[package]]
@ -303,12 +341,32 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 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]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.12" version = "0.2.12"
@ -321,6 +379,12 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@ -1,2 +1,2 @@
[workspace] [workspace]
members = [ "recipes", "recipe-store" ] members = [ "recipes", "recipe-store", "kitchen" ]

11
kitchen/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "kitchen"
version = "0.1.0"
authors = ["Jeremy Wall <jeremy@marzhillstudios.com>"]
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" }

31
kitchen/src/api.rs Normal file
View File

@ -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<SqliteBackend> for Api {
fn from(store: SqliteBackend) -> Self {
Api { store }
}
}

18
kitchen/src/main.rs Normal file
View File

@ -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!");
}

View File

@ -8,7 +8,12 @@ edition = "2018"
[dependencies] [dependencies]
recipes = { path="../recipes" } recipes = { path="../recipes" }
chrono = "~0.4"
[dependencies.rusqlite] [dependencies.rusqlite]
version = "~0.25.0" version = "~0.25.0"
features = ["backup", "bundled", "session"] features = ["backup", "bundled", "session", "chrono", "uuid"]
[dependencies.uuid]
version = "~0.8.2"
features = ["v4"]

View File

@ -15,9 +15,11 @@
use std::convert::From; use std::convert::From;
use std::path::Path; 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. // TODO Model the error domain of our storage layer.
#[derive(Debug)] #[derive(Debug)]
@ -60,11 +62,15 @@ pub enum IterResult<Entity> {
} }
pub trait RecipeStore { 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<Mealplan, StorageError>;
fn fetch_mealplans_after_date(&self, date: NaiveDate) -> Result<Vec<Mealplan>, StorageError>;
fn store_recipe(&self, e: &Recipe) -> Result<(), StorageError>;
fn fetch_all_recipes(&self) -> Result<Vec<Recipe>, StorageError>; fn fetch_all_recipes(&self) -> Result<Vec<Recipe>, StorageError>;
fn fetch_recipe_steps(&self, recipe_id: i64) -> Result<Option<Vec<Step>>, StorageError>; fn fetch_recipe_steps(&self, recipe_id: Uuid) -> Result<Option<Vec<Step>>, StorageError>;
fn fetch_recipe(&mut self, k: &str) -> Result<Option<Recipe>, StorageError>; fn fetch_recipe_by_title(&self, k: &str) -> Result<Option<Recipe>, StorageError>;
fn fetch_recipe_ingredients(&self, recipe_id: i64) -> Result<Vec<Ingredient>, StorageError>; fn fetch_recipe_by_id(&self, k: Uuid) -> Result<Option<Recipe>, StorageError>;
fn fetch_recipe_ingredients(&self, recipe_id: Uuid) -> Result<Vec<Ingredient>, StorageError>;
} }
pub struct SqliteBackend { pub struct SqliteBackend {
@ -91,11 +97,8 @@ impl SqliteBackend {
Ok(stmt.query_row([], |r| r.get(0))?) Ok(stmt.query_row([], |r| r.get(0))?)
} }
pub fn start_transaction<'a>(&'a mut self) -> SqliteResult<Transaction<'a>> { pub fn start_transaction<'a>(&'a mut self) -> SqliteResult<TxHandle<'a>> {
self.conn.transaction().map(|mut tx| { self.conn.transaction().map(|tx| TxHandle { tx })
tx.set_drop_behavior(DropBehavior::Commit);
tx
})
} }
pub fn create_schema(&self) -> SqliteResult<()> { pub fn create_schema(&self) -> SqliteResult<()> {
@ -111,17 +114,34 @@ impl SqliteBackend {
return Ok(()); return Ok(());
} }
self.conn.execute(
"CREATE TABLE IF NOT EXISTS mealplans (
id BLOB PRIMARY KEY,
start_date TEXT
)",
[],
)?;
self.conn.execute( self.conn.execute(
"CREATE TABLE IF NOT EXISTS recipes ( "CREATE TABLE IF NOT EXISTS recipes (
id INTEGER PRIMARY KEY, id BLOB PRIMARY KEY,
title TEXT UNIQUE NOT NULL, title TEXT UNIQUE NOT NULL,
desc TEXT 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( self.conn.execute(
"CREATE TABLE IF NOT EXISTS steps ( "CREATE TABLE IF NOT EXISTS steps (
recipe_id INTEGER NOT NULL, recipe_id BLOB NOT NULL,
step_idx INTEGER NOT NULL, step_idx INTEGER NOT NULL,
prep_time INTEGER, -- in seconds prep_time INTEGER, -- in seconds
instructions TEXT NOT NULL, instructions TEXT NOT NULL,
@ -132,7 +152,7 @@ impl SqliteBackend {
)?; )?;
self.conn.execute( self.conn.execute(
"CREATE TABLE IF NOT EXISTS step_ingredients ( "CREATE TABLE IF NOT EXISTS step_ingredients (
recipe_id INTEGER NOT NULL, recipe_id BLOB NOT NULL,
step_idx INTEGER NOT NULL, step_idx INTEGER NOT NULL,
ingredient_idx INTEGER NOT NULL, ingredient_idx INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
@ -146,10 +166,16 @@ impl SqliteBackend {
)?; )?;
Ok(()) Ok(())
} }
}
pub struct TxHandle<'conn> {
tx: Transaction<'conn>,
}
impl<'conn> TxHandle<'conn> {
pub fn serialize_step_stmt_rows( pub fn serialize_step_stmt_rows(
mut stmt: rusqlite::Statement, mut stmt: rusqlite::Statement,
recipe_id: i64, recipe_id: Uuid,
) -> Result<Option<Vec<Step>>, StorageError> { ) -> Result<Option<Vec<Step>>, StorageError> {
if let Ok(step_iter) = stmt.query_map(params![recipe_id], |row| { if let Ok(step_iter) = stmt.query_map(params![recipe_id], |row| {
let prep_time: Option<i64> = row.get(2)?; let prep_time: Option<i64> = row.get(2)?;
@ -168,8 +194,47 @@ impl SqliteBackend {
return Ok(None); 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<Recipe, rusqlite::Error> { fn map_recipe_row(r: &rusqlite::Row) -> Result<Recipe, rusqlite::Error> {
let id: i64 = r.get(0)?; let id: Uuid = r.get(0)?;
let title: String = r.get(1)?; let title: String = r.get(1)?;
let desc: String = r.get(2)?; let desc: String = r.get(2)?;
Ok(Recipe::new_id(id, title, desc)) 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<Vec<Recipe>, StorageError> { fn fetch_all_recipes(&self) -> Result<Vec<Recipe>, 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 recipe_iter = stmt.query_map([], |r| Self::map_recipe_row(r))?;
let mut recipes = Vec::new(); let mut recipes = Vec::new();
for next in recipe_iter { for next in recipe_iter {
@ -200,28 +265,17 @@ impl RecipeStore for SqliteBackend {
Ok(recipes) 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. // If we don't have a transaction already we should start one.
let tx = self.start_transaction()?; self.tx.execute(
if let Some(id) = recipe.id { "INSERT OR REPLACE INTO recipes (id, title, desc) VALUES (?1, ?2, ?3)",
tx.execute( params![recipe.id, recipe.title, recipe.desc],
"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);
}
for (idx, step) in recipe.steps.iter().enumerate() { 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])?; params![recipe.id, dbg!(idx), step.prep_time.map(|v| v.as_secs()) , step.instructions])?;
for (ing_idx, ing) in step.ingredients.iter().enumerate() { 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)", "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])?); 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(()) Ok(())
} }
fn fetch_recipe_steps(&self, recipe_id: i64) -> Result<Option<Vec<Step>>, StorageError> { fn store_mealplan(&self, plan: &Mealplan) -> Result<(), StorageError> {
let stmt = self self.tx.execute(
.conn "INSERT OR REPLACE INTO mealplans (id, start_date) VALUES (?1, ?2)",
.prepare("SELECT * from steps WHERE recipe_id = ?1 ORDER BY step_idx")?; params![plan.id, plan.start_date],
SqliteBackend::serialize_step_stmt_rows(stmt, recipe_id) )?;
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<Option<Recipe>, StorageError> { fn fetch_mealplan(&self, plan_id: Uuid) -> Result<Mealplan, StorageError> {
let tx = self.start_transaction()?; let mut stmt = self.tx.prepare("SELECT * FROM mealplans WHERE id = ?1")?;
let mut stmt = tx.prepare("SELECT * FROM recipes WHERE title = ?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<Vec<Mealplan>, 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<Option<Vec<Step>>, 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<Option<Recipe>, 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 recipe_iter = stmt.query_map(params![key], |r| Self::map_recipe_row(r))?;
let mut recipe = recipe_iter let mut recipe = recipe_iter
.filter(|res| res.is_ok()) // TODO(jwall): What about failures here? .filter(|res| res.is_ok()) // TODO(jwall): What about failures here?
.map(|r| r.unwrap()) .map(|r| r.unwrap())
.next(); .next();
// TODO(jwall): abstract this so it's shared between methods.
if let Some(recipe) = recipe.as_mut() { 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. self.fill_recipe_steps(recipe)?;
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);
}
}
} }
return Ok(recipe); return Ok(recipe);
} }
fn fetch_recipe_ingredients(&self, recipe_id: i64) -> Result<Vec<Ingredient>, StorageError> { fn fetch_recipe_by_title(&self, key: &str) -> Result<Option<Recipe>, StorageError> {
let mut stmt = self.conn.prepare( 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<Vec<Ingredient>, StorageError> {
let mut stmt = self.tx.prepare(
"SELECT * FROM step_ingredients WHERE recipe_id = ?1 ORDER BY step_idx, ingredient_idx", "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 ing_iter = stmt.query_map(params![recipe_id], |row| Self::map_ingredient_row(row))?;

View File

@ -47,7 +47,7 @@ fn test_schema_creation() {
#[test] #[test]
fn test_recipe_store_update_roundtrip_full() { fn test_recipe_store_update_roundtrip_full() {
let mut in_memory = init_sqlite_store!(); 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 recipe = Recipe::new("my recipe", "my description");
let mut step1 = Step::new( let mut step1 = Step::new(
Some(std::time::Duration::from_secs(60 * 30)), 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"); let step2 = Step::new(None, "combine ingredients");
recipe.add_steps(vec![step1.clone(), step2.clone()]); recipe.add_steps(vec![step1.clone(), step2.clone()]);
in_memory tx.store_recipe(&mut recipe)
.store_recipe(&mut recipe)
.expect("We expect the recpe to store successfully"); .expect("We expect the recpe to store successfully");
assert!(recipe.id.is_some());
let recipes: Vec<Recipe> = in_memory let recipes: Vec<Recipe> = tx
.fetch_all_recipes() .fetch_all_recipes()
.expect("We expect to get recipes back out"); .expect("We expect to get recipes back out");
assert_eq!(recipes.len(), 1); assert_eq!(recipes.len(), 1);
let recipe: Option<Recipe> = in_memory let recipe: Option<Recipe> = tx
.fetch_recipe("my recipe") .fetch_recipe_by_title("my recipe")
.expect("We expect the recipe to come back out"); .expect("We expect the recipe to come back out");
assert!(recipe.is_some()); assert!(recipe.is_some());
let recipe = recipe.unwrap(); let recipe = recipe.unwrap();
@ -97,6 +95,7 @@ fn test_recipe_store_update_roundtrip_full() {
#[test] #[test]
fn test_fetch_recipe_ingredients() { fn test_fetch_recipe_ingredients() {
let mut in_memory = init_sqlite_store!(); 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 recipe = Recipe::new("my recipe", "my description");
let mut step1 = Step::new( let mut step1 = Step::new(
Some(std::time::Duration::from_secs(60 * 30)), Some(std::time::Duration::from_secs(60 * 30)),
@ -114,12 +113,11 @@ fn test_fetch_recipe_ingredients() {
let step2 = Step::new(None, "combine ingredients"); let step2 = Step::new(None, "combine ingredients");
recipe.add_steps(vec![step1.clone(), step2.clone()]); recipe.add_steps(vec![step1.clone(), step2.clone()]);
in_memory tx.store_recipe(&mut recipe)
.store_recipe(&mut recipe)
.expect("We expect the recpe to store successfully"); .expect("We expect the recpe to store successfully");
let ingredients = in_memory let ingredients = tx
.fetch_recipe_ingredients(recipe.id.unwrap()) .fetch_recipe_ingredients(recipe.id)
.expect("We expect to fetch ingredients for the recipe"); .expect("We expect to fetch ingredients for the recipe");
assert_eq!(ingredients.len(), 2); assert_eq!(ingredients.len(), 2);
assert_eq!(ingredients, step1.ingredients); assert_eq!(ingredients, step1.ingredients);

View File

@ -8,6 +8,11 @@ edition = "2018"
[dependencies] [dependencies]
abortable_parser = "~0.2.4" abortable_parser = "~0.2.4"
chrono = "~0.4"
[dependencies.uuid]
version = "~0.8.2"
features = ["v4"]
[dependencies.num-rational] [dependencies.num-rational]
version = "~0.4.0" version = "~0.4.0"

View File

@ -15,12 +15,48 @@ pub mod unit;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use chrono::NaiveDate;
use uuid::{self, Uuid};
use unit::*; use unit::*;
#[derive(Debug, Clone, PartialEq)]
pub struct Mealplan {
pub id: uuid::Uuid,
pub start_date: Option<NaiveDate>,
pub recipes: Vec<Recipe>,
}
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<Iter>(&mut self, recipes: Iter)
where
Iter: IntoIterator<Item = Recipe>,
{
self.recipes.extend(recipes.into_iter())
}
}
/// A Recipe with a title, description, and a series of steps. /// A Recipe with a title, description, and a series of steps.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Recipe { pub struct Recipe {
pub id: Option<i64>, pub id: uuid::Uuid, // TODO(jwall): use uuid instead?
pub title: String, pub title: String,
pub desc: String, pub desc: String,
pub steps: Vec<Step>, pub steps: Vec<Step>,
@ -29,16 +65,16 @@ pub struct Recipe {
impl Recipe { impl Recipe {
pub fn new<S: Into<String>>(title: S, desc: S) -> Self { pub fn new<S: Into<String>>(title: S, desc: S) -> Self {
Self { Self {
id: None, id: uuid::Uuid::new_v4(),
title: title.into(), title: title.into(),
desc: desc.into(), desc: desc.into(),
steps: Vec::new(), steps: Vec::new(),
} }
} }
pub fn new_id<S: Into<String>>(id: i64, title: S, desc: S) -> Self { pub fn new_id<S: Into<String>>(id: uuid::Uuid, title: S, desc: S) -> Self {
Self { Self {
id: Some(id), id: id,
title: title.into(), title: title.into(),
desc: desc.into(), desc: desc.into(),
steps: Vec::new(), steps: Vec::new(),
@ -46,7 +82,10 @@ impl Recipe {
} }
/// Add steps to the end of the recipe. /// Add steps to the end of the recipe.
pub fn add_steps(&mut self, steps: Vec<Step>) { pub fn add_steps<Iter>(&mut self, steps: Iter)
where
Iter: IntoIterator<Item = Step>,
{
self.steps.extend(steps.into_iter()); self.steps.extend(steps.into_iter());
} }
@ -122,8 +161,11 @@ impl Step {
} }
} }
pub fn add_ingredients(&mut self, mut ingredients: Vec<Ingredient>) { pub fn add_ingredients<Iter>(&mut self, ingredients: Iter)
self.ingredients.append(&mut ingredients); where
Iter: IntoIterator<Item = Ingredient>,
{
self.ingredients.extend(ingredients.into_iter());
} }
pub fn add_ingredient(&mut self, ingredient: Ingredient) { pub fn add_ingredient(&mut self, ingredient: Ingredient) {
@ -140,7 +182,7 @@ pub struct IngredientKey(String, Option<String>, String);
/// uniquely identify an ingredient. /// uniquely identify an ingredient.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Ingredient { pub struct Ingredient {
pub id: Option<i64>, pub id: Option<i64>, // TODO(jwall): use uuid instead?
pub name: String, pub name: String,
pub form: Option<String>, pub form: Option<String>,
pub amt: Measure, pub amt: Measure,