cook_with_rust/
lib.rs

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                            // println!("Serving => {:?}", serving);
134                            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            // println!("Line => {:?}", e);
145            let _line = e.as_str().to_string().clone();
146            e.into_inner().for_each(|ingredients_cookware| {
147                // println!("Ingredient / Cookware => {:?}", ingredients_cookware);
148                if ingredients_cookware.as_rule() == Rule::ingredient {
149                    source_edited = source_edited.replace(ingredients_cookware.as_str(), "@");
150                    // println!("Ingredient => {:?}", ingredients_cookware);
151                    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                            // println!("Ingredient Property => {:?}", ingredient_property);
159                            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                                                        // println!("Res => {:?}", res);
193                                                        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                                                        // println!("Res => {:?}", res);
203                                                        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                    // println!("Name => {}", name);
299                } else if ingredients_cookware.as_rule() == Rule::cookware {
300                    source_edited = source_edited.replace(ingredients_cookware.as_str(), "#");
301                    // println!("Cookware => {:?}", ingredients_cookware);
302                    let mut name = String::new();
303                    ingredients_cookware
304                        .into_inner()
305                        .for_each(|cookware_property| {
306                            // println!("Cookware Property => {:?}", cookware_property);
307                            name.push_str(cookware_property.as_str());
308                            name.push(' ');
309                        });
310                    name.pop().unwrap();
311                    // println!("Name => {}", name);
312                    metadata.cookware.push(name);
313                } else if ingredients_cookware.as_rule() == Rule::timer {
314                    source_edited = source_edited.replace(ingredients_cookware.as_str(), "~");
315                    // println!("Timer => {:?}", ingredients_cookware);
316                    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                            // println!("Timer Property => {:?}", timer_property);
324                            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    // println!("{:#?}", successful_parse);
340    // println!("Source edited: {}", source_edited);
341    // println!("{:#?}", metadata);
342    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}