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"
|
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"
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
[workspace]
|
[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]
|
[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"]
|
@ -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))?;
|
||||||
|
@ -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);
|
||||||
|
@ -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"
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user