mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
We are going to revisit all this later.
This commit is contained in:
parent
b21bdd0252
commit
d261850bd6
232
Cargo.lock
generated
232
Cargo.lock
generated
@ -2,15 +2,9 @@
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "abortable_parser"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f520551d986b2be672fac2b1d749874f914ecc03fb347c7b0b5a7e541ffe435"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
|
||||
checksum = "bec7b38b411d838e24b7914898b2d3cf3e24adbd81b6edf778e80ea23fe5e9d1"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@ -18,25 +12,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
@ -49,15 +24,6 @@ version = "1.0.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@ -77,17 +43,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
@ -115,18 +70,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "fs2"
|
||||
version = "0.4.3"
|
||||
@ -148,79 +91,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "kitchen"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"recipe-store",
|
||||
"recipes",
|
||||
"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]]
|
||||
name = "libc"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
@ -257,16 +141,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.0"
|
||||
@ -309,36 +183,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "radix_trie"
|
||||
version = "0.2.1"
|
||||
@ -349,16 +193,6 @@ dependencies = [
|
||||
"nibble_vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "recipe-store"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"recipes",
|
||||
"rusqlite",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "recipes"
|
||||
version = "0.1.0"
|
||||
@ -388,44 +222,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rustyline"
|
||||
version = "8.0.0"
|
||||
@ -455,12 +251,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.6.1"
|
||||
@ -490,12 +280,6 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.0"
|
||||
@ -511,18 +295,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
|
@ -1,2 +1,2 @@
|
||||
[workspace]
|
||||
members = [ "recipes", "recipe-store", "kitchen" ]
|
||||
members = [ "recipes", "kitchen" ]
|
@ -8,5 +8,4 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
recipes = {path = "../recipes" }
|
||||
recipe-store = {path = "../recipe-store" }
|
||||
rustyline = "~8.0.0"
|
||||
|
@ -11,23 +11,3 @@
|
||||
// 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 {
|
||||
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
137
kitchen/src/cli.rs
Normal 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();
|
||||
//}
|
@ -12,7 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
mod api;
|
||||
mod cli;
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
//cli::main_impl();
|
||||
}
|
||||
|
42
models/recipe.als
Normal file
42
models/recipe.als
Normal 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
|
||||
}
|
@ -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"]
|
@ -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;
|
@ -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);
|
||||
}
|
@ -7,7 +7,7 @@ edition = "2018"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
abortable_parser = "~0.2.5"
|
||||
abortable_parser = "~0.2.6"
|
||||
chrono = "~0.4"
|
||||
|
||||
[dependencies.uuid]
|
||||
|
@ -61,29 +61,37 @@ impl Mealplan {
|
||||
pub struct Recipe {
|
||||
pub id: uuid::Uuid,
|
||||
pub title: String,
|
||||
pub desc: String,
|
||||
pub desc: Option<String>,
|
||||
pub steps: Vec<Step>,
|
||||
}
|
||||
|
||||
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 {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
title: title.into(),
|
||||
desc: desc.into(),
|
||||
desc: desc.map(|s| s.into()),
|
||||
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 {
|
||||
id: id,
|
||||
title: title.into(),
|
||||
desc: desc.into(),
|
||||
desc: desc.map(|s| s.into()),
|
||||
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.
|
||||
pub fn add_steps<Iter>(&mut self, steps: Iter)
|
||||
where
|
||||
|
Loading…
x
Reference in New Issue
Block a user