mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Start building out the command line API for this
This commit is contained in:
parent
32bb68b911
commit
5a7ea05522
64
Cargo.lock
generated
64
Cargo.lock
generated
@ -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"
|
||||
|
@ -1,2 +1,2 @@
|
||||
[workspace]
|
||||
members = [ "recipes", "recipe-store" ]
|
||||
members = [ "recipes", "recipe-store", "kitchen" ]
|
11
kitchen/Cargo.toml
Normal file
11
kitchen/Cargo.toml
Normal 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
31
kitchen/src/api.rs
Normal 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
18
kitchen/src/main.rs
Normal 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!");
|
||||
}
|
@ -8,7 +8,12 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
recipes = { path="../recipes" }
|
||||
chrono = "~0.4"
|
||||
|
||||
[dependencies.rusqlite]
|
||||
version = "~0.25.0"
|
||||
features = ["backup", "bundled", "session"]
|
||||
features = ["backup", "bundled", "session", "chrono", "uuid"]
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "~0.8.2"
|
||||
features = ["v4"]
|
@ -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<Entity> {
|
||||
}
|
||||
|
||||
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_recipe_steps(&self, recipe_id: i64) -> Result<Option<Vec<Step>>, StorageError>;
|
||||
fn fetch_recipe(&mut self, k: &str) -> Result<Option<Recipe>, StorageError>;
|
||||
fn fetch_recipe_ingredients(&self, recipe_id: i64) -> Result<Vec<Ingredient>, StorageError>;
|
||||
fn fetch_recipe_steps(&self, recipe_id: Uuid) -> Result<Option<Vec<Step>>, StorageError>;
|
||||
fn fetch_recipe_by_title(&self, k: &str) -> Result<Option<Recipe>, 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 {
|
||||
@ -91,11 +97,8 @@ impl SqliteBackend {
|
||||
Ok(stmt.query_row([], |r| r.get(0))?)
|
||||
}
|
||||
|
||||
pub fn start_transaction<'a>(&'a mut self) -> SqliteResult<Transaction<'a>> {
|
||||
self.conn.transaction().map(|mut tx| {
|
||||
tx.set_drop_behavior(DropBehavior::Commit);
|
||||
tx
|
||||
})
|
||||
pub fn start_transaction<'a>(&'a mut self) -> SqliteResult<TxHandle<'a>> {
|
||||
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<Option<Vec<Step>>, StorageError> {
|
||||
if let Ok(step_iter) = stmt.query_map(params![recipe_id], |row| {
|
||||
let prep_time: Option<i64> = 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<Recipe, rusqlite::Error> {
|
||||
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<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 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(
|
||||
self.tx.execute(
|
||||
"INSERT OR REPLACE INTO recipes (id, title, desc) VALUES (?1, ?2, ?3)",
|
||||
params![id, recipe.title, recipe.desc],
|
||||
params![recipe.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() {
|
||||
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<Option<Vec<Step>>, 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<Option<Recipe>, 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<Mealplan, StorageError> {
|
||||
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<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 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<Vec<Ingredient>, StorageError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
fn fetch_recipe_by_title(&self, key: &str) -> Result<Option<Recipe>, 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<Vec<Ingredient>, 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))?;
|
||||
|
@ -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<Recipe> = in_memory
|
||||
let recipes: Vec<Recipe> = tx
|
||||
.fetch_all_recipes()
|
||||
.expect("We expect to get recipes back out");
|
||||
assert_eq!(recipes.len(), 1);
|
||||
let recipe: Option<Recipe> = in_memory
|
||||
.fetch_recipe("my recipe")
|
||||
let recipe: Option<Recipe> = 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);
|
||||
|
@ -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"
|
@ -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<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.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Recipe {
|
||||
pub id: Option<i64>,
|
||||
pub id: uuid::Uuid, // TODO(jwall): use uuid instead?
|
||||
pub title: String,
|
||||
pub desc: String,
|
||||
pub steps: Vec<Step>,
|
||||
@ -29,16 +65,16 @@ pub struct Recipe {
|
||||
impl Recipe {
|
||||
pub fn new<S: Into<String>>(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<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 {
|
||||
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<Step>) {
|
||||
pub fn add_steps<Iter>(&mut self, steps: Iter)
|
||||
where
|
||||
Iter: IntoIterator<Item = Step>,
|
||||
{
|
||||
self.steps.extend(steps.into_iter());
|
||||
}
|
||||
|
||||
@ -122,8 +161,11 @@ impl Step {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_ingredients(&mut self, mut ingredients: Vec<Ingredient>) {
|
||||
self.ingredients.append(&mut ingredients);
|
||||
pub fn add_ingredients<Iter>(&mut self, ingredients: Iter)
|
||||
where
|
||||
Iter: IntoIterator<Item = Ingredient>,
|
||||
{
|
||||
self.ingredients.extend(ingredients.into_iter());
|
||||
}
|
||||
|
||||
pub fn add_ingredient(&mut self, ingredient: Ingredient) {
|
||||
@ -140,7 +182,7 @@ pub struct IngredientKey(String, Option<String>, String);
|
||||
/// uniquely identify an ingredient.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Ingredient {
|
||||
pub id: Option<i64>,
|
||||
pub id: Option<i64>, // TODO(jwall): use uuid instead?
|
||||
pub name: String,
|
||||
pub form: Option<String>,
|
||||
pub amt: Measure,
|
||||
|
Loading…
x
Reference in New Issue
Block a user