We are going to revisit all this later.

This commit is contained in:
Jeremy Wall 2021-11-02 21:45:41 -04:00
parent b21bdd0252
commit d261850bd6
12 changed files with 198 additions and 788 deletions

232
Cargo.lock generated
View File

@ -2,15 +2,9 @@
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]] [[package]]
name = "abortable_parser" name = "abortable_parser"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f520551d986b2be672fac2b1d749874f914ecc03fb347c7b0b5a7e541ffe435" checksum = "bec7b38b411d838e24b7914898b2d3cf3e24adbd81b6edf778e80ea23fe5e9d1"
[[package]]
name = "ahash"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@ -18,25 +12,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bindgen"
version = "0.58.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"lazy_static",
"lazycell",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.2.1"
@ -49,15 +24,6 @@ version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
[[package]]
name = "cexpr"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
dependencies = [
"nom",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -77,17 +43,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "clang-sys"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]] [[package]]
name = "dirs-next" name = "dirs-next"
version = "2.0.0" version = "2.0.0"
@ -115,18 +70,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]] [[package]]
name = "fs2" name = "fs2"
version = "0.4.3" version = "0.4.3"
@ -148,79 +91,20 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8"
dependencies = [
"hashbrown",
]
[[package]] [[package]]
name = "kitchen" name = "kitchen"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"recipe-store",
"recipes", "recipes",
"rustyline", "rustyline",
] ]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.93" version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
[[package]]
name = "libloading"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
dependencies = [
"cfg-if",
"winapi",
]
[[package]]
name = "libsqlite3-sys"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19cb1effde5f834799ac5e5ef0e40d45027cd74f271b1de786ba8abb30e2164d"
dependencies = [
"bindgen",
"cc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.14" version = "0.4.14"
@ -257,16 +141,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "nom"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
"memchr",
"version_check",
]
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.0" version = "0.4.0"
@ -309,36 +183,6 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "pkg-config"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
[[package]]
name = "proc-macro2"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]] [[package]]
name = "radix_trie" name = "radix_trie"
version = "0.2.1" version = "0.2.1"
@ -349,16 +193,6 @@ dependencies = [
"nibble_vec", "nibble_vec",
] ]
[[package]]
name = "recipe-store"
version = "0.1.0"
dependencies = [
"chrono",
"recipes",
"rusqlite",
"uuid",
]
[[package]] [[package]]
name = "recipes" name = "recipes"
version = "0.1.0" version = "0.1.0"
@ -388,44 +222,6 @@ dependencies = [
"redox_syscall", "redox_syscall",
] ]
[[package]]
name = "regex"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548"
[[package]]
name = "rusqlite"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc783b7ddae608338003bac1fa00b6786a75a9675fbd8e87243ecfdea3f6ed2"
dependencies = [
"bitflags",
"chrono",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"memchr",
"smallvec",
"uuid",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustyline" name = "rustyline"
version = "8.0.0" version = "8.0.0"
@ -455,12 +251,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "shlex"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.6.1" version = "1.6.1"
@ -490,12 +280,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.0" version = "0.2.0"
@ -511,18 +295,6 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "vcpkg"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d"
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.0+wasi-snapshot-preview1" version = "0.10.0+wasi-snapshot-preview1"

View File

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

View File

@ -8,5 +8,4 @@ edition = "2018"
[dependencies] [dependencies]
recipes = {path = "../recipes" } recipes = {path = "../recipes" }
recipe-store = {path = "../recipe-store" }
rustyline = "~8.0.0" rustyline = "~8.0.0"

View File

@ -11,23 +11,3 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::convert::From;
use recipe_store::{RecipeStore, SqliteBackend};
use recipes::*;
pub struct Api {
store: SqliteBackend,
}
impl Api {
pub fn new_recipe_from_str(&self, input: &str) {}
pub fn new_mealplan_from_str(&self, input: &str) {}
}
impl From<SqliteBackend> for Api {
fn from(store: SqliteBackend) -> Self {
Api { store }
}
}

137
kitchen/src/cli.rs Normal file
View File

@ -0,0 +1,137 @@
// Copyright 2021 Jeremy Wall
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::convert::Into;
use std::convert::TryInto;
use rustyline::error::ReadlineError;
use rustyline::Editor;
use recipes::{Ingredient, Recipe, Step};
pub enum CliResponse<T> {
Interrupt,
EndOfInput,
ReadItem(T),
}
use CliResponse::*;
macro_rules! try_cli_resp {
($res:expr) => {
match $res {
ReadItem(item) => item,
EndOfInput => return Ok(EndOfInput),
Interrupt => return Ok(Interrupt),
}
};
}
macro_rules! handle_yes_no {
($rl:expr, $( $rest:tt )+) => {
match try_cli_resp!(read_prompt($rl, "Y/n: ")?).as_str() {
"y" | "Y" | "yes" | "" => {
$( $rest )*
}
_ => {
break
}
}
};
}
fn read_prompt(rl: &mut Editor<()>, prompt: &str) -> Result<CliResponse<String>, String> {
Ok(match rl.readline(prompt) {
Ok(line) => ReadItem(line),
Err(ReadlineError::Interrupted) => Interrupt,
Err(ReadlineError::Eof) => EndOfInput,
Err(e) => return Err(e.to_string()),
})
}
fn read_new_ingredient(rl: &mut Editor<()>) -> Result<CliResponse<Option<Ingredient>>, String> {
let read_item = try_cli_resp!(read_prompt(rl, "> ")?);
Ok(if read_item.is_empty() {
ReadItem(None)
} else {
ReadItem(Some(Ingredient::parse(&read_item)?))
})
}
fn read_ingredients(rl: &mut Editor<()>) -> Result<CliResponse<Vec<Ingredient>>, String> {
println!("Enter Ingredients in the following form below: <amt> <unit> <name> [(modifier)]");
println!("<Ctrl-C> or enter an empty line to stop entering ingredients");
let mut ingredient_list = Vec::new();
loop {
match read_new_ingredient(rl)? {
Interrupt => break,
EndOfInput => return Ok(EndOfInput),
ReadItem(None) => break,
ReadItem(Some(ingredient)) => ingredient_list.push(ingredient),
}
}
Ok(ReadItem(ingredient_list))
}
fn read_new_step(rl: &mut Editor<()>) -> Result<CliResponse<Step>, String> {
println!("Enter Recipe Step details below");
let instructions = try_cli_resp!(read_prompt(rl, "Step Instructions: ")?);
let ingredients = try_cli_resp!(read_ingredients(rl)?);
let mut step = Step::new(None, instructions);
step.add_ingredients(ingredients);
Ok(ReadItem(step))
}
fn read_steps(rl: &mut Editor<()>) -> Result<CliResponse<Vec<Step>>, String> {
let mut steps = Vec::new();
loop {
println!("Enter a recipe step?");
handle_yes_no! {rl,
let step = try_cli_resp!(read_new_step(rl)?);
steps.push(step);
};
}
Ok(ReadItem(steps))
}
//pub fn read_new_recipe(rl: &mut Editor<()>) -> Result<CliResponse<Recipe>, String> {
// println!("Enter recipe details below.");
// let title = try_cli_resp!(read_prompt(rl, "Title: ")?);
// let desc = try_cli_resp!(read_prompt(rl, "Description: ")?);
// let steps = try_cli_resp!(read_steps(rl)?);
// let mut recipe = Recipe::new(title, desc);
// recipe.add_steps(steps);
// Ok(ReadItem(recipe))
//}
//fn read_loop<S: RecipeStore>(rl: &mut Editor<()>, store: S) -> Result<CliResponse<()>, String> {
// loop {
// println!("Enter a recipe?");
// handle_yes_no! {rl,
// let recipe = try_cli_resp!(read_new_recipe(rl)?);
// // TODO Store this recipe
// store.store_recipe(&recipe)?;
// };
// }
// Ok(ReadItem(()))
//}
//pub fn main_impl<Factory, Err>(factory: Factory)
//where
// Factory: TryInto<Error = Err>,
// Err: std::fmt::Debug,
//{
// let mut rl = Editor::<()>::new();
// let store = factory.try_into().unwrap();
// // TODO(jwall): handle history in a cross platform way?
// //read_loop(&mut rl, store).unwrap();
//}

View File

@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
mod api; mod api;
mod cli;
fn main() { fn main() {
println!("Hello, world!"); //cli::main_impl();
} }

42
models/recipe.als Normal file
View File

@ -0,0 +1,42 @@
sig Recipe {
, desc: String
, title: String
, ingredients: set Ingredient
}
sig Cat {
, name: String
}
abstract sig Unit { }
sig Tsp extends Unit {}
sig Tbsp extends Unit {}
sig Cup extends Unit {}
sig Pint extends Unit {}
sig Quart extends Unit {}
sig Gallon extends Unit {}
abstract sig Wgt extends Unit {
}
sig MilliGram extends Wgt {}
sig Gram extends Wgt {}
sig KiloGram extends Wgt {}
abstract sig Amt {}
sig Count extends Amt {
, value: Int
}
sig Frac extends Amt {
, numerator: Int
, denominator: Int
}
sig Ingredient {
, category: Cat
, name: String
, unit: Unit
, amt: Amt
}

View File

@ -1,19 +0,0 @@
[package]
name = "recipe-store"
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" }
chrono = "~0.4"
[dependencies.rusqlite]
version = "~0.25.0"
features = ["backup", "bundled", "session", "chrono", "uuid"]
[dependencies.uuid]
version = "~0.8.2"
features = ["v4"]

View File

@ -1,386 +0,0 @@
// Copyright 2021 Jeremy Wall
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// Storage backend for recipes.
use std::convert::From;
use std::path::Path;
use chrono::NaiveDate;
use rusqlite::{params, Connection, Result as SqliteResult, Transaction};
use uuid::Uuid;
use recipes::{unit::Measure, Ingredient, Mealplan, Recipe, Step};
// TODO Model the error domain of our storage layer.
#[derive(Debug)]
pub struct StorageError {
message: String,
}
impl From<rusqlite::Error> for StorageError {
fn from(e: rusqlite::Error) -> Self {
match e {
rusqlite::Error::SqliteFailure(e, msg) => StorageError {
message: format!("{}: {}", e, msg.unwrap_or_default()),
},
rusqlite::Error::SqliteSingleThreadedMode => unimplemented!(),
rusqlite::Error::FromSqlConversionFailure(_, _, _) => unimplemented!(),
rusqlite::Error::IntegralValueOutOfRange(_, _) => todo!(),
rusqlite::Error::Utf8Error(_) => todo!(),
rusqlite::Error::NulError(_) => todo!(),
rusqlite::Error::InvalidParameterName(_) => todo!(),
rusqlite::Error::InvalidPath(_) => todo!(),
rusqlite::Error::ExecuteReturnedResults => todo!(),
rusqlite::Error::QueryReturnedNoRows => todo!(),
rusqlite::Error::InvalidColumnIndex(_) => todo!(),
rusqlite::Error::InvalidColumnName(_) => todo!(),
rusqlite::Error::InvalidColumnType(_, _, _) => todo!(),
rusqlite::Error::StatementChangedRows(_) => todo!(),
rusqlite::Error::ToSqlConversionFailure(_) => todo!(),
rusqlite::Error::InvalidQuery => todo!(),
rusqlite::Error::MultipleStatement => todo!(),
rusqlite::Error::InvalidParameterCount(_, _) => todo!(),
_ => todo!(),
}
}
}
pub enum IterResult<Entity> {
Some(Entity),
Err(StorageError),
None,
}
pub trait RecipeStore {
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: 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 {
conn: Connection,
}
impl SqliteBackend {
pub fn new<P: AsRef<Path>>(path: P) -> SqliteResult<Self> {
Ok(Self {
conn: Connection::open(path)?,
})
}
pub fn new_in_memory() -> SqliteResult<Self> {
Ok(Self {
conn: Connection::open_in_memory()?,
})
}
pub fn get_schema_version(&self) -> SqliteResult<Option<u32>> {
let mut stmt = self
.conn
.prepare("SELECT max(version) from schema_version")?;
Ok(stmt.query_row([], |r| r.get(0))?)
}
pub fn start_transaction<'a>(&'a mut self) -> SqliteResult<TxHandle<'a>> {
self.conn.transaction().map(|tx| TxHandle { tx })
}
pub fn create_schema(&self) -> SqliteResult<()> {
self.conn.execute(
"CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY )",
[],
)?;
let version = self.get_schema_version()?;
if let None = version {
self.conn
.execute("INSERT INTO schema_version ( version ) values ( 0 )", [])?;
} else {
return Ok(());
}
self.conn.execute(
"CREATE TABLE IF NOT EXISTS mealplans (
id BLOB PRIMARY KEY,
start_date TEXT
)",
[],
)?;
self.conn.execute(
"CREATE TABLE IF NOT EXISTS recipes (
id BLOB PRIMARY KEY,
title TEXT UNIQUE NOT NULL,
desc TEXT NOT NULL
)",
[],
)?;
self.conn.execute(
"CREATE TABLE IF NOT EXISTS mealplan_recipes (
plan_id BLOB NOT NULL,
recipe_id BLOB NOT NULL,
recipe_idx INTEGER NOT NULL,
FOREIGN KEY(plan_id) REFERENCES mealplans(id)
FOREIGN KEY(recipe_id) REFERENCES recipes(id)
)",
[],
)?;
self.conn.execute(
"CREATE TABLE IF NOT EXISTS steps (
recipe_id BLOB NOT NULL,
step_idx INTEGER NOT NULL,
prep_time INTEGER, -- in seconds
instructions TEXT NOT NULL,
FOREIGN KEY(recipe_id) REFERENCES recipes(id)
CONSTRAINT step_key PRIMARY KEY (recipe_id, step_idx)
)",
[],
)?;
self.conn.execute(
"CREATE TABLE IF NOT EXISTS step_ingredients (
recipe_id BLOB NOT NULL,
step_idx INTEGER NOT NULL,
ingredient_idx INTEGER NOT NULL,
name TEXT NOT NULL,
amt TEXT NOT NULL,
category TEXT NOT NULL,
form TEXT,
FOREIGN KEY(recipe_id, step_idx) REFERENCES steps(recipe_id, step_idx),
CONSTRAINT step_ingredients_key PRIMARY KEY (recipe_id, step_idx, name, form)
)",
[],
)?;
Ok(())
}
}
pub struct TxHandle<'conn> {
tx: Transaction<'conn>,
}
impl<'conn> TxHandle<'conn> {
pub fn serialize_step_stmt_rows(
mut stmt: rusqlite::Statement,
recipe_id: Uuid,
) -> Result<Option<Vec<Step>>, StorageError> {
if let Ok(step_iter) = stmt.query_map(params![recipe_id], |row| {
let prep_time: Option<i64> = row.get(2)?;
let instructions: String = row.get(3)?;
Ok(Step::new(
prep_time.map(|i| std::time::Duration::from_secs(i as u64)),
instructions,
))
}) {
let mut steps = Vec::new();
for step in step_iter {
steps.push(step?);
}
return Ok(Some(steps));
}
return Ok(None);
}
fn fill_recipe_steps(&self, recipe: &mut Recipe) -> Result<(), StorageError> {
let stmt = self
.tx
.prepare("SELECT * FROM steps WHERE recipe_id = ?1 ORDER BY step_idx")?;
let steps = Self::serialize_step_stmt_rows(stmt, recipe.id)?;
let mut stmt = self.tx.prepare("SELECT * from step_ingredients WHERE recipe_id = ?1 and step_idx = ?2 ORDER BY ingredient_idx")?;
if let Some(mut steps) = steps {
for (step_idx, mut step) in steps.drain(0..).enumerate() {
// TODO(jwall): Fetch the ingredients.
let ing_iter = stmt.query_map(params![recipe.id, step_idx], |row| {
Self::map_ingredient_row(row)
})?;
for ing in ing_iter {
step.ingredients.push(ing?);
}
recipe.add_step(step);
}
}
Ok(())
}
fn fill_plan_recipes(&self, plan: &mut Mealplan) -> Result<(), StorageError> {
let mut stmt = self
.tx
.prepare("SELECT recipe_id from mealplan_recipes where plan_id = ?1")?;
let id_iter = stmt.query_map(params![plan.id], |row| {
let id: Uuid = row.get(0)?;
Ok(id)
})?;
for id in id_iter {
// TODO(jwall): A potential optimzation here is to do this in a single
// select instead of a one at a time.
if let Some(recipe) = self.fetch_recipe_by_id(id?)? {
plan.recipes.push(recipe);
}
}
Ok(())
}
fn map_recipe_row(r: &rusqlite::Row) -> Result<Recipe, rusqlite::Error> {
let id: Uuid = r.get(0)?;
let title: String = r.get(1)?;
let desc: String = r.get(2)?;
Ok(Recipe::new_with_id(id, title, desc))
}
fn map_ingredient_row(r: &rusqlite::Row) -> Result<Ingredient, rusqlite::Error> {
let name: String = r.get(3)?;
let amt: String = r.get(4)?;
let category = r.get(5)?;
let form = r.get(6)?;
Ok(Ingredient::new(
name,
form,
dbg!(Measure::parse(dbg!(&amt)).unwrap()),
category,
))
}
}
impl<'conn> RecipeStore for TxHandle<'conn> {
fn fetch_all_recipes(&self) -> Result<Vec<Recipe>, StorageError> {
let mut stmt = self.tx.prepare("SELECT * FROM recipes")?;
let recipe_iter = stmt.query_map([], |r| Self::map_recipe_row(r))?;
let mut recipes = Vec::new();
for next in recipe_iter {
recipes.push(next?);
}
Ok(recipes)
}
fn store_recipe(&self, recipe: &Recipe) -> Result<(), StorageError> {
// If we don't have a transaction already we should start one.
self.tx.execute(
"INSERT OR REPLACE INTO recipes (id, title, desc) VALUES (?1, ?2, ?3)",
params![recipe.id, recipe.title, recipe.desc],
)?;
for (idx, step) in recipe.steps.iter().enumerate() {
self.tx.execute("INSERT INTO steps (recipe_id, step_idx, prep_time, instructions) VALUES (?1, ?2, ?3, ?4)",
params![recipe.id, dbg!(idx), step.prep_time.map(|v| v.as_secs()) , step.instructions])?;
for (ing_idx, ing) in step.ingredients.iter().enumerate() {
dbg!(self.tx.execute(
"INSERT INTO step_ingredients (recipe_id, step_idx, ingredient_idx, name, amt, category, form) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![recipe.id, dbg!(idx), ing_idx, ing.name, format!("{}", ing.amt), ing.category, ing.form])?);
}
}
Ok(())
}
fn store_mealplan(&self, plan: &Mealplan) -> Result<(), StorageError> {
self.tx.execute(
"INSERT OR REPLACE INTO mealplans (id, start_date) VALUES (?1, ?2)",
params![plan.id, plan.start_date],
)?;
for (idx, recipe) in plan.recipes.iter().enumerate() {
self.tx.execute(
"INSERT INTO mealplan_recipes (plan_id, recipe_id, recipe_idx) VALUES (?1, ?2, ?3)",
params![plan.id, recipe.id, idx],
)?;
}
Ok(())
}
fn fetch_mealplan(&self, plan_id: Uuid) -> Result<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() {
self.fill_recipe_steps(recipe)?;
}
return Ok(recipe);
}
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))?;
let mut ingredients = Vec::new();
for i in ing_iter {
ingredients.push(i?);
}
Ok(ingredients)
}
}
#[cfg(test)]
mod tests;

