lemma-engine 0.8.12

A language that means business.
Documentation
use crate::evaluation::operations::{OperationRecord, OperationResult, VetoType};
use crate::parsing::ast::DateTimeValue;
use crate::planning::semantics::{DataPath, Expression, LemmaType, RulePath, Source};
use indexmap::IndexMap;
use serde::Serialize;
use std::collections::BTreeSet;

/// Rule info with resolved expressions for use in evaluation response.
/// Evaluation uses only semantics types; no parsing types.
#[derive(Debug, Clone, Serialize)]
pub struct EvaluatedRule {
    pub name: String,
    pub path: RulePath,
    pub default_expression: Expression,
    pub unless_branches: Vec<(Option<Expression>, Expression)>,
    pub source_location: Source,
    pub rule_type: LemmaType,
}

/// Grouped data from a specific spec (semantics types only).
#[derive(Debug, Clone, Serialize)]
pub struct DataGroup {
    pub data_path: String,
    pub referencing_data_name: String,
    pub data: Vec<crate::planning::semantics::Data>,
}

/// Response from evaluating a Lemma spec
#[derive(Debug, Clone, Serialize)]
pub struct Response {
    pub spec_name: String,
    pub spec_hash: Option<String>,
    pub spec_effective_from: Option<DateTimeValue>,
    pub spec_effective_to: Option<DateTimeValue>,
    pub data: Vec<DataGroup>,
    pub results: IndexMap<String, RuleResult>,
}

/// Result of evaluating a single rule (semantics types only).
#[derive(Debug, Clone, Serialize)]
pub struct RuleResult {
    #[serde(skip_serializing)]
    pub rule: EvaluatedRule,
    pub result: OperationResult,
    pub data: Vec<DataGroup>,
    #[serde(skip_serializing)]
    pub operations: Vec<OperationRecord>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub explanation: Option<crate::evaluation::explanation::Explanation>,
    /// Computed type of this rule's result (semantics).
    pub rule_type: LemmaType,
}

impl Response {
    /// Looks up a rule result by name.
    ///
    /// Returns an error if the rule is not found.
    pub fn get(&self, rule_name: &str) -> Result<&RuleResult, crate::error::Error> {
        self.results
            .get(rule_name)
            .ok_or_else(|| crate::error::Error::rule_not_found(rule_name, None::<String>))
    }

    pub fn add_result(&mut self, result: RuleResult) {
        self.results.insert(result.rule.name.clone(), result);
    }

    pub fn filter_rules(&mut self, rule_names: &[String]) {
        self.results.retain(|name, _| rule_names.contains(name));
    }

    /// All [`DataPath`]s reported as missing by any rule result (`VetoType::MissingData`).
    #[must_use]
    pub fn missing_data(&self) -> BTreeSet<DataPath> {
        self.missing_data_ordered().into_iter().collect()
    }

    /// [`DataPath`]s with `MissingData` vetos, in **rule result order** (matches evaluation order),
    /// first occurrence only.
    #[must_use]
    pub fn missing_data_ordered(&self) -> Vec<DataPath> {
        let mut seen = std::collections::HashSet::new();
        let mut out = Vec::new();
        for rr in self.results.values() {
            if let OperationResult::Veto(VetoType::MissingData { data }) = &rr.result {
                if seen.insert(data.clone()) {
                    out.push(data.clone());
                }
            }
        }
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::planning::semantics::{
        primitive_boolean, primitive_number, Expression, ExpressionKind, LemmaType, LiteralValue,
        RulePath, Span,
    };
    use rust_decimal::Decimal;
    use std::str::FromStr;

    fn dummy_source() -> Source {
        Source::new(
            "test",
            Span {
                start: 0,
                end: 0,
                line: 1,
                col: 1,
            },
        )
    }

    fn dummy_evaluated_rule(name: &str) -> EvaluatedRule {
        EvaluatedRule {
            name: name.to_string(),
            path: RulePath::new(vec![], name.to_string()),
            default_expression: Expression::new(
                ExpressionKind::Literal(Box::new(LiteralValue::from_bool(true))),
                dummy_source(),
            ),
            unless_branches: vec![],
            source_location: dummy_source(),
            rule_type: primitive_number().clone(),
        }
    }

    #[test]
    fn test_response_serialization() {
        let mut results = IndexMap::new();
        results.insert(
            "test_rule".to_string(),
            RuleResult {
                rule: dummy_evaluated_rule("test_rule"),
                result: OperationResult::Value(Box::new(LiteralValue::number(
                    Decimal::from_str("42").unwrap(),
                ))),
                data: vec![],
                operations: vec![],
                explanation: None,
                rule_type: primitive_number().clone(),
            },
        );
        let response = Response {
            spec_name: "test_spec".to_string(),
            spec_hash: None,
            spec_effective_from: None,
            spec_effective_to: None,
            data: vec![],
            results,
        };

        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("test_spec"));
        assert!(json.contains("test_rule"));
        assert!(json.contains("results"));
    }

