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}