use lemma::DateTimeValue;
use lemma::Engine;
use rust_decimal::Decimal;
use std::collections::HashMap;
#[test]
fn test_single_level_spec_ref_with_rule_reference() {
let mut engine = Engine::new();
let base_spec = r#"
spec pricing
data base_price: 100
data tax_rate: 21%
rule final_price: base_price * (1 + tax_rate)
"#;
let line_item_spec = r#"
spec line_item
uses pricing
data quantity: 10
rule line_total: pricing.final_price * quantity
"#;
engine
.load(
base_spec,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"pricing.lemma",
))),
)
.unwrap();
engine
.load(
line_item_spec,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"line_item.lemma",
))),
)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run(None, "line_item", Some(&now), HashMap::new(), true)
.unwrap();
let line_total = response
.results
.values()
.find(|r| r.rule.name == "line_total")
.unwrap();
assert!(!line_total.vetoed);
let lit = line_total
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(1210)
),
other => panic!("Expected Number for line_total, got {:?}", other),
}
}
#[test]
fn test_multi_level_spec_rule_reference() {
let mut engine = Engine::new();
let base_spec = r#"
spec base
data value: 100
rule doubled: value * 2
"#;
let middle_spec = r#"
spec middle
uses base_ref: base
rule middle_calc: base_ref.doubled + 50
"#;
let top_spec = r#"
spec top
uses middle_ref: middle
rule top_calc: middle_ref.middle_calc
"#;
engine.load(base_spec, lemma::SourceType::Volatile).unwrap();
engine
.load(middle_spec, lemma::SourceType::Volatile)
.unwrap();
engine.load(top_spec, lemma::SourceType::Volatile).unwrap();
let now = DateTimeValue::now();
let response = engine
.run(None, "top", Some(&now), HashMap::new(), true)
.unwrap();
let top_calc = response
.results
.values()
.find(|r| r.rule.name == "top_calc")
.expect("top_calc rule not found in results");
assert!(!top_calc.vetoed);
let lit = top_calc
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(250)
),
other => panic!("Expected Number for top_calc, got {:?}", other),
}
}
#[test]
fn test_data_spec_shorthand_rejected() {
let mut engine = Engine::new();
let specs = r#"
spec a
data x: spec other
"#;
let errs = engine.load(specs, lemma::SourceType::Volatile).unwrap_err();
let msg = errs
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("; ");
assert!(
(msg.contains("uses") && msg.contains("spec"))
|| msg.contains("Dotted paths require `with`"),
"expected spec-import or dotted-LHS rejection, got: {msg}"
);
}
#[test]
fn test_multi_level_data_access_through_spec_refs() {
let mut engine = Engine::new();
let base_spec = r#"
spec base
data value: 50
"#;
let middle_spec = r#"
spec middle
uses config: base
with config.value: 100
"#;
let top_spec = r#"
spec top
uses settings: middle
rule final_value: settings.config.value * 2
"#;
engine.load(base_spec, lemma::SourceType::Volatile).unwrap();
engine
.load(middle_spec, lemma::SourceType::Volatile)
.unwrap();
engine.load(top_spec, lemma::SourceType::Volatile).unwrap();
let now = DateTimeValue::now();
let response = engine
.run(None, "top", Some(&now), HashMap::new(), true)
.unwrap();
let final_value = response
.results
.values()
.find(|r| r.rule.name == "final_value")
.unwrap();
assert!(!final_value.vetoed);
let lit = final_value
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(200)
),
other => panic!("Expected Number for final_value, got {:?}", other),
}
}
#[test]
fn test_deep_nested_data_binding() {
let mut engine = Engine::new();
let pricing_spec = r#"
spec pricing
data base_price: 100
data tax_rate: 21%
rule final_price: base_price * (1 + tax_rate)
"#;
let line_item_spec = r#"
spec line_item
uses pricing
data quantity: 10
rule line_total: pricing.final_price * quantity
"#;
let order_spec = r#"
spec order
uses line: line_item
with line.pricing.tax_rate: 10%
with line.quantity: 5
rule order_total: line.line_total
"#;
engine
.load(pricing_spec, lemma::SourceType::Volatile)
.unwrap();
engine
.load(line_item_spec, lemma::SourceType::Volatile)
.unwrap();
engine
.load(order_spec, lemma::SourceType::Volatile)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run(None, "order", Some(&now), HashMap::new(), true)
.unwrap();
let order_total = response
.results
.values()
.find(|r| r.rule.name == "order_total")
.expect("order_total rule not found");
assert!(!order_total.vetoed);
let lit = order_total
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(550)
),
other => panic!("Expected Number for order_total, got {:?}", other),
}
}
#[test]
fn test_different_paths_different_results() {
let mut engine = Engine::new();
let base_spec = r#"
spec base
data price: 100
rule total: price * 1.21
"#;
let wrapper_spec = r#"
spec wrapper
uses base
"#;
let comparison_spec = r#"
spec comparison
uses path1: wrapper
uses path2: wrapper
with path2.base.price: 75
rule total1: path1.base.total
rule total2: path2.base.total
rule difference: total2 - total1
"#;
engine.load(base_spec, lemma::SourceType::Volatile).unwrap();
engine
.load(wrapper_spec, lemma::SourceType::Volatile)
.unwrap();
engine
.load(comparison_spec, lemma::SourceType::Volatile)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run(None, "comparison", Some(&now), HashMap::new(), true)
.unwrap();
let total1 = response
.results
.values()
.find(|r| r.rule.name == "total1")
.unwrap();
let total2 = response
.results
.values()
.find(|r| r.rule.name == "total2")
.unwrap();
let difference = response
.results
.values()
.find(|r| r.rule.name == "difference")
.unwrap();
assert!(!total1.vetoed);
let lit = total1
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(121)
),
other => panic!("Expected Number for total1, got {:?}", other),
}
assert!(!total2.vetoed);
let lit = total2
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::new(9075, 2)
),
other => panic!("Expected Number for total2, got {:?}", other),
}
assert!(!difference.vetoed);
let lit = difference
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::new(-3025, 2)
),
other => panic!("Expected Number for difference, got {:?}", other),
}
}
#[test]
fn test_multiple_independent_spec_refs() {
let mut engine = Engine::new();
let config1_spec = r#"
spec config1
data value: 100
rule doubled: value * 2
"#;
let config2_spec = r#"
spec config2
data value: 50
rule tripled: value * 3
"#;
let combined_spec = r#"
spec combined
uses c1: config1
uses c2: config2
rule sum: c1.doubled + c2.tripled
rule product: c1.value * c2.value
"#;
engine
.load(config1_spec, lemma::SourceType::Volatile)
.unwrap();
engine
.load(config2_spec, lemma::SourceType::Volatile)
.unwrap();
engine
.load(combined_spec, lemma::SourceType::Volatile)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run(None, "combined", Some(&now), HashMap::new(), true)
.unwrap();
let sum = response
.results
.values()
.find(|r| r.rule.name == "sum")
.unwrap();
let product = response
.results
.values()
.find(|r| r.rule.name == "product")
.unwrap();
assert!(!sum.vetoed);
let lit = sum
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(350)
),
other => panic!("Expected Number for sum, got {:?}", other),
}
assert!(!product.vetoed);
let lit = product
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(5000)
),
other => panic!("Expected Number for product, got {:?}", other),
}
}
#[test]
fn test_transitive_rule_dependencies() {
let mut engine = Engine::new();
let base_spec = r#"
spec base
data x: 10
rule x_squared: x * x
"#;
let middle_spec = r#"
spec middle
uses base_config: base
with base_config.x: 20
rule x_squared_plus_ten: base_config.x_squared + 10
"#;
let top_spec = r#"
spec top
uses middle_config: middle
rule final_result: middle_config.x_squared_plus_ten * 2
"#;
engine
.load(
base_spec,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("base.lemma"))),
)
.unwrap();
engine
.load(
middle_spec,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"middle.lemma",
))),
)
.unwrap();
engine
.load(
top_spec,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("top.lemma"))),
)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run(None, "top", Some(&now), HashMap::new(), true)
.unwrap();
let final_result = response
.results
.values()
.find(|r| r.rule.name == "final_result")
.unwrap();
assert!(!final_result.vetoed);
let lit = final_result
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(820)
),
other => panic!("Expected Number for final_result, got {:?}", other),
}
}
#[test]
fn test_same_spec_different_bindings() {
let mut engine = Engine::new();
let pricing_spec = r#"
spec pricing
data price: 100
data discount: 0%
rule final_price: price * (1 - discount)
"#;
let scenario_spec = r#"
spec scenarios
uses retail: pricing
with retail.discount: 5%
uses wholesale: pricing
with wholesale.discount: 15%
with wholesale.price: 80
rule retail_final: retail.final_price
rule wholesale_final: wholesale.final_price
rule price_difference: retail_final - wholesale_final
"#;
engine
.load(pricing_spec, lemma::SourceType::Volatile)
.unwrap();
engine
.load(scenario_spec, lemma::SourceType::Volatile)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run(None, "scenarios", Some(&now), HashMap::new(), true)
.unwrap();
let retail_final = response
.results
.values()
.find(|r| r.rule.name == "retail_final")
.unwrap();
let wholesale_final = response
.results
.values()
.find(|r| r.rule.name == "wholesale_final")
.unwrap();
let price_difference = response
.results
.values()
.find(|r| r.rule.name == "price_difference")
.unwrap();
assert!(!retail_final.vetoed);
let lit = retail_final
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(95)
),
other => panic!("Expected Number for retail_final, got {:?}", other),
}
assert!(!wholesale_final.vetoed);
let lit = wholesale_final
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(68)
),
other => panic!("Expected Number for wholesale_final, got {:?}", other),
}
assert!(!price_difference.vetoed);
let lit = price_difference
.trace
.as_ref()
.expect("explanation")
.result
.value()
.expect("value");
match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::ValueKind::Number(*n).as_decimal_magnitude().unwrap(),
Decimal::from(27)
),
other => panic!("Expected Number for price_difference, got {:?}", other),
}
}
#[test]
fn test_data_spec_shorthand_rejected_with_dotted_lhs() {
let mut engine = Engine::new();
let specs = r#"
spec a
rule x: 5
spec c
uses aa: a
rule y: aa.x > 1
spec d
uses cc: c
data cc.aa: spec a
rule yy: cc.y
"#;
let errs = engine.load(specs, lemma::SourceType::Volatile).unwrap_err();
let msg = errs
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("; ");
assert!(
(msg.contains("uses") && msg.contains("spec"))
|| msg.contains("Dotted paths require `with`"),
"expected spec-import or dotted-LHS rejection, got: {msg}"
);
}