View File

@ -1,124 +0,0 @@
// Copyright 2021 Jeremy Wall
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::*;
use std::convert::Into;
macro_rules! init_sqlite_store {
() => {{
let in_memory =
SqliteBackend::new_in_memory().expect("We expect in memory connections to succeed");
in_memory
.create_schema()
.expect("We expect the schema creation to succeed");
let version = in_memory
.get_schema_version()
.expect("We expect the version fetch to succeed");
assert!(version.is_some());
assert_eq!(version.unwrap(), 0);
in_memory
}};
}
#[test]
fn test_schema_creation() {
let in_memory = init_sqlite_store!();
in_memory
.create_schema()
.expect("create_schema is idempotent");
let version = in_memory
.get_schema_version()
.expect("We expect the version fetch to succeed");
assert!(version.is_some());
assert_eq!(version.unwrap(), 0);
}
#[test]
fn test_recipe_store_update_roundtrip_full() {
let mut in_memory = init_sqlite_store!();
let tx = in_memory.start_transaction().unwrap();
let mut recipe = Recipe::new("my recipe", "my description");
let mut step1 = Step::new(
Some(std::time::Duration::from_secs(60 * 30)),
"mix thoroughly",
);
step1.add_ingredients(vec![
Ingredient::new("flour", None, Measure::cup(1.into()), "dry goods"),
Ingredient::new(
"salt",
Some("Ground".to_owned()),
Measure::tsp(1.into()),
"seasoning",
),
]);
let step2 = Step::new(None, "combine ingredients");
recipe.add_steps(vec![step1.clone(), step2.clone()]);
tx.store_recipe(&mut recipe)
.expect("We expect the recpe to store successfully");
let recipes: Vec<Recipe> = tx
.fetch_all_recipes()
.expect("We expect to get recipes back out");
assert_eq!(recipes.len(), 1);
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();
assert_eq!(recipe.title, "my recipe");
assert_eq!(recipe.desc, "my description");
assert_eq!(recipe.steps.len(), 2);
let step1_got = &recipe.steps[0];
let step2_got = &recipe.steps[1];
assert_eq!(step1_got.prep_time, step1.prep_time);
assert_eq!(step1_got.instructions, step1.instructions);
assert_eq!(step1_got.ingredients.len(), step1.ingredients.len());
assert_eq!(step1_got.ingredients[0], step1.ingredients[0]);
assert_eq!(step1_got.ingredients[1], step1.ingredients[1]);
assert_eq!(step2_got.prep_time, step2.prep_time);
assert_eq!(step2_got.instructions, step2.instructions);
assert_eq!(step2_got.ingredients.len(), step2.ingredients.len());
}
#[test]
fn test_fetch_recipe_ingredients() {
let mut in_memory = init_sqlite_store!();
let tx = in_memory.start_transaction().unwrap();
let mut recipe = Recipe::new("my recipe", "my description");
let mut step1 = Step::new(
Some(std::time::Duration::from_secs(60 * 30)),
"mix thoroughly",
);
step1.add_ingredients(vec![
Ingredient::new("flour", None, Measure::cup(1.into()), "dry goods"),
Ingredient::new(
"salt",
Some("Ground".to_owned()),
Measure::tsp(1.into()),
"seasoning",
),
]);
let step2 = Step::new(None, "combine ingredients");
recipe.add_steps(vec![step1.clone(), step2.clone()]);
tx.store_recipe(&mut recipe)
.expect("We expect the recpe to store successfully");
let ingredients = tx
.fetch_recipe_ingredients(recipe.id)
.expect("We expect to fetch ingredients for the recipe");
assert_eq!(ingredients.len(), 2);
assert_eq!(ingredients, step1.ingredients);
}

