diff --git a/Cargo.lock b/Cargo.lock index 0dc0b6c..01a2e07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,9 +2,9 @@ # It is not intended for manual editing. [[package]] name = "abortable_parser" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2febc32aeed847847255580d5938d62c8e3f6cb0a019cc0d36dc1405827d4ec" +checksum = "8f520551d986b2be672fac2b1d749874f914ecc03fb347c7b0b5a7e541ffe435" [[package]] name = "ahash" @@ -88,6 +88,33 @@ dependencies = [ "libloading", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -100,6 +127,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getrandom" version = "0.2.2" @@ -141,6 +178,7 @@ version = "0.1.0" dependencies = [ "recipe-store", "recipes", + "rustyline", ] [[package]] @@ -183,12 +221,42 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", +] + [[package]] name = "nom" version = "5.1.2" @@ -271,6 +339,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "recipe-store" version = "0.1.0" @@ -291,6 +369,25 @@ dependencies = [ "uuid", ] +[[package]] +name = "redox_syscall" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + [[package]] name = "regex" version = "1.4.5" @@ -329,6 +426,35 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustyline" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e1b597fcd1eeb1d6b25b493538e5aa19629eb08932184b85fef931ba87e893" +dependencies = [ + "bitflags", + "cfg-if", + "dirs-next", + "fs2", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "smallvec", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "shlex" version = "1.0.0" @@ -352,12 +478,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-width" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "uuid" version = "0.8.2" diff --git a/kitchen/Cargo.toml b/kitchen/Cargo.toml index 998fa55..59c7bbc 100644 --- a/kitchen/Cargo.toml +++ b/kitchen/Cargo.toml @@ -9,3 +9,4 @@ edition = "2018" [dependencies] recipes = {path = "../recipes" } recipe-store = {path = "../recipe-store" } +rustyline = "~8.0.0" diff --git a/recipes/Cargo.toml b/recipes/Cargo.toml index c809120..ac1e4cc 100644 --- a/recipes/Cargo.toml +++ b/recipes/Cargo.toml @@ -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.4" +abortable_parser = "~0.2.5" chrono = "~0.4" [dependencies.uuid] diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index c720366..36d4c0b 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -11,13 +11,16 @@ // 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 parse; pub mod unit; use std::collections::BTreeMap; +use std::str::FromStr; use chrono::NaiveDate; use uuid::{self, Uuid}; +use parse::{ingredient, measure}; use unit::*; #[derive(Debug, Clone, PartialEq)] @@ -201,6 +204,26 @@ impl Ingredient { self.amt.measure_type(), ); } + + pub fn parse(s: &str) -> Result { + Ok(match ingredient(abortable_parser::StrIter::new(s)) { + abortable_parser::Result::Complete(_, ing) => ing, + abortable_parser::Result::Abort(e) | abortable_parser::Result::Fail(e) => { + return Err(format!("Failed to parse as Ingredient {:?}", e)) + } + abortable_parser::Result::Incomplete(_) => { + return Err(format!("Incomplete input: {}", s)) + } + }) + } +} + +impl FromStr for Ingredient { + type Err = String; + + fn from_str(s: &str) -> Result { + Ingredient::parse(s) + } } impl std::fmt::Display for Ingredient { diff --git a/recipes/src/parse.rs b/recipes/src/parse.rs new file mode 100644 index 0000000..734e6fb --- /dev/null +++ b/recipes/src/parse.rs @@ -0,0 +1,190 @@ +// 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::str::FromStr; + +use abortable_parser::{ + ascii_digit, ascii_ws, consume_all, discard, do_each, either, eoi, make_fn, not, optional, + peek, repeat, separated, text_token, trap, until, Result, StrIter, +}; +use num_rational::Ratio; + +use crate::{ + unit::{Measure, Measure::*, Quantity, VolumeMeasure::*}, + Ingredient, +}; + +make_fn!(ws, + consume_all!(either!( + text_token!(" "), + text_token!("\t"), + text_token!("\r") + )) +); + +make_fn!(nonzero, + peek!(not!(do_each!( + n => consume_all!(text_token!("0")), + _ => ws, + (n) + ))) +); + +make_fn!(num, + do_each!( + _ => peek!(ascii_digit), + n => consume_all!(ascii_digit), + (u32::from_str(n).unwrap()) + ) +); + +make_fn!( + pub ratio>, + do_each!( + // First we assert non-zero numerator + //_ => nonzero, + numer => num, + _ => text_token!("/"), + denom => num, + (Ratio::new(numer, denom)) + ) +); + +make_fn!(unit, + do_each!( + u => either!( + text_token!("tsp"), + text_token!("tbsp"), + text_token!("floz"), + text_token!("ml"), + text_token!("ltr"), + text_token!("cup"), + text_token!("qrt"), + text_token!("pint"), + text_token!("pnt"), + text_token!("gal"), + text_token!("gal"), + text_token!("cnt"), + text_token!("g"), + text_token!("gram")), + (u)) +); + +make_fn!( + pub quantity, + either!( + do_each!( + whole => num, + _ => ws, + frac => ratio, + _ => ws, + (Quantity::Whole(whole) + Quantity::Frac(frac)) + ), + do_each!( + frac => ratio, + _ => ws, + (Quantity::Frac(frac)) + ), + do_each!( + whole => num, + _ => ws, + (Quantity::whole(whole)) + ) + ) +); + +make_fn!( + pub measure_parts)>, + do_each!( + qty => quantity, + unit => optional!(do_each!( + _ => ws, + unit => unit, + (unit) + )), + _ => ws, + ((qty, unit)) + ) +); + +pub fn measure(i: StrIter) -> abortable_parser::Result { + match measure_parts(i) { + Result::Complete(i, (qty, unit)) => { + return Result::Complete( + i.clone(), + match unit { + Some("tsp") => Volume(Tsp(qty)), + Some("tbsp") => Volume(Tbsp(qty)), + Some("floz") => Volume(Floz(qty)), + Some("ml") => Volume(ML(qty)), + Some("ltr") | Some("liter") => Volume(Ltr(qty)), + Some("cup") | Some("cp") => Volume(Cup(qty)), + Some("qrt") | Some("quart") => Volume(Qrt(qty)), + Some("pint") | Some("pnt") => Volume(Pint(qty)), + Some("cnt") | Some("count") => Count(qty), + Some("g") => Gram(qty), + Some("gram") => Gram(qty), + Some(u) => { + return Result::Abort(abortable_parser::Error::new( + format!("Invalid Unit {}", u), + Box::new(i), + )) + } + None => Count(qty), + }, + ) + } + Result::Fail(e) => { + return Result::Fail(e); + } + Result::Abort(e) => { + return Result::Abort(e); + } + Result::Incomplete(i) => return Result::Incomplete(i), + } +} + +make_fn!( + pub ingredient_name, + do_each!( + name => until!(ascii_ws), + _ => ws, + (name) + ) +); + +make_fn!( + ingredient_modifier, + do_each!( + _ => text_token!("("), + modifier => until!(text_token!(")")), + _ => text_token!(")"), + (modifier) + ) +); + +make_fn!( + pub ingredient, + do_each!( + _ => optional!(ws), + measure => measure, + name => ingredient_name, + modifier => optional!(ingredient_modifier), + (Ingredient::new(name, modifier.map(|s| s.to_owned()), measure, "")) + ) +); + +make_fn!( + pub ingredient_list>, + separated!(text_token!("\n"), ingredient) +); diff --git a/recipes/src/test.rs b/recipes/src/test.rs index 1f61d21..b4bb13e 100644 --- a/recipes/src/test.rs +++ b/recipes/src/test.rs @@ -16,6 +16,7 @@ use VolumeMeasure::*; use std::convert::Into; +use abortable_parser::{Result as ParseResult, StrIter}; use num_rational::Ratio; #[test] @@ -233,3 +234,107 @@ fn test_ingredient_display() { assert_eq!(format!("{}", i), expected); } } + +use Measure::*; + +#[test] +fn test_ratio_parse() { + if let ParseResult::Complete(_, rat) = parse::ratio(StrIter::new("1/2")) { + assert_eq!(rat, Ratio::new(1, 2)) + } else { + assert!(false) + } +} + +#[test] +fn test_quantity_parse() { + for (i, expected) in vec![ + ("1 ", Quantity::Whole(1)), + ("1/2 ", Quantity::Frac(Ratio::new(1, 2))), + ("1 1/2 ", Quantity::Frac(Ratio::new(3, 2))), + ] { + match parse::quantity(StrIter::new(i)) { + ParseResult::Complete(_, qty) => assert_eq!(qty, expected), + err => assert!(false, "{:?}", err), + } + } +} + +#[test] +fn test_ingredient_name_parse() { + for (i, expected) in vec![("flour ", "flour"), ("flour (", "flour")] { + match parse::ingredient_name(StrIter::new(i)) { + ParseResult::Complete(_, n) => assert_eq!(n, expected), + err => assert!(false, "{:?}", err), + } + } +} + +#[test] +fn test_ingredient_parse() { + for (i, expected) in vec![ + ( + "1 cup flour ", + Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), + ), + ( + "\t1 cup flour ", + Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), + ), + ( + "1 cup apple (chopped)", + Ingredient::new( + "apple", + Some("chopped".to_owned()), + Volume(Cup(Quantity::Whole(1))), + "", + ), + ), + ( + "1 cup apple (chopped) ", + Ingredient::new( + "apple", + Some("chopped".to_owned()), + Volume(Cup(Quantity::Whole(1))), + "", + ), + ), + ] { + match ingredient(StrIter::new(i)) { + ParseResult::Complete(_, ing) => assert_eq!(ing, expected), + err => assert!(false, "{:?}", err), + } + } +} + +#[test] +fn test_ingredient_list_parse() { + for (i, expected) in vec![ + ( + "1 cup flour ", + vec![Ingredient::new( + "flour", + None, + Volume(Cup(Quantity::Whole(1))), + "", + )], + ), + ( + "1 cup flour \n1/2 tsp butter ", + vec![ + Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), + Ingredient::new( + "butter", + None, + Volume(Tsp(Quantity::Frac(Ratio::new(1, 2)))), + "", + ), + ], + ), + ] { + match ingredient_list(StrIter::new(i)) { + ParseResult::Complete(_, ing) => assert_eq!(ing, expected), + err => assert!(false, "{:?}", err), + } + } +} diff --git a/recipes/src/unit.rs b/recipes/src/unit.rs index 4225f0b..596a6fe 100644 --- a/recipes/src/unit.rs +++ b/recipes/src/unit.rs @@ -22,15 +22,15 @@ use std::{ convert::TryFrom, fmt::Display, ops::{Add, Div, Mul, Sub}, - str::FromStr, }; use abortable_parser::{ - self, consume_all, do_each, either, make_fn, not, optional, peek, run, text_token, trap, - Result, StrIter, + consume_all, do_each, either, make_fn, not, optional, peek, text_token, trap, Result, StrIter, }; use num_rational::Ratio; +use crate::parse::measure; + #[derive(Copy, Clone, Debug)] /// Volume Measurements for ingredients in a recipe. pub enum VolumeMeasure { @@ -285,21 +285,7 @@ impl Measure { pub fn parse(input: &str) -> std::result::Result { Ok(match measure(StrIter::new(input)) { - Result::Complete(_, (qty, Some(unit))) => match unit { - "tsp" => Volume(Tsp(qty)), - "tbsp" => Volume(Tbsp(qty)), - "floz" => Volume(Floz(qty)), - "ml" => Volume(ML(qty)), - "ltr" => Volume(Ltr(qty)), - "cup" => Volume(Cup(qty)), - "qrt" => Volume(Qrt(qty)), - "pint" | "pnt" => Volume(Pint(qty)), - "cnt" => Count(qty), - "g" => Gram(qty), - "gram" => Gram(qty), - u => return Err(format!("Invalid unit {}", u)), - }, - Result::Complete(_, (qty, None)) => Count(qty), + Result::Complete(i, measure) => measure, Result::Abort(e) | Result::Fail(e) => { return Err(format!("Failed to parse as Measure {:?}", e)) } @@ -468,98 +454,3 @@ impl Display for Quantity { } } } - -make_fn!(nonzero, - peek!(not!(do_each!( - n => consume_all!(text_token!("0")), - _ => optional!(ws), - (n) - ))) -); - -make_fn!(num, - do_each!( - n => consume_all!(either!( - text_token!("0"), - text_token!("1"), - text_token!("2"), - text_token!("3"), - text_token!("4"), - text_token!("5"), - text_token!("6"), - text_token!("7"), - text_token!("8"), - text_token!("9") - )), - (u32::from_str(n).unwrap()) - ) -); - -make_fn!(ws, - consume_all!(either!( - text_token!(" "), - text_token!("\t") - )) -); - -make_fn!(ratio>, - do_each!( - // First we assert non-zero numerator - _ => nonzero, - numer => num, - _ => optional!(ws), - _ => text_token!("/"), - _ => optional!(ws), - denom => num, - (Ratio::new(numer, denom)) - ) -); - -make_fn!(unit, - do_each!( - u => either!( - text_token!("tsp"), - text_token!("tbsp"), - text_token!("floz"), - text_token!("ml"), - text_token!("ltr"), - text_token!("cup"), - text_token!("qrt"), - text_token!("pint"), - text_token!("pnt"), - text_token!("gal"), - text_token!("gal"), - text_token!("cnt"), - text_token!("g"), - text_token!("gram")), - (u)) -); - -make_fn!( - quantity, - either!( - do_each!( - whole => num, - frac => ratio, - (Quantity::Whole(whole) + Quantity::Frac(frac)) - ), - do_each!( - frac => ratio, - (Quantity::Frac(frac)) - ), - do_each!( - whole => num, - (Quantity::whole(whole)) - ) - ) -); - -make_fn!( - measure)>, - do_each!( - qty => quantity, - _ => optional!(ws), - unit => optional!(unit), - ((qty, unit)) - ) -);