Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::add_days; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::hospitality_allergen_matrix_data::{Allergen, ALLERGENS, ALLERGENS_HISTORY}; ← this capability’s own data, compiled from data/allergens.json into the same file by fune build
fn list_in_force(jurisdiction: &str, on_date: &str) -> Vec<&'static Allergen> {
let mut rows: Vec<&'static Allergen> = ALLERGENS
.iter()
.filter(|a| {
a.jurisdiction == jurisdiction && a.valid_from <= on_date && a.valid_to.map_or(true, |to| on_date <= to)
})
.collect();
if !rows.is_empty() {
rows.sort_by_key(|a| a.annex_number);
return rows;
}
// A history-pruned build may have dropped the list that applied then; say so
// rather than answering with nothing.
if ALLERGENS_HISTORY != "full" {
let earliest = ALLERGENS.iter().filter(|a| a.jurisdiction == jurisdiction).map(|a| a.valid_from).min();
if let Some(earliest) = earliest {
if on_date < earliest {
panic!(
"no allergen list for {} on {}: this build was installed with history={}, so it only carries lists from {}",
jurisdiction, on_date, ALLERGENS_HISTORY, earliest
);
}
}
}
panic!("no allergen list for {} on {}", jurisdiction, on_date)
}
fn dedupe(names: Vec<String>) -> Vec<String> {
let mut seen: Vec<String> = Vec::new();
for name in names {
if !seen.contains(&name) {
seen.push(name);
}
}
seen
}
/// The regulated allergens in a dish, from what its ingredients declare.
///
/// Every allergen on the list gets a row, in the legal order, so a menu or
/// allergen chart shows "none" as a decision rather than a gap. An allergen any
/// ingredient contains is "contains"; otherwise one any ingredient may contain
/// is "may-contain". The sources say which ingredients to check.
///
/// # Panics
/// Panics on a bad date, no list for the jurisdiction and date, or an
/// allergen code that is not on the list.
pub fn allergen_matrix(ingredients: &[IngredientAllergens], jurisdiction: &str, on_date: &str) -> Vec<AllergenRow> {
add_days(on_date, 0);
let list = list_in_force(jurisdiction, on_date);
for ingredient in ingredients {
for code in ingredient.contains.iter().chain(ingredient.may_contain.iter()) {
if !list.iter().any(|a| a.code == code.as_str()) {
panic!(
"unknown allergen \"{}\" in \"{}\": not on the {} list on {}",
code, ingredient.name, jurisdiction, on_date
);
}
}
}
list.iter()
.map(|allergen| {
let contains: Vec<String> = ingredients
.iter()
.filter(|i| i.contains.iter().any(|c| c == allergen.code))
.map(|i| i.name.clone())
.collect();
let may: Vec<String> = ingredients
.iter()
.filter(|i| i.may_contain.iter().any(|c| c == allergen.code))
.map(|i| i.name.clone())
.collect();
let status = if !contains.is_empty() {
"contains"
} else if !may.is_empty() {
"may-contain"
} else {
"none"
};
AllergenRow {
code: allergen.code.to_string(),
name: allergen.name.to_string(),
status: status.to_string(),
sources: dedupe(if contains.is_empty() { may } else { contains }),
}
})
.collect()
}
fn strings(v: &Value) -> Vec<String> {
v.as_arr().iter().map(|s| s.as_str().to_string()).collect()
}
pub fn ingredient_allergens_from_value(v: &Value) -> IngredientAllergens {
IngredientAllergens {
name: v.get("name").as_str().to_string(),
contains: strings(v.get("contains")),
may_contain: strings(v.get("mayContain")),
}
}
pub fn allergen_row_to_value(r: &AllergenRow) -> Value {
Value::obj(vec![
("code", Value::str(&r.code)),
("name", Value::str(&r.name)),
("status", Value::str(&r.status)),
("sources", Value::Arr(r.sources.iter().map(|s| Value::str(s)).collect())),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let ingredients: Vec<IngredientAllergens> = args[0].as_arr().iter().map(ingredient_allergens_from_value).collect();
Value::Arr(
allergen_matrix(&ingredients, args[1].as_str(), args[2].as_str())
.iter()
.map(allergen_row_to_value)
.collect(),
)
}