Skip to content

Commit f7f44d7

Browse files
committed
feat(bindings): add use_common_names for ingredient list normalization
Expose common name resolution from aisle configuration in the bindings, allowing consumers to replace ingredient aliases (e.g. "apples") with their canonical common names (e.g. "apple gala") after combining.
1 parent 37319df commit f7f44d7

3 files changed

Lines changed: 103 additions & 2 deletions

File tree

bindings/src/aisle.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub struct AisleCategory {
2424
pub struct AisleConf {
2525
pub categories: Vec<AisleCategory>, // cache for quick category search
2626
pub cache: AisleReverseCategory,
27+
pub common_names: HashMap<String, String>, // lowercase name/alias -> common name
2728
}
2829

2930
#[uniffi::export]
@@ -38,6 +39,17 @@ impl AisleConf {
3839
pub fn category_for(&self, ingredient_name: String) -> Option<String> {
3940
self.cache.get(&ingredient_name).cloned()
4041
}
42+
43+
/// Returns the common name for an ingredient using aisle configuration
44+
///
45+
/// Performs case-insensitive lookup against ingredient names and aliases.
46+
/// Returns the original name if not found in the configuration.
47+
pub fn common_name_for(&self, ingredient_name: String) -> String {
48+
self.common_names
49+
.get(&ingredient_name.to_lowercase())
50+
.cloned()
51+
.unwrap_or(ingredient_name)
52+
}
4153
}
4254

4355
pub fn into_category(original: &OriginalAisleCategory) -> AisleCategory {

bindings/src/lib.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ pub fn deref_timer(recipe: &Arc<CooklangRecipe>, index: u32) -> Timer {
102102
pub fn parse_aisle_config(input: String) -> Arc<AisleConf> {
103103
let mut categories: Vec<AisleCategory> = Vec::new();
104104
let mut cache: AisleReverseCategory = AisleReverseCategory::default();
105+
let mut common_names: std::collections::HashMap<String, String> = std::collections::HashMap::new();
105106

106107
// Use the lenient parser that handles duplicates as warnings
107108
let result = parse_lenient(&input);
@@ -131,16 +132,18 @@ pub fn parse_aisle_config(input: String) -> Arc<AisleConf> {
131132
// building cache
132133
category.ingredients.iter().for_each(|i| {
133134
cache.insert(i.name.clone(), category.name.clone());
135+
common_names.insert(i.name.to_lowercase(), i.name.clone());
134136

135137
i.aliases.iter().for_each(|a| {
136138
cache.insert(a.to_string(), category.name.clone());
139+
common_names.insert(a.to_lowercase(), i.name.clone());
137140
});
138141
});
139142

140143
categories.push(category);
141144
});
142145

143-
let config = AisleConf { categories, cache };
146+
let config = AisleConf { categories, cache, common_names };
144147

145148
Arc::new(config)
146149
}
@@ -178,6 +181,24 @@ pub fn combine_ingredients_selected(
178181
combined
179182
}
180183

184+
/// Replaces ingredient names with their common names from aisle configuration
185+
///
186+
/// # Arguments
187+
/// * `list` - The ingredient list to normalize
188+
/// * `aisle` - The aisle configuration containing common name mappings
189+
///
190+
/// # Returns
191+
/// A new ingredient list with names replaced by their common names
192+
#[uniffi::export]
193+
pub fn use_common_names(list: IngredientList, aisle: &AisleConf) -> IngredientList {
194+
let mut normalized: IngredientList = IngredientList::default();
195+
for (ingredient_name, quantity) in list.iter() {
196+
let common_name = aisle.common_name_for(ingredient_name.clone());
197+
add_to_ingredient_list(&mut normalized, &common_name, &quantity);
198+
}
199+
normalized
200+
}
201+
181202
// Metadata helper functions
182203
/// Gets the servings from recipe metadata
183204
///
@@ -886,6 +907,74 @@ dried oregano
886907
);
887908
}
888909

910+
#[test]
911+
fn test_use_common_names() {
912+
use crate::{
913+
combine_ingredients, parse_aisle_config, use_common_names, Amount, Ingredient, Value,
914+
};
915+
916+
let ingredients = vec![
917+
Ingredient {
918+
name: "apples".to_string(),
919+
amount: Some(Amount {
920+
quantity: Value::Number { value: 3.0 },
921+
units: None,
922+
}),
923+
descriptor: None,
924+
reference: None,
925+
},
926+
Ingredient {
927+
name: "eggs".to_string(),
928+
amount: Some(Amount {
929+
quantity: Value::Number { value: 2.0 },
930+
units: None,
931+
}),
932+
descriptor: None,
933+
reference: None,
934+
},
935+
Ingredient {
936+
name: "salt".to_string(),
937+
amount: Some(Amount {
938+
quantity: Value::Number { value: 1.0 },
939+
units: Some("tsp".to_string()),
940+
}),
941+
descriptor: None,
942+
reference: None,
943+
},
944+
];
945+
946+
let aisle = parse_aisle_config(
947+
r#"
948+
[fruit and veg]
949+
apple gala | apples
950+
aubergine
951+
952+
[milk and dairy]
953+
egg | eggs
954+
"#
955+
.to_string(),
956+
);
957+
958+
let combined = combine_ingredients(&ingredients);
959+
960+
// "apples" should exist before common names
961+
assert!(combined.contains_key("apples"));
962+
assert!(combined.contains_key("eggs"));
963+
964+
let normalized = use_common_names(combined, &aisle);
965+
966+
// "apples" should be replaced by common name "apple gala"
967+
assert!(normalized.contains_key("apple gala"));
968+
assert!(!normalized.contains_key("apples"));
969+
970+
// "eggs" should be replaced by common name "egg"
971+
assert!(normalized.contains_key("egg"));
972+
assert!(!normalized.contains_key("eggs"));
973+
974+
// "salt" not in aisle config, should stay as is
975+
assert!(normalized.contains_key("salt"));
976+
}
977+
889978
#[test]
890979
fn test_parse_recipe_with_note() {
891980
use crate::{parse_recipe, Block, Item};

bindings/src/model.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ pub fn expand_with_ingredients(
387387
}
388388

389389
// I(dubadub) haven't found a way to export these methods with mutable argument
390-
fn add_to_ingredient_list(
390+
pub(crate) fn add_to_ingredient_list(
391391
list: &mut IngredientList,
392392
name: &String,
393393
quantity_to_add: &GroupedQuantity,

0 commit comments

Comments
 (0)