Skip to main content

lemma/evaluation/
response.rs

1use crate::evaluation::operations::{OperationRecord, OperationResult};
2use crate::parsing::ast::DateTimeValue;
3use crate::planning::semantics::{Expression, Fact, LemmaType, RulePath, Source};
4use indexmap::IndexMap;
5use serde::Serialize;
6
7/// Rule info with resolved expressions for use in evaluation response.
8/// Evaluation uses only semantics types; no parsing types.
9#[derive(Debug, Clone, Serialize)]
10pub struct EvaluatedRule {
11    pub name: String,
12    pub path: RulePath,
13    pub default_expression: Expression,
14    pub unless_branches: Vec<(Option<Expression>, Expression)>,
15    pub source_location: Source,
16    pub rule_type: LemmaType,
17}
18
19/// Facts from a specific spec (semantics types only).
20#[derive(Debug, Clone, Serialize)]
21pub struct Facts {
22    pub fact_path: String,
23    pub referencing_fact_name: String,
24    pub facts: Vec<Fact>,
25}
26
27/// Response from evaluating a Lemma spec
28#[derive(Debug, Clone, Serialize)]
29pub struct Response {
30    pub spec_name: String,
31    pub spec_hash: Option<String>,
32    pub spec_effective_from: Option<DateTimeValue>,
33    pub spec_effective_to: Option<DateTimeValue>,
34    pub facts: Vec<Facts>,
35    pub results: IndexMap<String, RuleResult>,
36}
37
38/// Result of evaluating a single rule (semantics types only).
39#[derive(Debug, Clone, Serialize)]
40pub struct RuleResult {
41    #[serde(skip_serializing)]
42    pub rule: EvaluatedRule,
43    pub result: OperationResult,
44    pub facts: Vec<Fact>,
45    #[serde(skip_serializing)]
46    pub operations: Vec<OperationRecord>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub explanation: Option<crate::evaluation::explanation::Explanation>,
49    /// Computed type of this rule's result (semantics).
50    pub rule_type: LemmaType,
51}
52
53impl Response {
54    pub fn add_result(&mut self, result: RuleResult) {
55        self.results.insert(result.rule.name.clone(), result);
56    }
57
58    pub fn filter_rules(&mut self, rule_names: &[String]) {
59        self.results.retain(|name, _| rule_names.contains(name));
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::planning::semantics::{
67        primitive_boolean, primitive_number, Expression, ExpressionKind, LemmaType, LiteralValue,
68        RulePath, Span,
69    };
70    use rust_decimal::Decimal;
71    use std::str::FromStr;
72
73    fn dummy_source() -> Source {
74        Source::new(
75            "test",
76            Span {
77                start: 0,
78                end: 0,
79                line: 1,
80                col: 1,
81            },
82        )
83    }
84
85    fn dummy_evaluated_rule(name: &str) -> EvaluatedRule {
86        EvaluatedRule {
87            name: name.to_string(),
88            path: RulePath::new(vec![], name.to_string()),
89            default_expression: Expression::new(
90                ExpressionKind::Literal(Box::new(LiteralValue::from_bool(true))),
91                dummy_source(),
92            ),
93            unless_branches: vec![],
94            source_location: dummy_source(),
95            rule_type: primitive_number().clone(),
96        }
97    }
98
99    #[test]
100    fn test_response_serialization() {
101        let mut results = IndexMap::new();
102        results.insert(
103            "test_rule".to_string(),
104            RuleResult {
105                rule: dummy_evaluated_rule("test_rule"),
106                result: OperationResult::Value(Box::new(LiteralValue::number(
107                    Decimal::from_str("42").unwrap(),
108                ))),
109                facts: vec![],
110                operations: vec![],
111                explanation: None,
112                rule_type: primitive_number().clone(),
113            },
114        );
115        let response = Response {
116            spec_name: "test_spec".to_string(),
117            spec_hash: None,
118            spec_effective_from: None,
119            spec_effective_to: None,
120            facts: vec![],
121            results,
122        };
123
124        let json = serde_json::to_string(&response).unwrap();
125        assert!(json.contains("test_spec"));
126        assert!(json.contains("test_rule"));
127        assert!(json.contains("results"));
128    }
129
130    #[test]
131    fn test_response_filter_rules() {
132        let mut results = IndexMap::new();
133        results.insert(
134            "rule1".to_string(),
135            RuleResult {
136                rule: dummy_evaluated_rule("rule1"),
137                result: OperationResult::Value(Box::new(LiteralValue::from_bool(true))),
138                facts: vec![],
139                operations: vec![],
140                explanation: None,
141                rule_type: primitive_boolean().clone(),
142            },
143        );
144        results.insert(
145            "rule2".to_string(),
146            RuleResult {
147                rule: dummy_evaluated_rule("rule2"),
148                result: OperationResult::Value(Box::new(LiteralValue::from_bool(false))),
149                facts: vec![],
150                operations: vec![],
151                explanation: None,
152                rule_type: primitive_boolean().clone(),
153            },
154        );
155        let mut response = Response {
156            spec_name: "test_spec".to_string(),
157            spec_hash: None,
158            spec_effective_from: None,
159            spec_effective_to: None,
160            facts: vec![],
161            results,
162        };
163
164        response.filter_rules(&["rule1".to_string()]);
165
166        assert_eq!(response.results.len(), 1);
167        assert_eq!(response.results.values().next().unwrap().rule.name, "rule1");
168    }
169
170    #[test]
171    fn test_rule_result_types() {
172        let success = RuleResult {
173            rule: dummy_evaluated_rule("rule1"),
174            result: OperationResult::Value(Box::new(LiteralValue::from_bool(true))),
175            facts: vec![],
176            operations: vec![],
177            explanation: None,
178            rule_type: primitive_boolean().clone(),
179        };
180        assert!(matches!(success.result, OperationResult::Value(_)));
181
182        let missing = RuleResult {
183            rule: dummy_evaluated_rule("rule3"),
184            result: OperationResult::Veto(Some("Missing fact: fact1".to_string())),
185            facts: vec![crate::planning::semantics::Fact {
186                path: crate::planning::semantics::FactPath::new(vec![], "fact1".to_string()),
187                value: crate::planning::semantics::FactValue::Literal(
188                    crate::planning::semantics::LiteralValue::from_bool(false),
189                ),
190                source: None,
191            }],
192            operations: vec![],
193            explanation: None,
194            rule_type: LemmaType::veto_type(),
195        };
196        assert_eq!(missing.facts.len(), 1);
197        assert_eq!(missing.facts[0].path.fact, "fact1");
198        assert!(matches!(missing.result, OperationResult::Veto(_)));
199
200        let veto = RuleResult {
201            rule: dummy_evaluated_rule("rule4"),
202            result: OperationResult::Veto(Some("Vetoed".to_string())),
203            facts: vec![],
204            operations: vec![],
205            explanation: None,
206            rule_type: LemmaType::veto_type(),
207        };
208        assert_eq!(
209            veto.result,
210            OperationResult::Veto(Some("Vetoed".to_string()))
211        );
212    }
213}