1extern crate pest;
2#[macro_use]
3extern crate pest_derive;
4
5use pest::iterators::Pair;
6use pest::Parser;
7use std::boxed::Box;
8use std::collections::HashMap;
9use std::ops::Add;
10use std::str::FromStr;
11use uuid::Uuid;
12use serde::{Serialize, Deserialize};
13
14#[derive(Parser)]
15#[grammar = "../CookLang.pest"]
16pub struct CookParser;
17
18#[derive(Debug, Serialize, Deserialize)]
19pub struct Recipe {
20 source: String,
21 metadata: Metadata,
22 instruction: String,
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26pub struct Metadata {
27 servings: Option<Vec<usize>>,
28 ominous: HashMap<String, String>,
29 ingredients: HashMap<String, Ingredient>,
30 ingredients_specifiers: Vec<IngredientSpecifier>,
31 cookware: Vec<String>,
32 timer: Vec<Timer>,
33}
34
35impl Metadata {
36 pub fn add_key_value(&mut self, key: String, value: String) {
37 self.ominous.insert(key, value);
38 }
39}
40
41#[derive(Debug, Serialize, Deserialize)]
42pub struct Timer {
43 amount: f64,
44 unit: String,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct IngredientSpecifier {
49 ingredient: String,
50 amount_in_step: Amount,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54pub struct Ingredient {
55 name: String,
56 id: Uuid,
57 amount: Option<Amount>,
58 unit: Option<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62enum Amount {
63 Multi(f64),
64 Servings(Vec<f64>),
65 Single(f64),
66}
67
68impl Add for Amount {
69 type Output = Amount;
70
71 fn add(self, rhs: Self) -> Self::Output {
72 match self {
73 Amount::Multi(a) => match rhs {
74 Amount::Multi(b) => Amount::Multi(a + b),
75 _ => {
76 panic!("Unallowed Addition");
77 }
78 },
79 Amount::Servings(a) => match rhs {
80 Amount::Servings(b) => {
81 Amount::Servings(a.iter().zip(b.iter()).map(|e| *e.0 + *e.1).collect())
82 }
83 _ => {
84 panic!("Unallowed Addition");
85 }
86 },
87 Amount::Single(a) => match rhs {
88 Amount::Single(b) => Amount::Single(a + b),
89 _ => {
90 panic!("Unallowed Addition");
91 }
92 },
93 }
94 }
95}
96
97pub fn parse(inp: &str) -> Result<Recipe, Box<dyn std::error::Error>> {
98 let successful_parse: Pair<_> = match CookParser::parse(Rule::cook_lang, inp) {
99 Ok(d) => d,
100 Err(e) => {
101 panic!("{:?}", e);
102 }
103 }
104 .next()
105 .unwrap();
106 let mut metadata = Metadata {
107 servings: None,
108 ominous: Default::default(),
109 ingredients: HashMap::new(),
110 ingredients_specifiers: vec![],
111 cookware: vec![],
112 timer: vec![],
113 };
114 let source = successful_parse.as_str().to_string();
115 let mut source_edited = source.clone();
116 let metadata_line_iterator = successful_parse.clone().into_inner();
117 metadata_line_iterator.for_each(|e| {
118 if e.as_rule() == Rule::metadata {
119 e.into_inner().for_each(|property| {
120 let mut key_value_iterator = property.into_inner();
121 let name = key_value_iterator.next().unwrap().as_str();
122
123 if name != "servings" {
124 let value = key_value_iterator.next().unwrap().as_str();
125 metadata.add_key_value(name.to_string(), value.to_string());
126 } else {
127 let mut servings = Vec::with_capacity(3);
128 key_value_iterator
129 .next()
130 .unwrap()
131 .into_inner()
132 .for_each(|serving| {
133 if serving.as_str() != "|" {
135 let serving_number = usize::from_str(serving.as_str())
136 .expect("Parsing of serving number failed");
137 servings.push(serving_number);
138 }
139 });
140 metadata.servings = Some(servings);
141 }
142 });
143 } else {
144 let _line = e.as_str().to_string().clone();
146 e.into_inner().for_each(|ingredients_cookware| {
147 if ingredients_cookware.as_rule() == Rule::ingredient {
149 source_edited = source_edited.replace(ingredients_cookware.as_str(), "@");
150 let mut name = String::new();
152 let mut ingredient_amount = None;
153 let mut ingredient_modified = None;
154 let mut ingredient_unit = None;
155 ingredients_cookware
156 .into_inner()
157 .for_each(|ingredient_property| {
158 match ingredient_property.as_rule() {
160 Rule::name => {
161 name.push_str(ingredient_property.as_str());
162 name.push(' ');
163 }
164 Rule::text => {
165 name.push_str(ingredient_property.as_str());
166 name.push(' ');
167 }
168 Rule::number => {
169 ingredient_property.into_inner().for_each(
170 |ingredient_amount_inner| match ingredient_amount.clone() {
171 None => {
172 ingredient_amount = Some(Amount::Single(
173 usize::from_str(
174 ingredient_amount_inner.as_str(),
175 )
176 .expect("Failed to parse ingredient amount")
177 as f64,
178 ))
179 }
180 Some(d) => {
181 let data_point = usize::from_str(
182 ingredient_amount_inner.as_str(),
183 )
184 .expect("Failed to parse ingredient amount")
185 as f64;
186 let ingredient_amount_raw = match d {
187 Amount::Multi(_) => {
188 panic!("This isn't allowed with multiply.")
189 }
190 Amount::Servings(dd) => {
191 let mut res = dd.clone();
192 let last = res.len() - 1;
194 if res.get(last).unwrap().clone() == 0.0 {
195 let reference =
196 res.get_mut(last).unwrap();
197 *reference = data_point;
198 } else {
199 let dat = res.pop().unwrap();
200 res.push(dat / data_point);
201 }
202 Amount::Servings(res)
204 }
205 Amount::Single(d) => {
206 Amount::Single(d / data_point)
207 }
208 };
209 ingredient_amount = Some(ingredient_amount_raw);
210 }
211 },
212 );
213 }
214 Rule::ingredient_separator => match ingredient_amount.clone() {
215 None => {
216 panic!("This shouldn't have happened.");
217 }
218 Some(d) => match d {
219 Amount::Multi(_) => {
220 panic!("This shouldn't have happened.")
221 }
222 Amount::Servings(dd) => {
223 let mut res = dd.clone();
224 res.push(0.0);
225 ingredient_amount = Some(Amount::Servings(res));
226 }
227 Amount::Single(dd) => {
228 ingredient_amount =
229 Some(Amount::Servings(vec![dd, 0.0]));
230 }
231 },
232 },
233 Rule::modified => {
234 let modified = ingredient_property
235 .into_inner()
236 .next()
237 .unwrap()
238 .as_str()
239 .to_string();
240 ingredient_modified = Some(modified);
241 }
242 Rule::unit => {
243 ingredient_unit = Some(ingredient_property.as_str().to_string())
244 }
245 Rule::scaling => {
246 ingredient_amount = match ingredient_amount.clone() {
247 Some(d) => match d {
248 Amount::Single(d) => Some(Amount::Multi(d)),
249 _ => {
250 panic!("This shouldn't have happened.")
251 }
252 },
253 None => {
254 panic!("This shouldn't have happened.")
255 }
256 }
257 }
258 _ => {
259 panic!("That should have happened")
260 }
261 }
262 });
263 if name.len() > 0 {
264 name.pop();
265 }
266 let ingredient_specifier = IngredientSpecifier {
267 ingredient: name.clone(),
268 amount_in_step: match ingredient_amount.clone() {
269 None => Amount::Single(0.0),
270 Some(d) => d,
271 },
272 };
273 metadata
274 .ingredients_specifiers
275 .push(ingredient_specifier.clone());
276 if metadata.ingredients.contains_key(&name) {
277 let mut ingredient = metadata.ingredients.get_mut(&name).unwrap();
278 match ingredient_amount.clone() {
279 None => {}
280 Some(amount) => {
281 ingredient.amount =
282 Some(ingredient.amount.as_ref().unwrap().clone() + amount);
283 }
284 }
285 if ingredient.unit != ingredient_unit {
286 panic!("Amount of ingredient is inconsistent.")
287 }
288 ingredient.unit = ingredient_unit;
289 } else {
290 let ingredient = Ingredient {
291 name: name.clone(),
292 id: Uuid::new_v4(),
293 amount: ingredient_amount,
294 unit: ingredient_unit,
295 };
296 metadata.ingredients.insert(name.clone(), ingredient);
297 }
298 } else if ingredients_cookware.as_rule() == Rule::cookware {
300 source_edited = source_edited.replace(ingredients_cookware.as_str(), "#");
301 let mut name = String::new();
303 ingredients_cookware
304 .into_inner()
305 .for_each(|cookware_property| {
306 name.push_str(cookware_property.as_str());
308 name.push(' ');
309 });
310 name.pop().unwrap();
311 metadata.cookware.push(name);
313 } else if ingredients_cookware.as_rule() == Rule::timer {
314 source_edited = source_edited.replace(ingredients_cookware.as_str(), "~");
315 let mut timer = Timer {
317 amount: 0.0,
318 unit: "".to_string(),
319 };
320 ingredients_cookware
321 .into_inner()
322 .for_each(|timer_property| {
323 if timer_property.as_rule() == Rule::number {
325 let amount = usize::from_str(timer_property.as_str())
326 .expect("Unaple to parse timer duration")
327 as f64;
328 timer.amount = amount;
329 } else {
330 let unit = timer_property.as_str().to_string();
331 timer.unit = unit;
332 }
333 });
334 metadata.timer.push(timer);
335 }
336 })
337 }
338 });
339 let recipe = Recipe {
343 source,
344 metadata,
345 instruction: source_edited
346 };
347 Ok(recipe)
348
349}
350
351#[cfg(test)]
352mod tests {
353 use crate::parse;
354 use std::fs::read_to_string;
355
356 #[test]
357 fn it_works() {
358 let test_rec = String::from(
359 "\
360>> value: key // This is a comment\n\
361// A comment line\n\
362>> servings: 1|2|3\n\
363Get some @fruit salat ananas{1/2*}(washed) and pull it\n\
364Use the #big potato masher{}\n\
365Start the timer ~{10%minutes}\n\
366",
367 );
368
369 let _recipe = parse(&test_rec).unwrap();
370 }
371
372 #[test]
373 fn coffee_souffle() {
374 let test_rec = read_to_string("spec/examples/Coffee Souffle.cook").unwrap();
375 parse(&test_rec).unwrap();
376 }
377
378 #[test]
379 fn easy_pancakes() {
380 let test_rec = read_to_string("spec/examples/Easy Pancakes.cook").unwrap();
381 parse(&test_rec).unwrap();
382 }
383
384 #[test]
385 fn fried_rice() {
386 let test_rec = read_to_string("spec/examples/Fried Rice.cook").unwrap();
387 parse(&test_rec).unwrap();
388 }
389
390 #[test]
391 fn olivier_salad() {
392 let test_rec = read_to_string("spec/examples/Olivier Salad.cook").unwrap();
393 parse(&test_rec).unwrap();
394 }
395}