1use std::collections::HashMap;
2
3use cooklang::model::Item as OriginalItem;
4use cooklang::quantity::{
5 Quantity as OriginalQuantity, ScalableValue as OriginalScalableValue, Value as OriginalValue,
6};
7use cooklang::ScalableRecipe as OriginalRecipe;
8
9#[derive(uniffi::Record, Debug)]
10pub struct CooklangRecipe {
11 pub metadata: HashMap<String, String>,
12 pub sections: Vec<Section>,
13 pub ingredients: Vec<Ingredient>,
14 pub cookware: Vec<Cookware>,
15 pub timers: Vec<Timer>,
16}
17pub type ComponentRef = u32;
18
19#[derive(uniffi::Record, Debug)]
20pub struct Section {
21 pub title: Option<String>,
22 pub blocks: Vec<Block>,
23 pub ingredient_refs: Vec<ComponentRef>,
24 pub cookware_refs: Vec<ComponentRef>,
25 pub timer_refs: Vec<ComponentRef>,
26}
27
28#[derive(uniffi::Enum, Debug)]
29pub enum Block {
30 StepBlock(Step),
31 NoteBlock(BlockNote),
32}
33
34#[derive(uniffi::Enum, Debug, PartialEq)]
35pub enum Component {
36 IngredientComponent(Ingredient),
37 CookwareComponent(Cookware),
38 TimerComponent(Timer),
39 TextComponent(String),
40}
41
42#[derive(uniffi::Record, Debug)]
43pub struct Step {
44 pub items: Vec<Item>,
45 pub ingredient_refs: Vec<ComponentRef>,
46 pub cookware_refs: Vec<ComponentRef>,
47 pub timer_refs: Vec<ComponentRef>,
48}
49
50#[derive(uniffi::Record, Debug)]
51pub struct BlockNote {
52 pub text: String,
53}
54
55#[derive(uniffi::Record, Debug, PartialEq, Clone)]
56pub struct Ingredient {
57 pub name: String,
58 pub amount: Option<Amount>,
59 pub descriptor: Option<String>,
60}
61
62#[derive(uniffi::Record, Debug, PartialEq, Clone)]
63pub struct Cookware {
64 pub name: String,
65 pub amount: Option<Amount>,
66}
67
68#[derive(uniffi::Record, Debug, PartialEq, Clone)]
69pub struct Timer {
70 pub name: Option<String>,
71 pub amount: Option<Amount>,
72}
73
74#[derive(uniffi::Enum, Debug, Clone, PartialEq)]
75pub enum Item {
76 Text { value: String },
77 IngredientRef { index: ComponentRef },
78 CookwareRef { index: ComponentRef },
79 TimerRef { index: ComponentRef },
80}
81
82pub type IngredientList = HashMap<String, GroupedQuantity>;
83
84pub(crate) fn into_group_quantity(amount: &Option<Amount>) -> GroupedQuantity {
85 let empty_units = "".to_string();
102
103 let key = if let Some(amount) = amount {
104 let units = amount.units.as_ref().unwrap_or(&empty_units);
105
106 match &amount.quantity {
107 Value::Number { .. } => GroupedQuantityKey {
108 name: units.to_string(),
109 unit_type: QuantityType::Number,
110 },
111 Value::Range { .. } => GroupedQuantityKey {
112 name: units.to_string(),
113 unit_type: QuantityType::Range,
114 },
115 Value::Text { .. } => GroupedQuantityKey {
116 name: units.to_string(),
117 unit_type: QuantityType::Text,
118 },
119 Value::Empty => GroupedQuantityKey {
120 name: units.to_string(),
121 unit_type: QuantityType::Empty,
122 },
123 }
124 } else {
125 GroupedQuantityKey {
126 name: empty_units,
127 unit_type: QuantityType::Empty,
128 }
129 };
130
131 let value = if let Some(amount) = amount {
132 amount.quantity.clone()
133 } else {
134 Value::Empty
135 };
136
137 GroupedQuantity::from([(key, value)])
138}
139
140#[derive(uniffi::Enum, Debug, Clone, Hash, Eq, PartialEq)]
141pub enum QuantityType {
142 Number,
143 Range, Text,
145 Empty,
146}
147
148#[derive(uniffi::Record, Debug, Clone, Hash, Eq, PartialEq)]
149pub struct GroupedQuantityKey {
150 pub name: String,
151 pub unit_type: QuantityType,
152}
153
154pub type GroupedQuantity = HashMap<GroupedQuantityKey, Value>;
155
156#[derive(uniffi::Record, Debug, Clone, PartialEq)]
157pub struct Amount {
158 pub(crate) quantity: Value,
159 pub(crate) units: Option<String>,
160}
161
162#[derive(uniffi::Enum, Debug, Clone, PartialEq)]
163pub enum Value {
164 Number { value: f64 },
165 Range { start: f64, end: f64 },
166 Text { value: String },
167 Empty,
168}
169
170pub type CooklangMetadata = HashMap<String, String>;
172
173trait Amountable {
174 fn extract_amount(&self) -> Amount;
175}
176
177impl Amountable for OriginalQuantity<OriginalScalableValue> {
178 fn extract_amount(&self) -> Amount {
179 let quantity = extract_quantity(&self.value);
180
181 let units = self.unit().as_ref().map(|u| u.to_string());
182
183 Amount { quantity, units }
184 }
185}
186
187impl Amountable for OriginalScalableValue {
188 fn extract_amount(&self) -> Amount {
189 let quantity = extract_quantity(self);
190
191 Amount {
192 quantity,
193 units: None,
194 }
195 }
196}
197
198fn extract_quantity(value: &OriginalScalableValue) -> Value {
199 match value {
200 OriginalScalableValue::Fixed(value) => extract_value(value),
201 OriginalScalableValue::Linear(value) => extract_value(value),
202 OriginalScalableValue::ByServings(values) => extract_value(values.first().unwrap()),
203 }
204}
205
206fn extract_value(value: &OriginalValue) -> Value {
207 match value {
208 OriginalValue::Number(num) => Value::Number { value: num.value() },
209 OriginalValue::Range { start, end } => Value::Range {
210 start: start.value(),
211 end: end.value(),
212 },
213 OriginalValue::Text(value) => Value::Text {
214 value: value.to_string(),
215 },
216 }
217}
218
219pub fn expand_with_ingredients(
220 ingredients: &[Ingredient],
221 base: &mut IngredientList,
222 addition: &Vec<ComponentRef>,
223) {
224 for index in addition {
225 let ingredient = ingredients.get(*index as usize).unwrap().clone();
226 let quantity = into_group_quantity(&ingredient.amount);
227 add_to_ingredient_list(base, &ingredient.name, &quantity);
228 }
229}
230
231fn add_to_ingredient_list(
233 list: &mut IngredientList,
234 name: &String,
235 quantity_to_add: &GroupedQuantity,
236) {
237 if let Some(quantity) = list.get_mut(name) {
238 merge_grouped_quantities(quantity, quantity_to_add);
239 } else {
240 list.insert(name.to_string(), quantity_to_add.clone());
241 }
242}
243
244pub fn merge_ingredient_lists(left: &mut IngredientList, right: &IngredientList) {
246 right
247 .iter()
248 .for_each(|(ingredient_name, grouped_quantity)| {
249 let quantity = left.entry(ingredient_name.to_string()).or_default();
250
251 merge_grouped_quantities(quantity, grouped_quantity);
252 });
253}
254
255pub(crate) fn merge_grouped_quantities(left: &mut GroupedQuantity, right: &GroupedQuantity) {
258 right.iter().for_each(|(key, value)| {
276 left.entry(key.clone()) .and_modify(|v| {
278 match key.unit_type {
279 QuantityType::Number => {
280 let Value::Number { value: assignable } = value else {
281 panic!("Unexpected type")
282 };
283 let Value::Number { value: stored } = v else {
284 panic!("Unexpected type")
285 };
286
287 *stored += assignable
288 }
289 QuantityType::Range => {
290 let Value::Range { start, end } = value else {
291 panic!("Unexpected type")
292 };
293 let Value::Range { start: s, end: e } = v else {
294 panic!("Unexpected type")
295 };
296
297 *s += start;
299 *e += end;
300 }
301 QuantityType::Text => {
302 let Value::Text {
303 value: ref assignable,
304 } = value
305 else {
306 panic!("Unexpected type")
307 };
308 let Value::Text { value: stored } = v else {
309 panic!("Unexpected type")
310 };
311
312 *stored += assignable;
313 }
314 QuantityType::Empty => {} }
316 })
317 .or_insert(value.clone());
318 });
319}
320
321pub(crate) fn into_item(item: &OriginalItem) -> Item {
322 match item {
323 OriginalItem::Text { value } => Item::Text {
324 value: value.to_string(),
325 },
326 OriginalItem::Ingredient { index } => Item::IngredientRef {
327 index: *index as u32,
328 },
329 OriginalItem::Cookware { index } => Item::CookwareRef {
330 index: *index as u32,
331 },
332 OriginalItem::Timer { index } => Item::TimerRef {
333 index: *index as u32,
334 },
335 OriginalItem::InlineQuantity { index: _ } => Item::Text {
337 value: "".to_string(),
338 },
339 }
340}
341
342pub(crate) fn into_simple_recipe(recipe: &OriginalRecipe) -> CooklangRecipe {
343 let mut metadata = CooklangMetadata::new();
344 let ingredients: Vec<Ingredient> = recipe.ingredients.iter().map(|i| i.into()).collect();
345 let cookware: Vec<Cookware> = recipe.cookware.iter().map(|i| i.into()).collect();
346 let timers: Vec<Timer> = recipe.timers.iter().map(|i| i.into()).collect();
347 let mut sections: Vec<Section> = Vec::new();
348
349 for section in &recipe.sections {
351 let mut blocks: Vec<Block> = Vec::new();
352
353 let mut ingredient_refs: Vec<u32> = Vec::new();
354 let mut cookware_refs: Vec<u32> = Vec::new();
355 let mut timer_refs: Vec<u32> = Vec::new();
356
357 for content in §ion.content {
359 match content {
360 cooklang::Content::Step(step) => {
361 let mut step_ingredient_refs: Vec<u32> = Vec::new();
362 let mut step_cookware_refs: Vec<u32> = Vec::new();
363 let mut step_timer_refs: Vec<u32> = Vec::new();
364
365 let mut items: Vec<Item> = Vec::new();
366 for item in &step.items {
368 let item = into_item(item);
369
370 match &item {
372 Item::IngredientRef { index } => {
373 step_ingredient_refs.push(*index);
374 }
375 Item::CookwareRef { index } => {
376 step_cookware_refs.push(*index);
377 }
378 Item::TimerRef { index } => {
379 step_timer_refs.push(*index);
380 }
381 _ => (),
382 };
383 items.push(item);
384 }
385 blocks.push(Block::StepBlock(Step {
386 items,
387 ingredient_refs: step_ingredient_refs.clone(),
388 cookware_refs: step_cookware_refs.clone(),
389 timer_refs: step_timer_refs.clone(),
390 }));
391 ingredient_refs.extend(step_ingredient_refs);
392 cookware_refs.extend(step_cookware_refs);
393 timer_refs.extend(step_timer_refs);
394 }
395
396 cooklang::Content::Text(text) => {
397 blocks.push(Block::NoteBlock(BlockNote {
398 text: text.to_string(),
399 }));
400 }
401 }
402 }
403
404 sections.push(Section {
405 title: section.name.clone(),
406 blocks,
407 ingredient_refs,
408 cookware_refs,
409 timer_refs,
410 });
411 }
412
413 for (key, value) in &recipe.metadata.map {
416 if let (Some(key), Some(value)) = (key.as_str(), value.as_str()) {
417 metadata.insert(key.to_string(), value.to_string());
418 }
419 }
420
421 CooklangRecipe {
422 metadata,
423 sections,
424 ingredients,
425 cookware,
426 timers,
427 }
428}
429
430impl From<&cooklang::Ingredient<OriginalScalableValue>> for Ingredient {
431 fn from(ingredient: &cooklang::Ingredient<OriginalScalableValue>) -> Self {
432 Ingredient {
433 name: ingredient.name.clone(),
434 amount: ingredient.quantity.as_ref().map(|q| q.extract_amount()),
435 descriptor: ingredient.note.clone(),
436 }
437 }
438}
439
440impl From<&cooklang::Cookware<OriginalScalableValue>> for Cookware {
441 fn from(cookware: &cooklang::Cookware<OriginalScalableValue>) -> Self {
442 Cookware {
443 name: cookware.name.clone(),
444 amount: cookware.quantity.as_ref().map(|q| q.extract_amount()),
445 }
446 }
447}
448
449impl From<&cooklang::Timer<OriginalScalableValue>> for Timer {
450 fn from(timer: &cooklang::Timer<OriginalScalableValue>) -> Self {
451 Timer {
452 name: Some(timer.name.clone().unwrap_or_default()),
453 amount: timer.quantity.as_ref().map(|q| q.extract_amount()),
454 }
455 }
456}