Skip to main content

lemma/evaluation/
response.rs

1use crate::evaluation::operations::{OperationRecord, OperationResult, VetoType};
2use crate::parsing::ast::DateTimeValue;
3use crate::planning::semantics::{DataPath, Expression, LemmaType, RulePath, Source};
4use indexmap::IndexMap;
5use serde::Serialize;
6use std::collections::BTreeSet;
7
8/// Rule info with resolved expressions for use in evaluation response.
9/// Evaluation uses only semantics types; no parsing types.
10#[derive(Debug, Clone, Serialize)]
11pub struct EvaluatedRule {
12    pub name: String,
13    pub path: RulePath,
14    pub default_expression: Expression,
15    pub unless_branches: Vec<(Option<Expression>, Expression)>,
16    pub source_location: Source,
17    pub rule_type: LemmaType,
18}
19
20/// Grouped data from a specific spec (semantics types only).
21#[derive(Debug, Clone, Serialize)]
22pub struct DataGroup {
23    pub data_path: String,
24    pub referencing_data_name: String,
25    pub data: Vec<crate::planning::semantics::Data>,
26}
27
28/// Response from evaluating a Lemma spec
29#[derive(Debug, Clone, Serialize)]
30pub struct Response {
31    pub spec_name: String,
32    pub spec_hash: Option<String>,
33    pub spec_effective_from: Option<DateTimeValue>,
34    pub spec_effective_to: Option<DateTimeValue>,
35    pub data: Vec<DataGroup>,
36    pub results: IndexMap<String, RuleResult>,
37}
38
39/// Result of evaluating a single rule (semantics types only).
40#[derive(Debug, Clone, Serialize)]
41pub struct RuleResult {
42    #[serde(skip_serializing)]
43    pub rule: EvaluatedRule,
44    pub result: OperationResult,
45    pub data: Vec<DataGroup>,
46    #[serde(skip_serializing)]
47    pub operations: Vec<OperationRecord>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub explanation: Option<crate::evaluation::explanation::Explanation>,
50    /// Computed type of this rule's result (semantics).
51    pub rule_type: LemmaType,
52}
53
54impl Response {
55    /// Looks up a rule result by name.
56    ///
57    /// Returns an error if the rule is not found.
58    pub fn get(&self, rule_name: &str) -> Result<&RuleResult, crate::error::Error> {
59        self.results
60            .get(rule_name)
61            .ok_or_else(|| crate::error::Error::rule_not_found(rule_name, None::<String>))
62    }
63
64    pub fn add_result(&mut self, result: RuleResult) {
65        self.results.insert(result.rule.name.clone(), result);
66    }
67
68    pub fn filter_rules(&mut self, rule_names: &[String]) {
69        self.results.retain(|name, _| rule_names.contains(name));
70    }
71
72    /// All [`DataPath`]s reported as missing by any rule result (`VetoType::MissingData`).
73    #[must_use]
74    pub fn missing_data(&self) -> BTreeSet<DataPath> {
75        self.missing_data_ordered().into_iter().collect()
76    }
77
78    /// [`DataPath`]s with `MissingData` vetos, in **rule result order** (matches evaluation order),
79    /// first occurrence only.
80    #[must_use]
81    pub fn missing_data_ordered(&self) -> Vec<DataPath> {
82        let mut seen = std::collections::HashSet::new();
83        let mut out = Vec::new();
84        for rr in self.results.values() {
85            if let OperationResult::Veto(VetoType::MissingData { data }) = &rr.result {
86                if seen.insert(data.clone()) {
87                    out.push(data.clone());
88                }
89            }
90        }
91        out
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::planning::semantics::{
99        primitive_boolean, primitive_number, Expression, ExpressionKind, LemmaType, LiteralValue,
100        RulePath, Span,
101    };
102    use rust_decimal::Decimal;
103
104    fn dummy_source() -> Source {
105        Source::new(
106            crate::parsing::source::SourceType::Volatile,
107            Span {
108                start: 0,
109                end: 0,
110                line: 1,
111                col: 1,
112            },
113        )
114    }
115
116    fn dummy_evaluated_rule(name: &str) -> EvaluatedRule {
117        EvaluatedRule {
118            name: name.to_string(),
119            path: RulePath::new(vec![], name.to_string()),
120            default_expression: Expression::new(
121                ExpressionKind::Literal(Box::new(LiteralValue::from_bool(true))),
122                dummy_source(),
123            ),
124            unless_branches: vec![],
125            source_location: dummy_source(),
126            rule_type: primitive_number().clone(),
127        }
128    }
129
130    #[test]
131    fn test_response_serialization() {
132        let mut results = IndexMap::new();
133        results.insert(
134            "test_rule".to_string(),
135            RuleResult {
136                rule: dummy_evaluated_rule("test_rule"),
137                result: OperationResult::Value(Box::new(LiteralValue::number_from_decimal(
138                    Decimal::from(42),
139                ))),
140                data: vec![],
141                operations: vec![],
142                explanation: None,
143                rule_type: primitive_number().clone(),
144            },
145        );
146        let response = Response {
147            spec_name: "test_spec".to_string(),
148            spec_hash: None,
149            spec_effective_from: None,
150            spec_effective_to: None,
151            data: vec![],
152            results,
153        };
154
155        let json = serde_json::to_string(&response).unwrap();
156        assert!(json.contains("test_spec"));
157        assert!(json.contains("test_rule"));
158        assert!(json.contains("results"));
159    }
160
161    #[test]
162    fn test_response_filter_rules() {
163        let mut results = IndexMap::new();
164        results.insert(
165            "rule1".to_string(),
166            RuleResult {
167                rule: dummy_evaluated_rule("rule1"),
168                result: OperationResult::Value(Box::new(LiteralValue::from_bool(true))),
169                data: vec![],
170                operations: vec![],
171                explanation: None,
172                rule_type: primitive_boolean().clone(),
173            },
174        );
175        results.insert(
176            "rule2".to_string(),
177            RuleResult {
178                rule: dummy_evaluated_rule("rule2"),
179                result: OperationResult::Value(Box::new(LiteralValue::from_bool(false))),
180                data: vec![],
181                operations: vec![],
182                explanation: None,
183                rule_type: primitive_boolean().clone(),
184            },
185        );
186        let mut response = Response {
187            spec_name: "test_spec".to_string(),
188            spec_hash: None,
189            spec_effective_from: None,
190            spec_effective_to: None,
191            data: vec![],
192            results,
193        };
194
195        response.filter_rules(&["rule1".to_string()]);
196
197        assert_eq!(response.results.len(), 1);
198        assert_eq!(response.results.values().next().unwrap().rule.name, "rule1");
199    }
200
201    #[test]
202    fn test_rule_result_types() {
203        let success = RuleResult {
204            rule: dummy_evaluated_rule("rule1"),
205            result: OperationResult::Value(Box::new(LiteralValue::from_bool(true))),
206            data: vec![],
207            operations: vec![],
208            explanation: None,
209            rule_type: primitive_boolean().clone(),
210        };
211        assert!(matches!(success.result, OperationResult::Value(_)));
212
213        let missing = RuleResult {
214            rule: dummy_evaluated_rule("rule3"),
215            result: OperationResult::Veto(crate::evaluation::operations::VetoType::MissingData {
216                data: crate::planning::semantics::DataPath::new(vec![], "data1".to_string()),
217            }),
218            data: vec![DataGroup {
219                data_path: String::new(),
220                referencing_data_name: String::new(),
221                data: vec![crate::planning::semantics::Data {
222                    path: crate::planning::semantics::DataPath::new(vec![], "data1".to_string()),
223                    value: crate::planning::semantics::DataValue::from_bound_literal(
224                        crate::planning::semantics::LiteralValue::from_bool(false),
225                    ),
226                    source: None,
227                }],
228            }],
229            operations: vec![],
230            explanation: None,
231            rule_type: LemmaType::veto_type(),
232        };
233        assert_eq!(missing.data.len(), 1);
234        assert_eq!(missing.data[0].data[0].path.data, "data1");
235        assert!(matches!(missing.result, OperationResult::Veto(_)));
236
237        let veto = RuleResult {
238            rule: dummy_evaluated_rule("rule4"),
239            result: OperationResult::Veto(crate::evaluation::operations::VetoType::UserDefined {
240                message: Some("Vetoed".to_string()),
241            }),
242            data: vec![],
243            operations: vec![],
244            explanation: None,
245            rule_type: LemmaType::veto_type(),
246        };
247        assert_eq!(
248            veto.result,
249            OperationResult::Veto(crate::evaluation::operations::VetoType::UserDefined {
250                message: Some("Vetoed".to_string()),
251            })
252        );
253    }
254}