cook_with_rust_parser/
lib.rs

1//! This is a parser crate for the [CookLang](https://github.com/cooklang/spec).  The main feature is parsing a String into a
2//! struct that implements serde and can be easily used from there.
3//!
4//! The implementation is nearly fully complete. Only image tags are missing. They are just ignored by now.
5//!
6
7extern crate pest;
8#[macro_use]
9extern crate pest_derive;
10
11use pest::iterators::Pair;
12use pest::Parser;
13use std::boxed::Box;
14use std::collections::HashMap;
15use std::ops::Add;
16use std::str::FromStr;
17use uuid::Uuid;
18use serde::{Serialize, Deserialize};
19
20#[derive(Parser)]
21#[grammar = "../CookLang.pest"]
22struct CookParser;
23
24/// Includes the raw source, metadata and instructions.
25#[derive(Debug, Serialize, Deserialize)]
26pub struct Recipe {
27    /// Raw source code of the recipe that this struct has been generated from.
28    pub source: String,
29    /// Contains the metadata of the recipe. Provided in the form of [Metadata].
30    pub metadata: Metadata,
31    /// Contains reduced instructions.
32    ///
33    /// For every mentioning of a ingredient there is an @ in replacement. The mentioning directly
34    /// links to an [IngredientSpecifier].
35    ///
36    /// For every mentioning of a cookware there is an # in replacement. The mentioning directly
37    /// links to a [String] describing the cookware.
38    ///
39    /// For every mentioning of a timer there is an ~ in replacement. The mentioning directly links
40    /// to a [Timer].
41    pub instruction: String,
42}
43
44/// The metadata from the recipe is described in this metadata struct.
45#[derive(Debug, Serialize, Deserialize)]
46pub struct Metadata {
47    /// Amount of servings. Is optional.
48    pub servings: Option<Vec<usize>>,
49    /// Other optional metadata contained in a [HashMap].
50    pub ominous: HashMap<String, String>,
51    /// Exact description of an [Ingredient] indexed by name.
52    pub ingredients: HashMap<String, Ingredient>,
53    /// Ingredient Specifier describing the mentioning of a [Ingredient]. The n-th mention of @
54    /// in [Recipe::instruction] is the n-th [IngredientSpecifier] in this [Vec].
55    pub ingredients_specifiers: Vec<IngredientSpecifier>,
56    /// The n-th mention of # in [Recipe::instruction] is the n-th [String] in this [Vec].
57    pub cookware: Vec<String>,
58    /// The n-th mention of ~ in [Recipe::instruction] is the n-th [Timer] in this [Vec].
59    pub timer: Vec<Timer>,
60}
61
62impl Metadata {
63    fn add_key_value(&mut self, key: String, value: String) {
64        self.ominous.insert(key, value);
65    }
66}
67/// A Timer.
68///
69/// Describing the timer you have to set in this mentioning in the instructions.
70#[derive(Debug, Serialize, Deserialize)]
71pub struct Timer {
72    /// The number of [Timer::unit]s in this Timer mentioning.
73    pub amount: f64,
74    /// The unit of this Timer contained in a [String].
75    pub unit: String,
76}
77
78/// IngredientSpecifier
79///
80/// References to a [Ingredient] in [Metadata::ingredients] by [String].
81#[derive(Debug, Clone, Deserialize, Serialize)]
82pub struct IngredientSpecifier {
83    /// Name of the ingredient this specifier references to. Have to be extracted from [Metadata::ingredients].
84    pub ingredient: String,
85    /// [Amount] to be used in this step.
86    pub amount_in_step: Amount,
87}
88
89#[derive(Debug, Serialize, Deserialize)]
90pub struct Ingredient {
91    /// Name of the ingredient.
92    pub name: String,
93    /// Uuid is currently not used.
94    pub id: Uuid,
95    /// Optional [Amount] specifier.
96    pub amount: Option<Amount>,
97    /// Unit this ingredient is measured in.
98    pub unit: Option<String>,
99}
100
101/// Specifies the amount of a [Ingredient].
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub enum Amount {
104    /// Scalable amount.
105    ///
106    /// To get the needed amount in the step or total needed amount [Amount::Multi::0] has to be
107    /// multiplied by the servings.
108    Multi(f64),
109    /// Static Servings amount.
110    Servings(Vec<f64>),
111    /// Static amount.
112    Single(f64),
113}
114
115impl Add for Amount {
116    type Output = Amount;
117
118    fn add(self, rhs: Self) -> Self::Output {
119        match self {
120            Amount::Multi(a) => match rhs {
121                Amount::Multi(b) => Amount::Multi(a + b),
122                _ => {
123                    panic!("Unallowed Addition");
124                }
125            },
126            Amount::Servings(a) => match rhs {
127                Amount::Servings(b) => {
128                    Amount::Servings(a.iter().zip(b.iter()).map(|e| *e.0 + *e.1).collect())
129                }
130                _ => {
131                    panic!("Unallowed Addition");
132                }
133            },
134            Amount::Single(a) => match rhs {
135                Amount::Single(b) => Amount::Single(a + b),
136                _ => {
137                    panic!("Unallowed Addition");
138                }
139            },
140        }
141    }
142}
143
144
145/// Parse the input into a [Recipe].
146pub fn parse(inp: &str) -> Result<Recipe, Box<dyn std::error::Error>> {
147    let successful_parse: Pair<_> = match CookParser::parse(Rule::cook_lang, inp) {
148        Ok(d) => d,
149        Err(e) => {
150            panic!("{:?}", e);
151        }
152    }
153    .next()
154    .unwrap();
155    let mut metadata = Metadata {
156        servings: None,
157        ominous: Default::default(),
158        ingredients: HashMap::new(),
159        ingredients_specifiers: vec![],
160        cookware: vec![],
161        timer: vec![],
162    };
163    let source = successful_parse.as_str().to_string();
164    let mut source_edited = source.clone();
165    let metadata_line_iterator = successful_parse.clone().into_inner();
166    metadata_line_iterator.for_each(|e| {
167        if e.as_rule() == Rule::metadata {
168            e.into_inner().for_each(|property| {
169                let mut key_value_iterator = property.into_inner();
170                let name = key_value_iterator.next().unwrap().as_str();
171
172                if name != "servings" {
173                    let value = key_value_iterator.next().unwrap().as_str();
174                    metadata.add_key_value(name.to_string(), value.to_string());
175                } else {
176                    let mut servings = Vec::with_capacity(3);
177                    key_value_iterator
178                        .next()
179                        .unwrap()
180                        .into_inner()
181                        .for_each(|serving| {
182                            // println!("Serving => {:?}", serving);
183                            if serving.as_str() != "|" {
184                                let serving_number = usize::from_str(serving.as_str())
185                                    .expect("Parsing of serving number failed");
186                                servings.push(serving_number);
187                            }
188                        });
189                    metadata.servings = Some(servings);
190                }
191            });
192        } else if e.as_rule() == Rule::comment {
193            println!("Replacing comment = {}", e.as_str());
194            source_edited = source_edited.replace(e.as_str(), "");
195
196        } else {
197            // println!("Line => {:?}", e);
198            let _line = e.as_str().to_string().clone();
199            e.into_inner().for_each(|ingredients_cookware| {
200                // println!("Ingredient / Cookware => {:?}", ingredients_cookware);
201                if ingredients_cookware.as_rule() == Rule::ingredient {
202                    source_edited = source_edited.replace(ingredients_cookware.as_str(), "@");
203                    // println!("Ingredient => {:?}", ingredients_cookware);
204                    let mut name = String::new();
205                    let mut ingredient_amount = None;
206                    let mut ingredient_modified = None;
207                    let mut ingredient_unit = None;
208                    ingredients_cookware
209                        .into_inner()
210                        .for_each(|ingredient_property| {
211                            // println!("Ingredient Property => {:?}", ingredient_property);
212                            match ingredient_property.as_rule() {
213                                Rule::name => {
214                                    name.push_str(ingredient_property.as_str());
215                                    name.push(' ');
216                                }
217                                Rule::text => {
218                                    name.push_str(ingredient_property.as_str());
219                                    name.push(' ');
220                                }
221                                Rule::number => {
222                                    ingredient_property.into_inner().for_each(
223                                        |ingredient_amount_inner| match ingredient_amount.clone() {
224                                            None => {
225                                                ingredient_amount = Some(Amount::Single(
226                                                    usize::from_str(
227                                                        ingredient_amount_inner.as_str(),
228                                                    )
229                                                    .expect("Failed to parse ingredient amount")
230                                                        as f64,
231                                                ))
232                                            }
233                                            Some(d) => {
234                                                let data_point = usize::from_str(
235                                                    ingredient_amount_inner.as_str(),
236                                                )
237                                                .expect("Failed to parse ingredient amount")
238                                                    as f64;
239                                                let ingredient_amount_raw = match d {
240                                                    Amount::Multi(_) => {
241                                                        panic!("This isn't allowed with multiply.")
242                                                    }
243                                                    Amount::Servings(dd) => {
244                                                        let mut res = dd.clone();
245                                                        // println!("Res => {:?}", res);
246                                                        let last = res.len() - 1;
247                                                        if res.get(last).unwrap().clone() == 0.0 {
248                                                            let reference =
249                                                                res.get_mut(last).unwrap();
250                                                            *reference = data_point;
251                                                        } else {
252                                                            let dat = res.pop().unwrap();
253                                                            res.push(dat / data_point);
254                                                        }
255                                                        // println!("Res => {:?}", res);
256                                                        Amount::Servings(res)
257                                                    }
258                                                    Amount::Single(d) => {
259                                                        Amount::Single(d / data_point)
260                                                    }
261                                                };
262                                                ingredient_amount = Some(ingredient_amount_raw);
263                                            }
264                                        },
265                                    );
266                                }
267                                Rule::ingredient_separator => match ingredient_amount.clone() {
268                                    None => {
269                                        panic!("This shouldn't have happened.");
270                                    }
271                                    Some(d) => match d {
272                                        Amount::Multi(_) => {
273                                            panic!("This shouldn't have happened.")
274                                        }
275                                        Amount::Servings(dd) => {
276                                            let mut res = dd.clone();
277                                            res.push(0.0);
278                                            ingredient_amount = Some(Amount::Servings(res));
279                                        }
280                                        Amount::Single(dd) => {
281                                            ingredient_amount =
282                                                Some(Amount::Servings(vec![dd, 0.0]));
283                                        }
284                                    },
285                                },
286                                Rule::modified => {
287                                    let modified = ingredient_property
288                                        .into_inner()
289                                        .next()
290                                        .unwrap()
291                                        .as_str()
292                                        .to_string();
293                                    ingredient_modified = Some(modified);
294                                }
295                                Rule::unit => {
296                                    ingredient_unit = Some(ingredient_property.as_str().to_string())
297                                }
298                                Rule::scaling => {
299                                    ingredient_amount = match ingredient_amount.clone() {
300                                        Some(d) => match d {
301                                            Amount::Single(d) => Some(Amount::Multi(d)),
302                                            _ => {
303                                                panic!("This shouldn't have happened.")
304                                            }
305                                        },
306                                        None => {
307                                            panic!("This shouldn't have happened.")
308                                        }
309                                    }
310                                }
311                                _ => {
312                                    panic!("That should have happened")
313                                }
314                            }
315                        });
316                    if name.len() > 0 {
317                        name.pop();
318                    }
319                    let ingredient_specifier = IngredientSpecifier {
320                        ingredient: name.clone(),
321                        amount_in_step: match ingredient_amount.clone() {
322                            None => Amount::Single(0.0),
323                            Some(d) => d,
324                        },
325                    };
326                    metadata
327                        .ingredients_specifiers
328                        .push(ingredient_specifier.clone());
329                    if metadata.ingredients.contains_key(&name) {
330                        let mut ingredient = metadata.ingredients.get_mut(&name).unwrap();
331                        match ingredient_amount.clone() {
332                            None => {}
333                            Some(amount) => {
334                                ingredient.amount =
335                                    Some(ingredient.amount.as_ref().unwrap().clone() + amount);
336                            }
337                        }
338                        if ingredient.unit != ingredient_unit {
339                            panic!("Amount of ingredient is inconsistent.")
340                        }
341                        ingredient.unit = ingredient_unit;
342                    } else {
343                        let ingredient = Ingredient {
344                            name: name.clone(),
345                            id: Uuid::new_v4(),
346                            amount: ingredient_amount,
347                            unit: ingredient_unit,
348                        };
349                        metadata.ingredients.insert(name.clone(), ingredient);
350                    }
351                    // println!("Name => {}", name);
352                } else if ingredients_cookware.as_rule() == Rule::cookware {
353                    source_edited = source_edited.replace(ingredients_cookware.as_str(), "#");
354                    // println!("Cookware => {:?}", ingredients_cookware);
355                    let mut name = String::new();
356                    ingredients_cookware
357                        .into_inner()
358                        .for_each(|cookware_property| {
359                            // println!("Cookware Property => {:?}", cookware_property);
360                            name.push_str(cookware_property.as_str());
361                            name.push(' ');
362                        });
363                    name.pop().unwrap();
364                    // println!("Name => {}", name);
365                    metadata.cookware.push(name);
366                } else if ingredients_cookware.as_rule() == Rule::timer {
367                    source_edited = source_edited.replace(ingredients_cookware.as_str(), "~");
368                    // println!("Timer => {:?}", ingredients_cookware);
369                    let mut timer = Timer {
370                        amount: 0.0,
371                        unit: "".to_string(),
372                    };
373                    ingredients_cookware
374                        .into_inner()
375                        .for_each(|timer_property| {
376                            // println!("Timer Property => {:?}", timer_property);
377                            if timer_property.as_rule() == Rule::number {
378                                let amount = usize::from_str(timer_property.as_str())
379                                    .expect("Unaple to parse timer duration")
380                                    as f64;
381                                timer.amount = amount;
382                            } else {
383                                let unit = timer_property.as_str().to_string();
384                                timer.unit = unit;
385                            }
386                        });
387                    metadata.timer.push(timer);
388                } else if ingredients_cookware.as_rule() == Rule::comment {
389                    println!("Replacing comment {}", ingredients_cookware.as_str());
390                    source_edited = source_edited.replace(ingredients_cookware.as_str(), "");
391                }
392            })
393        }
394    });
395    // println!("{:#?}", successful_parse);
396    // println!("Source edited: {}", source_edited);
397    // println!("{:#?}", metadata);
398    let recipe = Recipe {
399        source,
400        metadata,
401        instruction: source_edited
402    };
403    Ok(recipe)
404
405}
406
407#[cfg(test)]
408mod tests {
409    use crate::parse;
410    use std::fs::read_to_string;
411
412    #[test]
413    fn it_works() {
414        let test_rec = String::from(
415            "\
416>> value: key // This is a comment\n\
417// A comment line\n\
418>> servings: 1|2|3\n\
419Get some @fruit salat ananas{1/2*}(washed) and pull it\n\
420Use the #big potato masher{}\n\
421Start the timer ~{10%minutes}\n\
422",
423        );
424
425        let _recipe = parse(&test_rec).unwrap();
426    }
427
428    #[test]
429    fn coffee_souffle() {
430        let test_rec = read_to_string("../spec/examples/Coffee Souffle.cook").unwrap();
431        parse(&test_rec).unwrap();
432    }
433
434    #[test]
435    fn easy_pancakes() {
436        let test_rec = read_to_string("../spec/examples/Easy Pancakes.cook").unwrap();
437        parse(&test_rec).unwrap();
438    }
439
440    #[test]
441    fn fried_rice() {
442        let test_rec = read_to_string("../spec/examples/Fried Rice.cook").unwrap();
443        parse(&test_rec).unwrap();
444    }
445
446    #[test]
447    fn olivier_salad() {
448        let test_rec = read_to_string("../spec/examples/Olivier Salad.cook").unwrap();
449        parse(&test_rec).unwrap();
450    }
451}