use lemma::parsing::ast::DateTimeValue;
use lemma::{Engine, TypeSpecification};
use std::collections::HashMap;
#[test]
fn test_type_system_with_imports_and_extensions() {
let mut engine = Engine::new();
let age_spec = r#"
spec age
type age: number
-> minimum 0
-> maximum 150
"#;
let test_types_spec = r#"
spec test_types
type age from age
type adult_age: age
-> minimum 21
fact age: [age]
fact adult_age: [adult_age]
fact twenties: [adult_age -> maximum 30]
rule total: age + adult_age + twenties
"#;
engine
.load(age_spec, lemma::SourceType::Labeled("age.lemma"))
.unwrap();
engine
.load(
test_types_spec,
lemma::SourceType::Labeled("test_types.lemma"),
)
.unwrap();
let now = DateTimeValue::now();
let mut facts = HashMap::new();
facts.insert("age".to_string(), "25".to_string());
facts.insert("adult_age".to_string(), "30".to_string());
facts.insert("twenties".to_string(), "25".to_string());
let response = engine
.run("test_types", Some(&now), facts, false)
.expect("Evaluation failed");
assert_eq!(response.spec_name, "test_types");
let total_rule = response
.results
.values()
.find(|r| r.rule.name == "total")
.expect("total rule not found");
assert_eq!(total_rule.result.value().unwrap().to_string(), "80");
}
#[test]
fn test_scale_type_default_before_unit_declarations() {
let mut engine = Engine::new();
engine
.load(
r#"
spec pricing
type money: scale
-> default 4 eur
-> unit eur 1
-> unit usd 1.19
fact price: [money]
rule doubled: price * 2
"#,
lemma::SourceType::Labeled("pricing.lemma"),
)
.expect("default before unit should be valid");
let now = DateTimeValue::now();
let plan = engine.get_plan("pricing", Some(&now)).unwrap();
let schema = plan.schema();
let (price_type, _) = schema.facts.get("price").expect("price fact in schema");
assert!(price_type.is_scale(), "price must be scale money type");
assert_eq!(price_type.name(), "money");
match &price_type.specifications {
TypeSpecification::Scale { units, default, .. } => {
let names: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
assert!(names.contains(&"eur") && names.contains(&"usd"));
assert!(
default.is_some(),
"default 4 eur must appear in schema after planning"
);
}
other => panic!("expected Scale, got {:?}", other),
}
}
#[test]
fn test_scale_type_default_after_unit_declarations() {
let mut engine = Engine::new();
engine
.load(
r#"
spec pricing
type money: scale
-> unit eur 1
-> unit usd 1.19
-> default 4 eur
fact price: [money]
rule doubled: price * 2
"#,
lemma::SourceType::Labeled("pricing.lemma"),
)
.expect("default after unit should be valid");
let now = DateTimeValue::now();
let plan = engine.get_plan("pricing", Some(&now)).unwrap();
let schema = plan.schema();
let (price_type, _) = schema.facts.get("price").expect("price fact in schema");
assert!(price_type.is_scale(), "price must be scale money type");
assert_eq!(price_type.name(), "money");
match &price_type.specifications {
TypeSpecification::Scale { units, default, .. } => {
let names: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
assert!(names.contains(&"eur") && names.contains(&"usd"));
assert!(
default.is_some(),
"default 4 eur must appear in schema after planning"
);
}
other => panic!("expected Scale, got {:?}", other),
}
}
#[test]
fn test_schema_returns_facts_in_definition_order() {
let mut engine = Engine::new();
engine
.load(
r#"
spec ordering
fact zebra: [number]
fact alpha: [number]
fact middle: [number]
rule total: zebra + alpha + middle
"#,
lemma::SourceType::Labeled("ordering.lemma"),
)
.unwrap();
let now = DateTimeValue::now();
let plan = engine.get_plan("ordering", Some(&now)).unwrap();
let schema = plan.schema();
let fact_names: Vec<&String> = schema.facts.keys().collect();
assert_eq!(
fact_names,
vec!["zebra", "alpha", "middle"],
"Facts should be in definition order, not alphabetical"
);
}
#[test]
fn test_schema_for_rules_returns_facts_in_definition_order() {
let mut engine = Engine::new();
engine
.load(
r#"
spec ordering
fact zebra: [number]
fact alpha: [number]
fact middle: [number]
rule total: zebra + alpha + middle
"#,
lemma::SourceType::Labeled("ordering.lemma"),
)
.unwrap();
let now = DateTimeValue::now();
let plan = engine.get_plan("ordering", Some(&now)).unwrap();
let schema = plan.schema_for_rules(&["total".to_string()]).unwrap();
let fact_names: Vec<&String> = schema.facts.keys().collect();
assert_eq!(
fact_names,
vec!["zebra", "alpha", "middle"],
"schema_for_rules should also preserve definition order"
);
}
#[test]
fn test_schema_reports_none_for_default_valued_facts() {
let mut engine = Engine::new();
engine
.load(
r#"
spec defaults
fact quantity: [number -> default 10]
fact name: [text]
fact price: 99
rule total: quantity * price
"#,
lemma::SourceType::Labeled("defaults.lemma"),
)
.unwrap();
let now = DateTimeValue::now();
let plan = engine.get_plan("defaults", Some(&now)).unwrap();
let schema = plan.schema();
let (_, quantity_val) = schema.facts.get("quantity").expect("quantity should exist");
assert!(
quantity_val.is_none(),
"Default-valued fact 'quantity' should have None in schema (needs user input)"
);
let (_, name_val) = schema.facts.get("name").expect("name should exist");
assert!(
name_val.is_none(),
"Type-only fact 'name' should have None in schema"
);
let (_, price_val) = schema.facts.get("price").expect("price should exist");
assert!(
price_val.is_some(),
"Explicit-valued fact 'price' should have Some in schema (skip in interactive)"
);
}
#[test]
fn test_schema_scale_default_reports_none() {
let mut engine = Engine::new();
engine
.load(
r#"
spec salary
type money: scale
-> unit eur 1
-> unit usd 1.19
-> default 3000 eur
fact salary: [money]
rule doubled: salary * 2
"#,
lemma::SourceType::Labeled("salary.lemma"),
)
.unwrap();
let now = DateTimeValue::now();
let plan = engine.get_plan("salary", Some(&now)).unwrap();
let schema = plan.schema();
let (_, salary_val) = schema.facts.get("salary").expect("salary should exist");
assert!(
salary_val.is_none(),
"Scale fact with type default should have None in schema"
);
}