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    use std::str::FromStr;
104
105    fn dummy_source() -> Source {
106        Source::new(
107            "test",
108            Span {
109                start: 0,
110                end: 0,
111                line: 1,
112                col: 1,
113            },
114        )
115    }
116
117    fn dummy_evaluated_rule(name: &str) -> EvaluatedRule {
118        EvaluatedRule {
119            name: name.to_string(),
120            path: RulePath::new(vec![], name.to_string()),
121            default_expression: Expression::new(
122                ExpressionKind::Literal(Box::new(LiteralValue::from_bool(true))),
123                dummy_source(),
124            ),
125            unless_branches: vec![],
126            source_location: dummy_source(),
127            rule_type: primitive_number().clone(),
128        }
129    }
130
131    #[test]
132    fn test_response_serialization() {
133        let mut results = IndexMap::new();
134        results.insert(
135            "test_rule".to_string(),
136            RuleResult {
137                rule: dummy_evaluated_rule("test_rule"),
138                result: OperationResult::Value(Box::new(LiteralValue::number(
139                    Decimal::from_str("42").unwrap(),
140                ))),
141                data: vec![],
142                operations: vec![],
143                explanation: None,
144                rule_type: primitive_number().clone(),
145            },
146        );
147        let response = Response {
148            spec_name: "test_spec".to_string(),
149            spec_hash: None,
150            spec_effective_from: None,
151            spec_effective_to: None,
152            data: vec![],
153            results,
154        };
155
156        let json = serde_json::to_string(&response).unwrap();
157        assert!(json.contains("test_spec"));
158        assert!(json.contains("test_rule"));
159        assert!(json.contains("results"));
160    }
161
162    #[test]
163    fn test_response_filter_rules() {
164        let mut results = IndexMap::new();
165        results.insert(
166            "rule1".to_string(),
167            RuleResult {
168                rule: dummy_evaluated_rule("rule1"),
169                result: OperationResult::Value(Box::new(LiteralValue::from_bool(true))),
170                data: vec![],
171                operations: vec![],
172                explanation: None,
173                rule_type: primitive_boolean().clone(),
174            },
175        );
176        results.insert(
177            "rule2".to_string(),
178            RuleResult {
179                rule: dummy_evaluated_rule("rule2"),
180                result: OperationResult::Value(Box::new(LiteralValue::from_bool(false))),
181                data: vec![],
182                operations: vec![],
183                explanation: None,
184                rule_type: primitive_boolean().clone(),
185            },
186        );
187        let mut response = Response {
188            spec_name: "test_spec".to_string(),
189            spec_hash: None,
190            spec_effective_from: None,
191            spec_effective_to: None,
192            data: vec![],
193            results,
194        };
195
196        response.filter_rules(&["rule1".to_string()]);
197
198        assert_eq!(response.results.len(), 1);
199        assert_eq!(response.results.values().next().unwrap().rule.name, "rule1");
200    }
201
202    #[test]
203    fn test_rule_result_types() {
204        let success = RuleResult {
205            rule: dummy_evaluated_rule("rule1"),
206            result: OperationResult::Value(Box::new(LiteralValue::from_bool(true))),
207            data: vec![],
208            operations: vec![],
209            explanation: None,
210            rule_type: primitive_boolean().clone(),
211        };
212        assert!(matches!(success.result, OperationResult::Value(_)));
213
214        let missing = RuleResult {
215            rule: dummy_evaluated_rule("rule3"),
216            result: OperationResult::Veto(crate::evaluation::operations::VetoType::MissingData {
217                data: crate::planning::semantics::DataPath::new(vec![], "data1".to_string()),
218            }),
219            data: vec![DataGroup {
220                data_path: String::new(),
221                referencing_data_name: String::new(),
222                data: vec![crate::planning::semantics::Data {
223                    path: crate::planning::semantics::DataPath::new(vec![], "data1".to_string()),
224                    value: crate::planning::semantics::DataValue::Literal(
225                        crate::planning::semantics::LiteralValue::from_bool(false),
226                    ),
227                    source: None,
228                }],
229            }],
230            operations: vec![],
231            explanation: None,
232            rule_type: LemmaType::veto_type(),
233        };
234        assert_eq!(missing.data.len(), 1);
235        assert_eq!(missing.data[0].data[0].path.data, "data1");
236        assert!(matches!(missing.result, OperationResult::Veto(_)));
237
238        let veto = RuleResult {
239            rule: dummy_evaluated_rule("rule4"),
240            result: OperationResult::Veto(crate::evaluation::operations::VetoType::UserDefined {
241                message: Some("Vetoed".to_string()),
242            }),
243            data: vec![],
244            operations: vec![],
245            explanation: None,
246            rule_type: LemmaType::veto_type(),
247        };
248        assert_eq!(
249            veto.result,
250            OperationResult::Veto(crate::evaluation::operations::VetoType::UserDefined {
251                message: Some("Vetoed".to_string()),
252            })
253        );
254    }
255}