lemma-engine 0.8.21

A pure, declarative language for business rules.
Documentation
use lemma::{format_explanation, DateTimeValue, Engine, LiteralValue};
use std::collections::HashMap;

#[test]
fn explanation_generated_during_evaluation() {
    let mut engine = Engine::new();
    engine
        .load(
            r#"
spec test_explanation
data base_value: 100
rule doubled: base_value * 2
"#,
            lemma::SourceType::Volatile,
        )
        .unwrap();
    let now = DateTimeValue::now();
    let response = engine
        .run(
            None,
            "test_explanation",
            Some(&now),
            HashMap::new(),
            true,
            None,
        )
        .unwrap();

    let doubled = response
        .results
        .values()
        .find(|r| r.rule.name == "doubled")
        .expect("doubled rule");

    assert_eq!(
        doubled.display.clone().expect("display"),
        LiteralValue::number_from_decimal(rust_decimal::Decimal::from(200)).to_string(),
    );

    let explanation = doubled.explanation.as_ref().expect("explanation built");
    assert_eq!(explanation.rule.rule, "doubled");
    assert!(format_explanation(explanation).contains("base_value"));
}

#[test]
fn explanation_with_rule_reference() {
    let mut engine = Engine::new();
    engine
        .load(
            r#"
spec test_explanation_ref
data base_value: 50
rule doubled: base_value * 2
rule quadruple: doubled * 2
"#,
            lemma::SourceType::Volatile,
        )
        .unwrap();
    let now = DateTimeValue::now();
    let response = engine
        .run(
            None,
            "test_explanation_ref",
            Some(&now),
            HashMap::new(),
            true,
            None,
        )
        .unwrap();

    let quadruple = response
        .results
        .values()
        .find(|r| r.rule.name == "quadruple")
        .expect("quadruple rule");

    let explanation = quadruple.explanation.as_ref().expect("explanation");
    let text = format_explanation(explanation);
    assert!(text.contains("doubled * 2"));

    let json: serde_json::Value = serde_json::to_value(explanation).expect("serialize");
    // The multiplication's literal operand is shown in the body line itself;
    // the embedded rule is the only child.
    let child_type = json["children"][0]["type"]
        .as_str()
        .expect("embedded rule child type");
    assert_eq!(child_type, "rule");
    assert_eq!(json["children"][0]["rule"], "doubled");
}

#[test]
fn explanation_unless_branch_causes() {
    let mut engine = Engine::new();
    engine
        .load(
            r#"
spec test_unless
data flag: false
rule out: 1 unless flag then 2
"#,
            lemma::SourceType::Volatile,
        )
        .unwrap();
    let now = DateTimeValue::now();
    let response = engine
        .run(None, "test_unless", Some(&now), HashMap::new(), true, None)
        .unwrap();

    let explanation = response
        .results
        .get("out")
        .expect("out rule")
        .explanation
        .as_ref()
        .expect("explanation");
    assert_eq!(explanation.causes.len(), 1);
    // The condition `flag` was false; the cause states the fact that held.
    assert_eq!(explanation.causes[0].condition, "flag is false");
    assert_eq!(explanation.causes[0].value, "true");
}

#[test]
fn explanation_user_veto() {
    let mut engine = Engine::new();
    engine
        .load(
            r#"
spec test_veto
data price: -5
rule validated: price unless price < 0 then veto "negative"
"#,
            lemma::SourceType::Volatile,
        )
        .unwrap();
    let now = DateTimeValue::now();
    let response = engine
        .run(None, "test_veto", Some(&now), HashMap::new(), true, None)
        .unwrap();

    let result = response.results.get("validated").expect("validated");
    assert!(result.vetoed);
    let explanation = result.explanation.as_ref().expect("explanation");
    assert!(format_explanation(explanation).contains("veto"));
}

#[test]
fn explanation_cross_spec_rule() {
    let mut engine = Engine::new();
    engine
        .load(
            r#"
spec helper
rule helper_value: 10

spec main
uses helper
rule use_cross_spec: helper.helper_value + 1
"#,
            lemma::SourceType::Volatile,
        )
        .unwrap();
    let now = DateTimeValue::now();
    let response = engine
        .run(None, "main", Some(&now), HashMap::new(), true, None)
        .unwrap();

    let explanation = response
        .results
        .get("use_cross_spec")
        .expect("rule")
        .explanation
        .as_ref()
        .expect("explanation");
    assert!(format_explanation(explanation).contains("helper"));
    assert!(format_explanation(explanation).contains("helper_value"));
}

#[test]
fn explanation_unit_conversion_steps() {
    let mut engine = Engine::new();
    engine
        .load(
            r#"
spec t
data weight: measure -> unit kg 1 -> unit gram 0.001
data w: 2 kg
rule in_grams: w as gram
"#,
            lemma::SourceType::Volatile,
        )
        .unwrap();
    let response = engine
        .run(None, "t", None, HashMap::new(), true, None)
        .unwrap();
    let explanation = response
        .results
        .get("in_grams")
        .expect("rule")
        .explanation
        .as_ref()
        .expect("explanation");

    let json = serde_json::to_value(explanation).unwrap();
    let steps = &json["children"][0]["steps"];
    assert!(steps
        .as_array()
        .unwrap()
        .iter()
        .any(|s| s["role"] == "outcome"));
    assert!(steps
        .as_array()
        .unwrap()
        .iter()
        .any(|s| s["role"] == "source"));
}