lemma-engine 0.8.19

A language that means business.
Documentation
//! Targeted rule evaluation: `rules` slice scopes VM (explain:false) and response.

use lemma::{DataValueInput, DateTimeValue, Engine};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

fn load_order_pipeline() -> (Engine, HashMap<String, String>) {
    let mut engine = Engine::new();
    engine
        .load(
            include_str!("../benches/specs/order_pipeline.lemma"),
            lemma::SourceType::Path(Arc::new(PathBuf::from(
                "engine/benches/specs/order_pipeline.lemma",
            ))),
        )
        .expect("load order_pipeline");
    let json: serde_json::Value =
        serde_json::from_str(include_str!("../benches/specs/order_pipeline.inputs.json"))
            .expect("parse inputs");
    let data = json
        .as_object()
        .expect("object inputs")
        .iter()
        .map(|(k, v)| (k.clone(), v.as_str().expect("string input").to_string()))
        .collect();
    (engine, data)
}

fn explanation_tree_has_rule_node(value: &serde_json::Value, rule_name: &str) -> bool {
    match value {
        serde_json::Value::Object(obj) => {
            if obj.get("type").and_then(|t| t.as_str()) == Some("rule")
                && obj.get("rule").and_then(|r| r.as_str()) == Some(rule_name)
            {
                return true;
            }
            obj.values()
                .any(|v| explanation_tree_has_rule_node(v, rule_name))
        }
        serde_json::Value::Array(arr) => arr
            .iter()
            .any(|v| explanation_tree_has_rule_node(v, rule_name)),
        _ => false,
    }
}

#[test]
fn targeted_eval_one_rule_explain_false() {
    let (engine, data) = load_order_pipeline();
    let now = DateTimeValue::now();
    let response = engine
        .run(
            None,
            "bench_order_pipeline",
            Some(&now),
            data,
            false,
            Some(&["grand_total".to_string()]),
        )
        .expect("run");

    assert_eq!(response.results.len(), 1);
    let grand_total = response.results.get("grand_total").expect("grand_total");
    assert!(!grand_total.vetoed);
    assert!(grand_total.display.is_some());
    assert!(!response.results.contains_key("is_high_value"));
}

#[test]
fn explain_true_subset_full_embed() {
    let (engine, data) = load_order_pipeline();
    let now = DateTimeValue::now();
    let response = engine
        .run(
            None,
            "bench_order_pipeline",
            Some(&now),
            data,
            true,
            Some(&["grand_total".to_string()]),
        )
        .expect("run");

    assert_eq!(response.results.len(), 1);
    let grand_total = response.results.get("grand_total").expect("grand_total");
    let explanation = grand_total
        .explanation
        .as_ref()
        .expect("grand_total explanation");
    let json: serde_json::Value = serde_json::to_value(explanation).expect("serialize");
    assert!(
        explanation_tree_has_rule_node(&json, "pre_tax_total"),
        "expected embedded rule node for pre_tax_total, got: {json}"
    );
}

#[test]
fn empty_rules_errors() {
    let (engine, data) = load_order_pipeline();
    let now = DateTimeValue::now();
    let plan = engine
        .get_plan(None, "bench_order_pipeline", Some(&now))
        .expect("plan");
    let data_values: HashMap<String, DataValueInput> = data
        .into_iter()
        .map(|(k, v)| (k, DataValueInput::convenience(v)))
        .collect();
    let err = engine
        .run_plan(plan, Some(&now), data_values, false, Some(&[]))
        .expect_err("empty rules");
    assert!(err.to_string().contains("at least one rule required"));
}

#[test]
fn unknown_rule_errors() {
    let (engine, data) = load_order_pipeline();
    let now = DateTimeValue::now();
    let err = engine
        .run(
            None,
            "bench_order_pipeline",
            Some(&now),
            data,
            false,
            Some(&["not_a_real_rule".to_string()]),
        )
        .expect_err("unknown rule");
    assert!(err.to_string().contains("not_a_real_rule"));
}

#[test]
fn all_local_rules_explain_false() {
    let (engine, data) = load_order_pipeline();
    let now = DateTimeValue::now();
    let plan = engine
        .get_plan(None, "bench_order_pipeline", Some(&now))
        .expect("plan");
    let rule_count = plan.local_rule_names().len();
    let data_values: HashMap<String, DataValueInput> = data
        .into_iter()
        .map(|(k, v)| (k, DataValueInput::convenience(v)))
        .collect();
    let response = engine
        .run_plan(plan, Some(&now), data_values, false, None)
        .expect("run");
    assert_eq!(response.results.len(), rule_count);
    assert!(response.results.contains_key("grand_total"));
    assert!(response.results.contains_key("is_high_value"));
}

#[test]
fn ratio_canonical_matches_materialized() {
    let mut engine = Engine::new();
    engine
        .load(
            r#"
spec ratio_canonical
rule rate: 23 percent
"#,
            lemma::SourceType::Volatile,
        )
        .expect("load");
    let now = DateTimeValue::now();
    let response = engine
        .run(
            None,
            "ratio_canonical",
            Some(&now),
            HashMap::new(),
            false,
            None,
        )
        .expect("run");
    let rate = response.results.get("rate").expect("rate key canonical");
    assert_eq!(rate.rule.name, "rate");
    assert_eq!(rate.display.as_deref(), Some("23%"));
}