use lemma::parsing::ast::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(),
false,
lemma::EvaluationRequest::default(),
)
.unwrap();
let line_total = response
.results
.values()
.find(|r| r.rule.name == "line_total")
.unwrap();
match &line_total.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(1210)
),
other => panic!("Expected Number for line_total, got {:?}", other),
},
other => panic!("Expected Value 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(),
false,
lemma::EvaluationRequest::default(),
)
.unwrap();
let top_calc = response
.results
.values()
.find(|r| r.rule.name == "top_calc")
.expect("top_calc rule not found in results");
match &top_calc.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(250)
),
other => panic!("Expected Number for top_calc, got {:?}", other),
},
other => panic!("Expected Value 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 `fill`"),
"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
fill 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(),
false,
lemma::EvaluationRequest::default(),
)
.unwrap();
let final_value = response
.results
.values()
.find(|r| r.rule.name == "final_value")
.unwrap();
match &final_value.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(200)
),
other => panic!("Expected Number for final_value, got {:?}", other),
},
other => panic!("Expected Value 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
fill line.pricing.tax_rate: 10%
fill 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(),
false,
lemma::EvaluationRequest::default(),
)
.unwrap();
let order_total = response
.results
.values()
.find(|r| r.rule.name == "order_total")
.expect("order_total rule not found");
match &order_total.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(550)
),
other => panic!("Expected Number for order_total, got {:?}", other),
},
other => panic!("Expected Value 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
fill 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(),
false,
lemma::EvaluationRequest::default(),
)
.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();
match &total1.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(121)
),
other => panic!("Expected Number for total1, got {:?}", other),
},
other => panic!("Expected Value for total1, got {:?}", other),
}
match &total2.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::new(9075, 2)
),
other => panic!("Expected Number for total2, got {:?}", other),
},
other => panic!("Expected Value for total2, got {:?}", other),
}
match &difference.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::new(-3025, 2)
),
other => panic!("Expected Number for difference, got {:?}", other),
},
other => panic!("Expected Value 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(),
false,
lemma::EvaluationRequest::default(),
)
.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();
match &sum.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(350)
),
other => panic!("Expected Number for sum, got {:?}", other),
},
other => panic!("Expected Value for sum, got {:?}", other),
}
match &product.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(5000)
),
other => panic!("Expected Number for product, got {:?}", other),
},
other => panic!("Expected Value 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
fill 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(),
false,
lemma::EvaluationRequest::default(),
)
.unwrap();
let final_result = response
.results
.values()
.find(|r| r.rule.name == "final_result")
.unwrap();
match &final_result.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(820)
),
other => panic!("Expected Number for final_result, got {:?}", other),
},
other => panic!("Expected Value 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
fill retail.discount: 5%
uses wholesale: pricing
fill wholesale.discount: 15%
fill 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(),
false,
lemma::EvaluationRequest::default(),
)
.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();
match &retail_final.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(95)
),
other => panic!("Expected Number for retail_final, got {:?}", other),
},
other => panic!("Expected Value for retail_final, got {:?}", other),
}
match &wholesale_final.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(68)
),
other => panic!("Expected Number for wholesale_final, got {:?}", other),
},
other => panic!("Expected Value for wholesale_final, got {:?}", other),
}
match &price_difference.result {
lemma::OperationResult::Value(lit) => match &lit.value {
lemma::ValueKind::Number(n) => assert_eq!(
lemma::commit_rational_to_decimal(n).unwrap(),
Decimal::from(27)
),
other => panic!("Expected Number for price_difference, got {:?}", other),
},
other => panic!("Expected Value 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 `fill`"),
"expected spec-import or dotted-LHS rejection, got: {msg}"
);
}