    #[test]
    fn test_response_filter_rules() {
        let mut results = IndexMap::new();
        results.insert(
            "rule1".to_string(),
            RuleResult {
                rule: dummy_evaluated_rule("rule1"),
                result: OperationResult::Value(Box::new(LiteralValue::from_bool(true))),
                data: vec![],
                operations: vec![],
                explanation: None,
                rule_type: primitive_boolean().clone(),
            },
        );
        results.insert(
            "rule2".to_string(),
            RuleResult {
                rule: dummy_evaluated_rule("rule2"),
                result: OperationResult::Value(Box::new(LiteralValue::from_bool(false))),
                data: vec![],
                operations: vec![],
                explanation: None,
                rule_type: primitive_boolean().clone(),
            },
        );
        let mut response = Response {
            spec_name: "test_spec".to_string(),
            spec_hash: None,
            spec_effective_from: None,
            spec_effective_to: None,
            data: vec![],
            results,
        };

        response.filter_rules(&["rule1".to_string()]);

        assert_eq!(response.results.len(), 1);
        assert_eq!(response.results.values().next().unwrap().rule.name, "rule1");
    }

    #[test]
    fn test_rule_result_types() {
        let success = RuleResult {
            rule: dummy_evaluated_rule("rule1"),
            result: OperationResult::Value(Box::new(LiteralValue::from_bool(true))),
            data: vec![],
            operations: vec![],
            explanation: None,
            rule_type: primitive_boolean().clone(),
        };
        assert!(matches!(success.result, OperationResult::Value(_)));

        let missing = RuleResult {
            rule: dummy_evaluated_rule("rule3"),
            result: OperationResult::Veto(crate::evaluation::operations::VetoType::MissingData {
                data: crate::planning::semantics::DataPath::new(vec![], "data1".to_string()),
            }),
            data: vec![DataGroup {
                data_path: String::new(),
                referencing_data_name: String::new(),
                data: vec![crate::planning::semantics::Data {
                    path: crate::planning::semantics::DataPath::new(vec![], "data1".to_string()),
                    value: crate::planning::semantics::DataValue::Literal(
                        crate::planning::semantics::LiteralValue::from_bool(false),
                    ),
                    source: None,
                }],
            }],
            operations: vec![],
            explanation: None,
            rule_type: LemmaType::veto_type(),
        };
        assert_eq!(missing.data.len(), 1);
        assert_eq!(missing.data[0].data[0].path.data, "data1");
        assert!(matches!(missing.result, OperationResult::Veto(_)));

        let veto = RuleResult {
            rule: dummy_evaluated_rule("rule4"),
            result: OperationResult::Veto(crate::evaluation::operations::VetoType::UserDefined {
                message: Some("Vetoed".to_string()),
            }),
            data: vec![],
            operations: vec![],
            explanation: None,
            rule_type: LemmaType::veto_type(),
        };
        assert_eq!(
            veto.result,
            OperationResult::Veto(crate::evaluation::operations::VetoType::UserDefined {
                message: Some("Vetoed".to_string()),
            })
        );
    }
}