use lemma::evaluation::OperationResult;
use lemma::parsing::ast::DateTimeValue;
use lemma::Engine;
use lemma::ValueKind;
use lemma::VetoType;
use rust_decimal::Decimal;
use std::collections::HashMap;
#[test]
fn scale_comparison_converts_units_before_comparing() {
let code = r#"
spec pricing
data money: scale
-> unit eur 1
-> unit usd 1.19
data price: money
rule check: accept
unless price > 100 usd then veto "This price is too high."
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("test.lemma"))
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run(
"pricing",
Some(&now),
HashMap::from([("price".to_string(), "100 eur".to_string())]),
false,
)
.unwrap();
let rule_result = response
.results
.values()
.find(|r| r.rule.name == "check")
.unwrap();
assert_eq!(
rule_result.result,
OperationResult::Veto(VetoType::UserDefined {
message: Some("This price is too high.".to_string()),
})
);
}
#[test]
fn scale_data_value_rejects_unknown_unit() {
let code = r#"
spec pricing
data money: scale
-> unit eur 1
-> unit usd 1.19
data price: money
rule check: accept
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("test.lemma"))
.unwrap();
let now = DateTimeValue::now();
let err = engine
.run(
"pricing",
Some(&now),
HashMap::from([("price".to_string(), "100 btc".to_string())]),
false,
)
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("btc"), "actual error: {msg}");
}
#[test]
fn scale_in_operator_converts_units() {
let code = r#"
spec pricing
data money: scale
-> unit eur 1
-> unit usd 1.19
rule price_usd: 100 eur in usd
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("test.lemma"))
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run("pricing", Some(&now), HashMap::new(), false)
.unwrap();
let rule_result = response
.results
.values()
.find(|r| r.rule.name == "price_usd")
.unwrap();
let (value, lemma_type) = match &rule_result.result {
OperationResult::Value(lit) => (&lit.value, &lit.lemma_type),
other => panic!("Expected a Value result, got: {:?}", other),
};
assert!(
lemma_type.is_scale(),
"Expected scale type, got: {lemma_type:?}"
);
let (amount, unit) = match value {
ValueKind::Scale(amount, unit) => (amount, unit),
other => panic!("Expected a scale value, got: {other:?}"),
};
assert_eq!(*amount, Decimal::from(119));
assert_eq!(unit.as_str(), "usd");
}
#[test]
fn scale_add_subtract_converts_units_when_same_family() {
let code = r#"
spec t
data money: scale -> unit eur 1.00 -> unit usd 1.19
data gross: 7600 usd
data pension: 0 eur
rule taxable: gross - pension
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("test.lemma"))
.unwrap();
let now = DateTimeValue::now();
let response = engine.run("t", Some(&now), HashMap::new(), false).unwrap();
let rule_result = response
.results
.values()
.find(|r| r.rule.name == "taxable")
.unwrap();
match &rule_result.result {
OperationResult::Value(lit) => {
let (amount, unit) = match &lit.value {
ValueKind::Scale(a, u) => (a, u),
other => panic!("expected scale, got {other:?}"),
};
assert_eq!(unit.as_str(), "usd", "result unit follows left operand");
assert_eq!(*amount, Decimal::from(7600), "7600 usd - 0 eur = 7600 usd");
}
OperationResult::Veto(msg) => panic!("expected Value, got Veto: {msg:?}"),
}
}
#[test]
fn scale_in_operator_rejects_unknown_unit() {
let code = r#"
spec pricing
data money: scale
-> unit eur 1
-> unit usd 1.19
rule price_gbp: 100 eur in gbp
"#;
let mut engine = Engine::new();
let load_err = engine
.load(code, lemma::SourceType::Labeled("test.lemma"))
.unwrap_err();
let msg = load_err
.errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("; ");
assert!(msg.contains("Unknown unit 'gbp'"), "actual error: {msg}");
assert!(msg.contains("Valid units:"), "actual error: {msg}");
}
#[test]
fn named_scale_type_comparison_with_unit_literal() {
let code = r#"
spec shipping
data weight: scale -> unit kilogram 1.0
data package_weight: 2.5 kilogram
rule base_shipping: 5.99
unless package_weight > 1 kilogram then 8.99
unless package_weight > 5 kilogram then 15.99
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("test.lemma"))
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run("shipping", Some(&now), HashMap::new(), false)
.unwrap();
let rule_result = response
.results
.values()
.find(|r| r.rule.name == "base_shipping")
.unwrap();
match &rule_result.result {
OperationResult::Value(v) => match &v.value {
ValueKind::Number(d) => {
assert_eq!(*d, Decimal::new(899, 2));
}
other => panic!("Expected Number value, got {:?}", other),
},
OperationResult::Veto(reason) => panic!("Expected value, got Veto({:?})", reason),
}
}
#[test]
fn named_scale_type_arithmetic_within_same_family() {
let code = r#"
spec shipping
data money: scale -> unit USD 1.00
data base_fee: 5.99 USD
data surcharge: 2.00 USD
rule total: base_fee + surcharge
"#;
let mut engine = Engine::new();
engine
.load(code, lemma::SourceType::Labeled("test.lemma"))
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run("shipping", Some(&now), HashMap::new(), false)
.unwrap();
let rule_result = response
.results
.values()
.find(|r| r.rule.name == "total")
.unwrap();
match &rule_result.result {
OperationResult::Value(v) => match &v.value {
ValueKind::Scale(d, _) => {
assert_eq!(*d, Decimal::new(799, 2));
}
other => panic!("Expected Scale value, got {:?}", other),
},
OperationResult::Veto(reason) => panic!("Expected value, got Veto({:?})", reason),
}
}