Skip to content

Commit 098c933

Browse files
committed
feat: support full metadata in bindings
1 parent 20a39fa commit 098c933

2 files changed

Lines changed: 260 additions & 75 deletions

File tree

bindings/src/lib.rs

Lines changed: 159 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::sync::Arc;
22

33
use cooklang::aisle::parse_lenient;
4+
use cooklang::metadata::StdKey as OriginalStdKey;
45

56
pub mod aisle;
67
pub mod model;
@@ -9,37 +10,19 @@ use aisle::*;
910
use model::*;
1011

1112
#[uniffi::export]
12-
pub fn parse_recipe(input: String, scaling_factor: f64) -> CooklangRecipe {
13+
pub fn parse_recipe(input: String, scaling_factor: f64) -> Arc<CooklangRecipe> {
1314
let parser = cooklang::CooklangParser::canonical();
1415

1516
let (mut parsed, _warnings) = parser.parse(&input).into_result().unwrap();
1617

1718
parsed.scale(scaling_factor, parser.converter());
1819

19-
into_simple_recipe(&parsed)
20+
Arc::new(into_simple_recipe(&parsed))
2021
}
2122

22-
#[uniffi::export]
23-
pub fn parse_metadata(input: String, scaling_factor: f64) -> CooklangMetadata {
24-
let mut metadata = CooklangMetadata::new();
25-
let parser = cooklang::CooklangParser::canonical();
26-
27-
let (mut parsed, _warnings) = parser.parse(&input).into_result().unwrap();
28-
29-
parsed.scale(scaling_factor, parser.converter());
30-
31-
// converting IndexMap into HashMap
32-
let _ = &(parsed.metadata.map).iter().for_each(|(key, value)| {
33-
if let (Some(key), Some(value)) = (key.as_str(), value.as_str()) {
34-
metadata.insert(key.to_string(), value.to_string());
35-
}
36-
});
37-
38-
metadata
39-
}
4023

4124
#[uniffi::export]
42-
pub fn deref_component(recipe: &CooklangRecipe, item: Item) -> Component {
25+
pub fn deref_component(recipe: &Arc<CooklangRecipe>, item: Item) -> Component {
4326
match item {
4427
Item::IngredientRef { index } => {
4528
Component::IngredientComponent(recipe.ingredients.get(index as usize).unwrap().clone())
@@ -55,17 +38,17 @@ pub fn deref_component(recipe: &CooklangRecipe, item: Item) -> Component {
5538
}
5639

5740
#[uniffi::export]
58-
pub fn deref_ingredient(recipe: &CooklangRecipe, index: u32) -> Ingredient {
41+
pub fn deref_ingredient(recipe: &Arc<CooklangRecipe>, index: u32) -> Ingredient {
5942
recipe.ingredients.get(index as usize).unwrap().clone()
6043
}
6144

6245
#[uniffi::export]
63-
pub fn deref_cookware(recipe: &CooklangRecipe, index: u32) -> Cookware {
46+
pub fn deref_cookware(recipe: &Arc<CooklangRecipe>, index: u32) -> Cookware {
6447
recipe.cookware.get(index as usize).unwrap().clone()
6548
}
6649

6750
#[uniffi::export]
68-
pub fn deref_timer(recipe: &CooklangRecipe, index: u32) -> Timer {
51+
pub fn deref_timer(recipe: &Arc<CooklangRecipe>, index: u32) -> Timer {
6952
recipe.timers.get(index as usize).unwrap().clone()
7053
}
7154

@@ -134,6 +117,70 @@ pub fn combine_ingredients_selected(
134117
combined
135118
}
136119

120+
// Metadata helper functions
121+
#[uniffi::export]
122+
pub fn metadata_servings(recipe: &Arc<CooklangRecipe>) -> Option<Servings> {
123+
recipe.metadata.servings().map(|s| s.clone().into())
124+
}
125+
126+
#[uniffi::export]
127+
pub fn metadata_title(recipe: &Arc<CooklangRecipe>) -> Option<String> {
128+
recipe.metadata.title().map(|s| s.to_string())
129+
}
130+
131+
#[uniffi::export]
132+
pub fn metadata_description(recipe: &Arc<CooklangRecipe>) -> Option<String> {
133+
recipe.metadata.description().map(|s| s.to_string())
134+
}
135+
136+
#[uniffi::export]
137+
pub fn metadata_tags(recipe: &Arc<CooklangRecipe>) -> Option<Vec<String>> {
138+
recipe.metadata.tags().map(|tags| tags.into_iter().map(|t| t.to_string()).collect())
139+
}
140+
141+
#[uniffi::export]
142+
pub fn metadata_author(recipe: &Arc<CooklangRecipe>) -> Option<NameAndUrl> {
143+
recipe.metadata.author().map(|a| a.clone().into())
144+
}
145+
146+
#[uniffi::export]
147+
pub fn metadata_source(recipe: &Arc<CooklangRecipe>) -> Option<NameAndUrl> {
148+
recipe.metadata.source().map(|s| s.clone().into())
149+
}
150+
151+
#[uniffi::export]
152+
pub fn metadata_time(recipe: &Arc<CooklangRecipe>) -> Option<RecipeTime> {
153+
let converter = cooklang::Converter::empty();
154+
recipe.metadata.time(&converter).map(|t| t.clone().into())
155+
}
156+
157+
#[uniffi::export]
158+
pub fn metadata_get(recipe: &Arc<CooklangRecipe>, key: String) -> Option<String> {
159+
recipe.metadata.get(&key).and_then(|v| v.as_str()).map(|s| s.to_string())
160+
}
161+
162+
#[uniffi::export]
163+
pub fn metadata_get_std(recipe: &Arc<CooklangRecipe>, key: StdKey) -> Option<String> {
164+
let original_key = match key {
165+
StdKey::Title => OriginalStdKey::Title,
166+
StdKey::Description => OriginalStdKey::Description,
167+
StdKey::Tags => OriginalStdKey::Tags,
168+
StdKey::Author => OriginalStdKey::Author,
169+
StdKey::Source => OriginalStdKey::Source,
170+
StdKey::Course => OriginalStdKey::Course,
171+
StdKey::Time => OriginalStdKey::Time,
172+
StdKey::PrepTime => OriginalStdKey::PrepTime,
173+
StdKey::CookTime => OriginalStdKey::CookTime,
174+
StdKey::Servings => OriginalStdKey::Servings,
175+
StdKey::Difficulty => OriginalStdKey::Difficulty,
176+
StdKey::Cuisine => OriginalStdKey::Cuisine,
177+
StdKey::Diet => OriginalStdKey::Diet,
178+
StdKey::Images => OriginalStdKey::Images,
179+
StdKey::Locale => OriginalStdKey::Locale,
180+
};
181+
recipe.metadata.get(original_key).and_then(|v| v.as_str()).map(|s| s.to_string())
182+
}
183+
137184
uniffi::setup_scaffolding!();
138185

139186
#[cfg(test)]
@@ -168,15 +215,13 @@ a test @step @salt{1%mg} more text
168215
assert_eq!(
169216
match recipe
170217
.sections
171-
.into_iter()
172-
.next()
218+
.get(0)
173219
.expect("No blocks found")
174220
.blocks
175-
.into_iter()
176-
.next()
221+
.get(0)
177222
.expect("No blocks found")
178223
{
179-
Block::StepBlock(step) => step,
224+
Block::StepBlock(step) => step.clone(),
180225
_ => panic!("Expected first block to be a Step"),
181226
}
182227
.items,
@@ -216,24 +261,86 @@ a test @step @salt{1%mg} more text
216261
}
217262

218263
#[test]
219-
fn test_parse_metadata() {
220-
use crate::parse_metadata;
221-
use std::collections::HashMap;
264+
fn test_metadata_helpers() {
265+
use crate::{metadata_source, metadata_title, metadata_servings, metadata_tags, parse_recipe, Servings};
222266

223-
let metadata = parse_metadata(
267+
let recipe = parse_recipe(
224268
r#"---
269+
title: Test Recipe
225270
source: https://google.com
271+
servings: 4
272+
tags: easy, quick, vegetarian
226273
---
227274
a test @step @salt{1%mg} more text
228275
"#
229276
.to_string(),
230277
1.0,
231278
);
232279

280+
// Test title
233281
assert_eq!(
234-
metadata,
235-
HashMap::from([("source".to_string(), "https://google.com".to_string())])
282+
metadata_title(&recipe),
283+
Some("Test Recipe".to_string())
284+
);
285+
286+
// Test source
287+
let source = metadata_source(&recipe);
288+
assert!(source.is_some());
289+
let source = source.unwrap();
290+
assert_eq!(source.url, Some("https://google.com".to_string()));
291+
292+
// Test servings
293+
let servings = metadata_servings(&recipe);
294+
assert!(servings.is_some());
295+
match servings.unwrap() {
296+
Servings::Number { value } => assert_eq!(value, 4),
297+
_ => panic!("Expected number servings"),
298+
}
299+
300+
// Test tags
301+
let tags = metadata_tags(&recipe);
302+
assert!(tags.is_some());
303+
let tags = tags.unwrap();
304+
assert_eq!(tags.len(), 3);
305+
assert!(tags.contains(&"easy".to_string()));
306+
assert!(tags.contains(&"quick".to_string()));
307+
assert!(tags.contains(&"vegetarian".to_string()));
308+
}
309+
310+
#[test]
311+
fn test_metadata_advanced() {
312+
use crate::{metadata_author, metadata_servings, parse_recipe, Servings};
313+
314+
let recipe = parse_recipe(
315+
r#"---
316+
author: John Doe <https://johndoe.com>
317+
time: 1h 30m
318+
servings: 2-3 portions
319+
---
320+
Cook something delicious
321+
"#
322+
.to_string(),
323+
1.0,
236324
);
325+
326+
// Test author with URL
327+
let author = metadata_author(&recipe);
328+
assert!(author.is_some());
329+
let author = author.unwrap();
330+
assert_eq!(author.name, Some("John Doe".to_string()));
331+
assert_eq!(author.url, Some("https://johndoe.com".to_string()));
332+
333+
// Note: Time parsing requires units to be loaded in the converter
334+
// Since we're using an empty converter, time parsing won't work for "1h 30m"
335+
// We would need to add units configuration for this to work
336+
337+
// Test text servings
338+
let servings = metadata_servings(&recipe);
339+
assert!(servings.is_some());
340+
match servings.unwrap() {
341+
Servings::Text { value } => assert_eq!(value, "2-3 portions"),
342+
_ => panic!("Expected text servings"),
343+
}
237344
}
238345

239346
#[test]
@@ -380,19 +487,17 @@ Cook @onions{3%large} until brown
380487

381488
let first_section = recipe
382489
.sections
383-
.into_iter()
384-
.next()
490+
.get(0)
385491
.expect("No sections found");
386492

387493
assert_eq!(first_section.blocks.len(), 2);
388494

389495
// Check note block
390-
let mut iterator = first_section.blocks.into_iter();
391-
let note_block = iterator.next().expect("No blocks found");
496+
let note_block = first_section.blocks.get(0).expect("No blocks found");
392497

393498
assert_eq!(
394499
match note_block {
395-
Block::NoteBlock(note) => note,
500+
Block::NoteBlock(note) => note.clone(),
396501
_ => panic!("Expected first block to be a Note"),
397502
}
398503
.text,
@@ -401,11 +506,11 @@ Cook @onions{3%large} until brown
401506
);
402507

403508
// Check step block
404-
let step_block = iterator.next().expect("No blocks found");
509+
let step_block = first_section.blocks.get(1).expect("No blocks found");
405510

406511
assert_eq!(
407512
match step_block {
408-
Block::StepBlock(step) => step,
513+
Block::StepBlock(step) => step.clone(),
409514
_ => panic!("Expected second block to be a Step"),
410515
}
411516
.items,
@@ -438,19 +543,17 @@ simmer for 10 minutes
438543
);
439544
let first_section = recipe
440545
.sections
441-
.into_iter()
442-
.next()
546+
.get(0)
443547
.expect("No sections found");
444548
assert_eq!(first_section.blocks.len(), 2);
445549

446550
// Check first step
447-
let mut iterator = first_section.blocks.into_iter();
448-
let first_block = iterator.next().expect("No blocks found");
449-
let second_block = iterator.next().expect("No blocks found");
551+
let first_block = first_section.blocks.get(0).expect("No blocks found");
552+
let second_block = first_section.blocks.get(1).expect("No blocks found");
450553

451554
assert_eq!(
452555
match first_block {
453-
Block::StepBlock(step) => step,
556+
Block::StepBlock(step) => step.clone(),
454557
_ => panic!("Expected first block to be a Step"),
455558
}
456559
.items,
@@ -468,7 +571,7 @@ simmer for 10 minutes
468571
// Check second step
469572
assert_eq!(
470573
match second_block {
471-
Block::StepBlock(step) => step,
574+
Block::StepBlock(step) => step.clone(),
472575
_ => panic!("Expected second block to be a Step"),
473576
}
474577
.items,
@@ -502,21 +605,20 @@ Combine @cheese{100%g} and @spinach{50%g}, then season to taste.
502605
1.0,
503606
);
504607

505-
let mut sections = recipe.sections.into_iter();
608+
let sections = &recipe.sections;
506609

507610
// Check first section
508-
let first_section = sections.next().expect("No sections found");
611+
let first_section = sections.get(0).expect("No sections found");
509612
assert_eq!(first_section.title, Some("Dough".to_string()));
510613
assert_eq!(first_section.blocks.len(), 1);
511614

512615
let first_block = first_section
513616
.blocks
514-
.into_iter()
515-
.next()
617+
.get(0)
516618
.expect("No blocks found");
517619
assert_eq!(
518620
match first_block {
519-
Block::StepBlock(step) => step,
621+
Block::StepBlock(step) => step.clone(),
520622
_ => panic!("Expected block to be a Step"),
521623
}
522624
.items,
@@ -536,18 +638,17 @@ Combine @cheese{100%g} and @spinach{50%g}, then season to taste.
536638
);
537639

538640
// Check second section
539-
let second_section = sections.next().expect("No second section found");
641+
let second_section = sections.get(1).expect("No second section found");
540642
assert_eq!(second_section.title, Some("Filling".to_string()));
541643
assert_eq!(second_section.blocks.len(), 1);
542644

543645
let second_block = second_section
544646
.blocks
545-
.into_iter()
546-
.next()
647+
.get(0)
547648
.expect("No blocks found");
548649
assert_eq!(
549650
match second_block {
550-
Block::StepBlock(step) => step,
651+
Block::StepBlock(step) => step.clone(),
551652
_ => panic!("Expected block to be a Step"),
552653
}
553654
.items,

0 commit comments

Comments
 (0)