use lemma::parsing::ast::DateTimeValue;
use lemma::Engine;
use rust_decimal::Decimal;
use std::collections::HashMap;
use std::str::FromStr;
fn load_examples() -> Engine {
let mut engine = Engine::new();
let examples = [
"../cli/tests/integrations/examples/01_simple_facts.lemma",
"../cli/tests/integrations/examples/02_rules_and_unless.lemma",
"../cli/tests/integrations/examples/03_spec_references.lemma",
"../cli/tests/integrations/examples/04_unit_conversions.lemma",
"../cli/tests/integrations/examples/05_date_handling.lemma",
"../cli/tests/integrations/examples/06_tax_calculation.lemma",
"../cli/tests/integrations/examples/07_shipping_policy.lemma",
"../cli/tests/integrations/examples/08_rule_references.lemma",
"../cli/tests/integrations/examples/09_stress_test.lemma",
"../cli/tests/integrations/examples/10_compensation_policy.lemma",
"../cli/tests/integrations/examples/11_spec_composition.lemma",
];
for path in examples {
let content = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e));
engine
.load(&content, lemma::SourceType::Labeled(path))
.unwrap_or_else(|errs| {
panic!(
"Failed to parse {}: {}",
path,
errs.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ")
)
});
}
engine
}
#[test]
fn test_01_simple_facts() {
let engine = load_examples();
let now = DateTimeValue::now();
let response = engine
.run("simple_facts", Some(&now), HashMap::new(), false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "simple_facts");
assert_eq!(response.results.len(), 0);
}
#[test]
fn test_02_rules_and_unless() {
let engine = load_examples();
let now = DateTimeValue::now();
let mut facts = std::collections::HashMap::new();
facts.insert("base_price".to_string(), "100.00".to_string());
facts.insert("quantity".to_string(), "10".to_string());
facts.insert("is_premium".to_string(), "true".to_string());
facts.insert("customer_age".to_string(), "17".to_string());
let response = engine
.run("rules_and_unless", Some(&now), facts, false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "rules_and_unless");
let final_total = response.results.get("final_total").unwrap();
match &final_total.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(*n, Decimal::from_str("800").unwrap()),
other => panic!("Expected Number for final_total, got {:?}", other),
},
other => panic!("Expected Value for final_total, got {:?}", other),
}
let age_validation = response.results.get("age_validation").unwrap();
assert_eq!(
age_validation.result,
lemma::OperationResult::Veto(Some("Customer must be 18 or older".to_string()))
);
}
#[test]
fn test_03_spec_references() {
let engine = load_examples();
let now = DateTimeValue::now();
let response = engine
.run("base_employee", Some(&now), HashMap::new(), false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "base_employee");
assert!(response
.results
.values()
.any(|r| r.rule.name == "annual_salary"));
assert!(response
.results
.values()
.any(|r| r.rule.name == "is_eligible_for_bonus"));
let response = engine
.run("specific_employee", Some(&now), HashMap::new(), false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "specific_employee");
let salary_with_bonus = response.results.get("salary_with_bonus").unwrap();
match &salary_with_bonus.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(*n, Decimal::from_str("99000").unwrap()),
other => panic!("Expected Number for salary_with_bonus, got {:?}", other),
},
other => panic!("Expected Value for salary_with_bonus, got {:?}", other),
}
let employee_summary = response.results.get("employee_summary").unwrap();
match &employee_summary.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Text(s) => assert_eq!(s, "Alice Smith"),
other => panic!("Expected Text for employee_summary, got {:?}", other),
},
other => panic!("Expected Value for employee_summary, got {:?}", other),
}
let response = engine
.run("contractor", Some(&now), HashMap::new(), false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "contractor");
assert!(response
.results
.values()
.any(|r| r.rule.name == "total_payment"));
assert!(response
.results
.values()
.any(|r| r.rule.name == "benefits_eligible"));
}
#[test]
fn test_04_unit_conversions() {
let engine = load_examples();
let now = DateTimeValue::now();
let response = engine
.run("unit_conversions", Some(&now), HashMap::new(), false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "unit_conversions");
let duration_hours = response.results.get("duration_hours").unwrap();
match &duration_hours.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Duration(v, unit) => {
assert_eq!(*v, Decimal::from_str("1.5").unwrap());
assert_eq!(*unit, lemma::SemanticDurationUnit::Hour);
}
other => panic!("Expected Duration for duration_hours, got {:?}", other),
},
other => panic!("Expected Value for duration_hours, got {:?}", other),
}
let duration_seconds = response.results.get("duration_seconds").unwrap();
match &duration_seconds.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Duration(v, unit) => {
assert_eq!(*v, Decimal::from_str("5400").unwrap());
assert_eq!(*unit, lemma::SemanticDurationUnit::Second);
}
other => panic!("Expected Duration for duration_seconds, got {:?}", other),
},
other => panic!("Expected Value for duration_seconds, got {:?}", other),
}
let is_quick_processing = response.results.get("is_quick_processing").unwrap();
assert_eq!(
is_quick_processing.result,
lemma::OperationResult::Value(Box::new(lemma::LiteralValue::from_bool(true)))
);
}
#[test]
fn test_05_date_handling() {
let engine = load_examples();
let now = DateTimeValue::now();
let mut facts = std::collections::HashMap::new();
facts.insert("current_date".to_string(), "2024-06-15".to_string());
let response = engine
.run("date_handling", Some(&now), facts, false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "date_handling");
let probation_end = response.results.get("probation_end_date").unwrap();
match &probation_end.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Date(date) => {
assert_eq!(date.year, 2024);
assert_eq!(date.month, 5);
assert_eq!(date.day, 30);
}
other => panic!("Expected Date for probation_end_date, got {:?}", other),
},
other => panic!("Expected Value for probation_end_date, got {:?}", other),
}
let is_probation_complete = response.results.get("is_probation_complete").unwrap();
assert_eq!(
is_probation_complete.result,
lemma::OperationResult::Value(Box::new(lemma::LiteralValue::from_bool(true)))
);
}
#[test]
fn test_06_tax_calculation() {
let engine = load_examples();
let now = DateTimeValue::now();
let mut facts = HashMap::new();
facts.insert("income".to_string(), "80000".to_string());
facts.insert("deductions".to_string(), "10000".to_string());
facts.insert("country".to_string(), "NL".to_string());
facts.insert("filing_status".to_string(), "single".to_string());
let response = engine
.run("tax_calculation", Some(&now), facts, false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "tax_calculation");
let total_tax = response.results.get("total_tax").unwrap();
match &total_tax.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => {
assert!(
*n > Decimal::ZERO,
"total_tax should be positive, got: {}",
n
);
}
other => panic!("Expected Number for total_tax, got {:?}", other),
},
other => panic!("Expected Value for total_tax, got {:?}", other),
}
let after_tax_income = response.results.get("after_tax_income").unwrap();
match &after_tax_income.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => {
assert!(
*n < Decimal::from_str("80000").unwrap(),
"after_tax_income should be less than income, got: {}",
n
);
}
other => panic!("Expected Number for after_tax_income, got {:?}", other),
},
other => panic!("Expected Value for after_tax_income, got {:?}", other),
}
}
#[test]
fn test_07_shipping_policy() {
let engine = load_examples();
let now = DateTimeValue::now();
let mut facts = std::collections::HashMap::new();
facts.insert("order_total".to_string(), "75.00".to_string());
facts.insert("item_weight".to_string(), "8".to_string());
facts.insert("destination_country".to_string(), "NL".to_string());
facts.insert(
"destination_region".to_string(),
"North Holland".to_string(),
);
facts.insert("is_po_box".to_string(), "false".to_string());
facts.insert("is_expedited".to_string(), "false".to_string());
facts.insert("is_hazardous".to_string(), "false".to_string());
let response = engine
.run("shipping_policy", Some(&now), facts, false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "shipping_policy");
let final_shipping = response.results.get("final_shipping").unwrap();
match &final_shipping.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => {
assert!(
*n > Decimal::ZERO,
"final_shipping should be positive, got: {}",
n
);
}
other => panic!("Expected Number for final_shipping, got {:?}", other),
},
other => panic!("Expected Value for final_shipping, got {:?}", other),
}
let estimated_delivery_days = response.results.get("estimated_delivery_days").unwrap();
match &estimated_delivery_days.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Duration(v, unit) => {
assert_eq!(*v, Decimal::from_str("2").unwrap());
assert_eq!(*unit, lemma::SemanticDurationUnit::Day);
}
other => panic!(
"Expected Duration for estimated_delivery_days, got {:?}",
other
),
},
other => panic!(
"Expected Value for estimated_delivery_days, got {:?}",
other
),
}
}
#[test]
fn test_08_rule_references() {
let engine = load_examples();
let now = DateTimeValue::now();
let response = engine
.run("rule_references", Some(&now), HashMap::new(), false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "rule_references");
assert_eq!(
response.results.get("can_drive_legally").unwrap().result,
lemma::OperationResult::Value(Box::new(lemma::LiteralValue::from_bool(true)))
);
let driving_status = response.results.get("driving_status").unwrap();
match &driving_status.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Text(s) => assert_eq!(s, "Can drive legally"),
other => panic!("Expected Text for driving_status, got {:?}", other),
},
other => panic!("Expected Value for driving_status, got {:?}", other),
}
let response = engine
.run("eligibility_check", Some(&now), HashMap::new(), false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "eligibility_check");
assert_eq!(
response
.results
.get("can_travel_internationally")
.unwrap()
.result,
lemma::OperationResult::Veto(Some("Valid travel documents required".to_string()))
);
let eligibility_message = response.results.get("eligibility_message").unwrap();
assert_eq!(
eligibility_message.result,
lemma::OperationResult::Veto(Some("Valid travel documents required".to_string()))
);
}
#[test]
fn test_09_stress_test() {
let engine = load_examples();
let now = DateTimeValue::now();
let mut facts = std::collections::HashMap::new();
facts.insert("base_price".to_string(), "100.00".to_string());
facts.insert("quantity".to_string(), "50".to_string());
facts.insert("customer_tier".to_string(), "standard".to_string());
facts.insert("loyalty_points".to_string(), "5000".to_string());
facts.insert("package_weight".to_string(), "25".to_string());
facts.insert("delivery_distance".to_string(), "300".to_string());
facts.insert("is_express".to_string(), "false".to_string());
facts.insert("is_fragile".to_string(), "false".to_string());
facts.insert("payment_method".to_string(), "credit".to_string());
let response = engine
.run("stress_test", Some(&now), facts, false)
.expect("Evaluation should succeed");
assert_eq!(response.spec_name, "stress_test");
assert!(!response.results.is_empty());
}
#[test]
fn test_09_stress_test_config() {
let engine = load_examples();
let now = DateTimeValue::now();
let response = engine
.run("stress_test_config", Some(&now), HashMap::new(), false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "stress_test_config");
}
#[test]
fn test_09_stress_test_extended() {
let engine = load_examples();
let now = DateTimeValue::now();
let mut facts = std::collections::HashMap::new();
facts.insert("order.base_price".to_string(), "100.00".to_string());
facts.insert("order.quantity".to_string(), "100".to_string());
facts.insert("order.customer_tier".to_string(), "vip".to_string());
facts.insert("order.loyalty_points".to_string(), "10000".to_string());
facts.insert("order.package_weight".to_string(), "30".to_string());
facts.insert("order.delivery_distance".to_string(), "250".to_string());
facts.insert("order.is_express".to_string(), "true".to_string());
facts.insert("order.is_fragile".to_string(), "true".to_string());
facts.insert("order.payment_method".to_string(), "debit".to_string());
let response = engine
.run("stress_test_extended", Some(&now), facts, false)
.expect("Cross-spec rule references now work correctly");
assert_eq!(response.spec_name, "stress_test_extended");
assert!(!response.results.is_empty());
}
#[test]
fn test_10_compensation_policy() {
let engine = load_examples();
let now = DateTimeValue::now();
let response = engine
.run(
"compensation/base_policy",
Some(&now),
HashMap::new(),
false,
)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "compensation/base_policy");
assert!(response
.results
.values()
.any(|r| r.rule.name == "annual_health_cost"));
let response = engine
.run(
"compensation/engineering_dept",
Some(&now),
HashMap::new(),
false,
)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "compensation/engineering_dept");
assert!(response
.results
.values()
.any(|r| r.rule.name == "total_package"));
let response = engine
.run(
"compensation/senior_engineer",
Some(&now),
HashMap::new(),
false,
)
.unwrap();
assert_eq!(response.spec_name, "compensation/senior_engineer");
assert!(!response.results.is_empty());
let response = engine
.run(
"compensation/principal_engineer",
Some(&now),
HashMap::new(),
false,
)
.unwrap();
assert_eq!(response.spec_name, "compensation/principal_engineer");
assert!(!response.results.is_empty());
}
#[test]
fn test_11_spec_composition() {
let engine = load_examples();
let now = DateTimeValue::now();
let response = engine
.run("pricing/base_config", Some(&now), HashMap::new(), false)
.expect("Failed to evaluate base_config");
assert_eq!(response.spec_name, "pricing/base_config");
assert!(response
.results
.values()
.any(|r| r.rule.name == "final_price"));
let response = engine
.run("pricing/wholesale", Some(&now), HashMap::new(), false)
.expect("Failed to evaluate wholesale");
assert_eq!(response.spec_name, "pricing/wholesale");
assert!(response
.results
.values()
.any(|r| r.rule.name == "wholesale_final"));
let response = engine
.run("order/wholesale_order", Some(&now), HashMap::new(), false)
.expect("Cross-spec rule references now work correctly");
assert_eq!(response.spec_name, "order/wholesale_order");
let order_total = response
.results
.values()
.find(|r| r.rule.name == "order_total");
assert!(order_total.is_some(), "order_total rule should exist");
assert!(
order_total.unwrap().result.value().is_some(),
"order_total should have a value"
);
let response = engine
.run("order/comparison", Some(&now), HashMap::new(), false)
.expect("Evaluation should succeed (but rules will veto)");
assert_eq!(response.spec_name, "order/comparison");
assert!(response
.results
.values()
.any(|r| r.rule.name == "wholesale_total"));
assert!(response
.results
.values()
.any(|r| r.rule.name == "retail_total"));
assert!(response
.results
.values()
.any(|r| r.rule.name == "price_difference"));
let response = engine
.run("order/custom_wholesale", Some(&now), HashMap::new(), false)
.expect("Failed to evaluate custom_wholesale");
assert_eq!(response.spec_name, "order/custom_wholesale");
assert!(response
.results
.values()
.any(|r| r.rule.name == "custom_total"));
let response = engine
.run("complex/multi_reference", Some(&now), HashMap::new(), false)
.expect("Failed to evaluate multi_reference");
assert_eq!(response.spec_name, "complex/multi_reference");
let avg_discount = response
.results
.values()
.find(|r| r.rule.name == "avg_discount");
assert!(avg_discount.is_some(), "avg_discount rule should exist");
assert!(response
.results
.values()
.any(|r| r.rule.name == "price_range"));
}
#[test]
fn test_all_examples_parse() {
let engine = load_examples();
let specs = engine.list_specs();
assert!(
specs.len() >= 10,
"Expected at least 10 specs, found {}. Available: {:?}",
specs.len(),
specs
);
let key_specs = vec![
"simple_facts",
"rules_and_unless",
"stress_test",
"stress_test_extended",
];
let spec_names: Vec<&str> = specs.iter().map(|d| d.name.as_str()).collect();
for expected in key_specs {
assert!(
spec_names.contains(&expected),
"Expected spec '{}' not found. Available: {:?}",
expected,
spec_names
);
}
}