Functional Weave
Code in Rust

hospitality.allergen-matrix@1.0.1

impl/rust.rs

4,577 bytes · the Rust implementation · view raw

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(),
    )
}