View File

@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
abortable_parser = "~0.2.5" abortable_parser = "~0.2.6"
chrono = "~0.4" chrono = "~0.4"
[dependencies.uuid] [dependencies.uuid]

View File

@ -61,29 +61,37 @@ impl Mealplan {
pub struct Recipe { pub struct Recipe {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub title: String, pub title: String,
pub desc: String, pub desc: Option<String>,
pub steps: Vec<Step>, pub steps: Vec<Step>,
} }
impl Recipe { impl Recipe {
pub fn new<S: Into<String>>(title: S, desc: S) -> Self { pub fn new<S: Into<String>>(title: S, desc: Option<S>) -> Self {
Self { Self {
id: uuid::Uuid::new_v4(), id: uuid::Uuid::new_v4(),
title: title.into(), title: title.into(),
desc: desc.into(), desc: desc.map(|s| s.into()),
steps: Vec::new(), steps: Vec::new(),
} }
} }
pub fn new_with_id<S: Into<String>>(id: uuid::Uuid, title: S, desc: S) -> Self { pub fn new_with_id<S: Into<String>>(id: uuid::Uuid, title: S, desc: Option<S>) -> Self {
Self { Self {
id: id, id: id,
title: title.into(), title: title.into(),
desc: desc.into(), desc: desc.map(|s| s.into()),
steps: Vec::new(), steps: Vec::new(),
} }
} }
pub fn with_steps<Iter>(mut self, steps: Iter) -> Self
where
Iter: IntoIterator<Item = Step>,
{
self.add_steps(steps);
self
}
/// Add steps to the end of the recipe. /// Add steps to the end of the recipe.
pub fn add_steps<Iter>(&mut self, steps: Iter) pub fn add_steps<Iter>(&mut self, steps: Iter)
where where