Skip to main content

lemma/evaluation/
response.rs

1use crate::computation::{OperationResult, VetoType};
2use crate::evaluation::explanations::Explanation;
3
4use crate::parsing::ast::DateTimeValue;
5use crate::planning::semantics::{LemmaType, LiteralValue, RulePath, Source};
6use crate::planning::unit_family::FamilyUnitCatalog;
7use crate::result_value::{
8    rule_result_value_failure_message, rule_result_value_from_literal, RuleResultValue,
9    RuleResultValueFailure,
10};
11use indexmap::IndexMap;
12use std::sync::Arc;
13
14/// Rule info with resolved expressions for use in evaluation response.
15/// Evaluation uses only semantics types; no parsing types.
16#[derive(Debug, Clone)]
17pub struct EvaluatedRule {
18    pub name: String,
19    pub path: RulePath,
20    pub source_location: Source,
21    pub rule_type: Arc<LemmaType>,
22}
23
24/// Response from evaluating a Lemma spec
25#[derive(Debug, Clone)]
26pub struct Response {
27    pub spec_name: String,
28    pub effective: String,
29    /// Declared temporal window `[spec_effective_from, spec_effective_to)` of the
30    /// resolved spec version. Set by [`crate::Engine::run`] after evaluation;
31    /// `None` here (evaluation-internal construction) until that assignment.
32    pub spec_effective_from: Option<DateTimeValue>,
33    pub spec_effective_to: Option<DateTimeValue>,
34    pub results: IndexMap<String, RuleResult>,
35}
36
37/// Result of evaluating a single rule. Struct fields match the API JSON shape.
38#[derive(Debug, Clone)]
39pub struct RuleResult {
40    pub rule: EvaluatedRule,
41    pub veto_detail: Option<VetoType>,
42
43    pub vetoed: bool,
44    pub veto_reason: Option<String>,
45    pub rule_type: String,
46
47    /// Flattened value fields, including `result` when the rule is not vetoed.
48    pub result: Option<RuleResultValue>,
49    pub explanation: Option<Explanation>,
50    /// Unbound caller data paths still live for this rule under the current run data
51    /// (`DataPath::input_key` strings; subset of `Show.data` keys with non-empty
52    /// `needed_by_rules`). Ordered by evaluation / decision-tree preorder (first key =
53    /// next fact the live tree needs); `Show.data` stays declaration order.
54    missing_data: Vec<String>,
55}
56
57impl RuleResult {
58    /// Engine-rendered one-liner from the flattened [`RuleResultValue`].
59    #[must_use]
60    pub fn result(&self) -> Option<&str> {
61        self.result
62            .as_ref()
63            .and_then(|value| value.result.as_deref())
64    }
65
66    /// True when this result is a [`VetoType::MissingData`] veto.
67    ///
68    /// Value and non-`MissingData` vetoes are settled answers; leftover live keys in
69    /// [`Self::missing_data`] must not drive prompts or human "Missing data" display.
70    #[must_use]
71    pub fn is_missing_data(&self) -> bool {
72        matches!(
73            self.veto_detail.as_ref(),
74            Some(VetoType::MissingData { .. })
75        )
76    }
77
78    /// Unbound caller data paths still live for this rule (`DataPath::input_key`),
79    /// in evaluation / decision-tree order (contrast `Show.data` declaration order).
80    #[must_use]
81    pub fn missing_data(&self) -> &[String] {
82        &self.missing_data
83    }
84
85    /// Build a [`RuleResult`] for API output from a rule evaluation result.
86    ///
87    /// Measure and ratio payloads expand into every unit in the result type's family.
88    pub(crate) fn from_operation_result(
89        rule: EvaluatedRule,
90        operation_result: &OperationResult,
91        rule_type: &LemmaType,
92        family_units: &FamilyUnitCatalog,
93        explanation: Option<Explanation>,
94        missing_data: Vec<String>,
95    ) -> Self {
96        match operation_result {
97            OperationResult::Veto(VetoType::MissingData { data, .. }) => {
98                let key = data.input_key();
99                if !missing_data.iter().any(|listed| listed == &key) {
100                    panic!("BUG: MissingData path {key} not in missing_data {missing_data:?}");
101                }
102            }
103            _ => {
104                if !missing_data.is_empty() {
105                    panic!(
106                        "BUG: missing_data must be empty when result is not MissingData: {missing_data:?}"
107                    );
108                }
109            }
110        }
111        let rule_type_name = rule_type.name().to_string();
112        match operation_result {
113            OperationResult::Veto(veto) => Self {
114                rule,
115                veto_detail: Some(veto.clone()),
116                vetoed: true,
117                veto_reason: match &veto {
118                    VetoType::UserDefined { message: None } => None,
119                    _ => Some(veto.to_string()),
120                },
121                rule_type: rule_type_name,
122                result: None,
123                explanation,
124                missing_data,
125            },
126            OperationResult::Value(bound) => {
127                match rule_result_value_from_literal(&bound.to_literal(), rule_type, family_units) {
128                    Ok(value) => Self {
129                        rule,
130                        veto_detail: None,
131                        vetoed: false,
132                        veto_reason: None,
133                        rule_type: rule_type_name,
134                        result: Some(value),
135                        explanation,
136                        missing_data,
137                    },
138                    Err(failure) => vetoed_rule_result_for_rule_result_value_failure(
139                        rule,
140                        rule_type,
141                        family_units,
142                        explanation,
143                        failure,
144                        missing_data,
145                    ),
146                }
147            }
148        }
149    }
150
151    /// Reconstruct the evaluated [`LiteralValue`] from committed [`RuleResultValue`] fields.
152    ///
153    /// Panics if the rule is vetoed or fields cannot be reconstructed.
154    pub fn to_literal(&self) -> LiteralValue {
155        assert!(
156            !self.vetoed,
157            "BUG: to_literal called on vetoed rule '{}'",
158            self.rule.name
159        );
160        let value = self
161            .result
162            .as_ref()
163            .unwrap_or_else(|| panic!("BUG: non-vetoed rule '{}' missing result", self.rule.name));
164        value.to_literal(&self.rule.rule_type)
165    }
166}
167
168fn vetoed_rule_result_for_rule_result_value_failure(
169    rule: EvaluatedRule,
170    rule_type: &LemmaType,
171    family_units: &FamilyUnitCatalog,
172    explanation: Option<Explanation>,
173    failure: RuleResultValueFailure,
174    missing_data: Vec<String>,
175) -> RuleResult {
176    RuleResult::from_operation_result(
177        rule,
178        &OperationResult::Veto(VetoType::computation(
179            rule_result_value_failure_message(failure).to_string(),
180        )),
181        rule_type,
182        family_units,
183        explanation,
184        missing_data,
185    )
186}
187
188impl Response {
189    /// Looks up a rule result by name.
190    ///
191    /// Returns an error if the rule is not found.
192    pub fn get(&self, rule_name: &str) -> Result<&RuleResult, crate::error::Error> {
193        self.results
194            .get(rule_name)
195            .ok_or_else(|| crate::error::Error::rule_not_found(rule_name, None::<String>))
196    }
197
198    pub fn add_result(&mut self, result: RuleResult) {
199        self.results.insert(result.rule.name.clone(), result);
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::literals::DateGranularity;
207    use crate::parsing::ast::Span;
208    use crate::planning::semantics::{
209        primitive_number_arc, BaseMeasureVector, DataPath, LemmaType, LiteralValue, MeasureUnit,
210        MeasureUnits, RatioUnit, RatioUnits, RulePath, TypeExtends, TypeSpecification, ValueKind,
211    };
212    use crate::planning::unit_family::FamilyUnitCatalog;
213    use crate::planning::unit_index::UnitIndex;
214    use rust_decimal::Decimal;
215    use std::str::FromStr;
216    use std::sync::Arc;
217
218    fn dec(s: &str) -> Decimal {
219        Decimal::from_str(s).expect("BUG: test decimal literal")
220    }
221
222    fn empty_family_catalog() -> FamilyUnitCatalog {
223        FamilyUnitCatalog::default()
224    }
225
226    fn family_catalog_for_index(index: &UnitIndex) -> FamilyUnitCatalog {
227        FamilyUnitCatalog::build(index)
228    }
229
230    fn family_catalog_for_money(money: &LemmaType) -> FamilyUnitCatalog {
231        family_catalog_for_index(&unit_index_for_money(money))
232    }
233
234    fn family_catalog_for_ratio(ratio_type: &LemmaType) -> FamilyUnitCatalog {
235        family_catalog_for_index(&unit_index_for_ratio(ratio_type))
236    }
237
238    fn unit_index_for_money(money: &LemmaType) -> UnitIndex {
239        let mut index = UnitIndex::new();
240        let arc = Arc::new(money.clone());
241        for unit_name in money.measure_unit_names().into_iter().flatten() {
242            index
243                .merge_measure_unit(unit_name.to_string(), &arc, "money", None, "money")
244                .expect("test money unit index");
245        }
246        index
247    }
248
249    fn unit_index_for_ratio(ratio_type: &LemmaType) -> UnitIndex {
250        use crate::planning::semantics::primitive_ratio_arc;
251        let mut index = UnitIndex::new();
252        let arc = Arc::new(ratio_type.clone());
253        let type_name = ratio_type.name();
254        for unit_name in ratio_type.ratio_unit_names().into_iter().flatten() {
255            index
256                .merge_ratio_unit(
257                    unit_name.to_string(),
258                    &arc,
259                    &type_name,
260                    None,
261                    primitive_ratio_arc(),
262                )
263                .expect("test ratio unit index");
264        }
265        index
266    }
267
268    fn dummy_source() -> Source {
269        Source::new(
270            crate::parsing::source::SourceType::Volatile,
271            Span {
272                start: 0,
273                end: 0,
274                line: 1,
275                col: 1,
276            },
277        )
278    }
279
280    fn dummy_evaluated_rule(name: &str, rule_type: &LemmaType) -> EvaluatedRule {
281        EvaluatedRule {
282            name: name.to_string(),
283            path: RulePath::new(vec![], name.to_string()),
284            source_location: dummy_source(),
285            rule_type: Arc::new(rule_type.clone()),
286        }
287    }
288
289    #[test]
290    fn test_response_serialization() {
291        let mut results = IndexMap::new();
292        results.insert(
293            "test_rule".to_string(),
294            RuleResult::from_operation_result(
295                dummy_evaluated_rule("test_rule", primitive_number_arc().as_ref()),
296                &OperationResult::from_literal(LiteralValue::number_from_decimal(Decimal::from(
297                    42,
298                ))),
299                primitive_number_arc().as_ref(),
300                &empty_family_catalog(),
301                None,
302                Vec::new(),
303            ),
304        );
305        let response = Response {
306            spec_name: "test_spec".to_string(),
307            effective: "2026-01-01".to_string(),
308            spec_effective_from: None,
309            spec_effective_to: None,
310            results,
311        };
312
313        let json = serde_json::to_string(&crate::api::Response::from(&response)).unwrap();
314        assert!(json.contains("test_spec"));
315        assert!(json.contains("test_rule"));
316        assert!(json.contains("\"number\":\"42\""));
317        assert!(!json.contains("lemma_type"));
318    }
319
320    #[test]
321    fn response_number_json_never_uses_fraction_notation() {
322        use crate::computation::rational::decimal_to_rational;
323
324        let rational = decimal_to_rational(Decimal::new(1, 1) / Decimal::new(3, 1)).unwrap();
325        let decimal_string = rational.try_to_decimal().unwrap().to_string();
326        let mut results = IndexMap::new();
327        results.insert(
328            "third".to_string(),
329            RuleResult::from_operation_result(
330                dummy_evaluated_rule("third", primitive_number_arc().as_ref()),
331                &OperationResult::from_literal(LiteralValue::number_from_decimal(
332                    rational.try_to_decimal().unwrap(),
333                )),
334                primitive_number_arc().as_ref(),
335                &empty_family_catalog(),
336                None,
337                Vec::new(),
338            ),
339        );
340        // Override committed decimal number field to match serialization path under test
341        if let Some(rule) = results.get_mut("third") {
342            let decimal = rational.try_to_decimal().unwrap();
343            rule.result = Some(crate::result_value::RuleResultValue {
344                result: Some(decimal_string.clone()),
345                number: Some(decimal),
346                ..Default::default()
347            });
348        }
349
350        let response = Response {
351            spec_name: "test".to_string(),
352            effective: "test".to_string(),
353            spec_effective_from: None,
354            spec_effective_to: None,
355            results,
356        };
357
358        let json: serde_json::Value = serde_json::from_str(
359            &serde_json::to_string(&crate::api::Response::from(&response)).unwrap(),
360        )
361        .unwrap();
362        let number = json["results"]["third"]["number"]
363            .as_str()
364            .expect("number must be a JSON string");
365        assert!(
366            !number.contains('/'),
367            "API decimal string must not use fraction notation, got {number}"
368        );
369    }
370
371    #[test]
372    fn test_rule_result_veto() {
373        let missing = RuleResult::from_operation_result(
374            dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
375            &OperationResult::Veto(VetoType::missing_data(
376                DataPath::new(vec![], "data1".to_string()),
377                None,
378            )),
379            &LemmaType::veto_type(),
380            &empty_family_catalog(),
381            None,
382            vec!["data1".to_string()],
383        );
384        assert!(missing.vetoed);
385        assert!(missing.veto_reason.as_ref().unwrap().contains("data1"));
386
387        let veto = RuleResult::from_operation_result(
388            dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
389            &OperationResult::Veto(VetoType::UserDefined {
390                message: Some("Vetoed".to_string()),
391            }),
392            &LemmaType::veto_type(),
393            &empty_family_catalog(),
394            None,
395            Vec::new(),
396        );
397        assert_eq!(veto.veto_reason.as_deref(), Some("Vetoed"));
398    }
399
400    /// Attach hole: MissingData + empty missing_data is a BUG, not a silent stall.
401    #[test]
402    #[should_panic(expected = "BUG: MissingData")]
403    fn missing_data_veto_must_not_attach_empty_missing_data_list() {
404        RuleResult::from_operation_result(
405            dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
406            &OperationResult::Veto(VetoType::missing_data(
407                DataPath::new(vec![], "data1".to_string()),
408                None,
409            )),
410            &LemmaType::veto_type(),
411            &empty_family_catalog(),
412            None,
413            Vec::new(),
414        );
415    }
416
417    #[test]
418    #[should_panic(expected = "BUG: MissingData")]
419    fn missing_data_veto_must_include_veto_path_in_list() {
420        RuleResult::from_operation_result(
421            dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
422            &OperationResult::Veto(VetoType::missing_data(
423                DataPath::new(vec![], "data1".to_string()),
424                None,
425            )),
426            &LemmaType::veto_type(),
427            &empty_family_catalog(),
428            None,
429            vec!["other".to_string()],
430        );
431    }
432
433    #[test]
434    #[should_panic(expected = "BUG: missing_data must be empty")]
435    fn non_missing_data_result_must_not_attach_leftover_missing_data() {
436        RuleResult::from_operation_result(
437            dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
438            &OperationResult::Veto(VetoType::UserDefined {
439                message: Some("Vetoed".to_string()),
440            }),
441            &LemmaType::veto_type(),
442            &empty_family_catalog(),
443            None,
444            vec!["leftover".to_string()],
445        );
446    }
447
448    #[test]
449    fn rule_result_value_out_of_memory_is_not_decimal_limit_veto() {
450        let result = vetoed_rule_result_for_rule_result_value_failure(
451            dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
452            primitive_number_arc().as_ref(),
453            &empty_family_catalog(),
454            None,
455            RuleResultValueFailure::OutOfMemory,
456            Vec::new(),
457        );
458        assert_eq!(result.veto_reason.as_deref(), Some("out of memory"));
459        assert_ne!(
460            result.veto_reason.as_deref(),
461            Some("Calculated result exceeds decimal value limit")
462        );
463    }
464
465    #[test]
466    fn rule_result_value_decimal_limit_uses_commit_message() {
467        let result = vetoed_rule_result_for_rule_result_value_failure(
468            dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
469            primitive_number_arc().as_ref(),
470            &empty_family_catalog(),
471            None,
472            RuleResultValueFailure::DecimalLimit,
473            Vec::new(),
474        );
475        assert_eq!(
476            result.veto_reason.as_deref(),
477            Some("Calculated result exceeds decimal value limit")
478        );
479    }
480
481    fn test_money_type() -> LemmaType {
482        LemmaType::new(
483            "money".to_string(),
484            TypeSpecification::Measure {
485                minimum: None,
486                maximum: None,
487                decimals: Some(2),
488                units: MeasureUnits::from(vec![
489                    MeasureUnit {
490                        name: "eur".to_string(),
491                        factor: crate::computation::rational::rational_one(),
492                        derived_measure_factors: Vec::new(),
493                        decomposition: BaseMeasureVector::new(),
494                        minimum: None,
495                        maximum: None,
496                        suggestion_magnitude: None,
497                    },
498                    MeasureUnit {
499                        name: "usd".to_string(),
500                        factor: crate::computation::rational::decimal_to_rational(Decimal::new(
501                            91, 2,
502                        ))
503                        .expect("factor"),
504                        derived_measure_factors: Vec::new(),
505                        decomposition: BaseMeasureVector::new(),
506                        minimum: None,
507                        maximum: None,
508                        suggestion_magnitude: None,
509                    },
510                ]),
511                traits: Vec::new(),
512                decomposition: Some(BaseMeasureVector::new()),
513                help: String::new(),
514            },
515            TypeExtends::Primitive,
516        )
517    }
518
519    #[test]
520    fn measure_rule_result_value_uses_rule_type_when_expression_index_empty() {
521        let money = test_money_type();
522        let family_units = family_catalog_for_money(&money);
523        let ten_usd = LiteralValue {
524            value: ValueKind::Measure(
525                crate::computation::rational::checked_mul(
526                    &crate::computation::rational::decimal_to_rational(Decimal::from(10))
527                        .expect("ten"),
528                    &crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
529                        .expect("usd factor"),
530                )
531                .expect("canonical usd"),
532            ),
533        };
534        let result = RuleResult::from_operation_result(
535            dummy_evaluated_rule("total", &money),
536            &OperationResult::from_literal(ten_usd),
537            &money,
538            &family_units,
539            None,
540            Vec::new(),
541        );
542        let measure = result
543            .result
544            .as_ref()
545            .expect("value")
546            .measure
547            .clone()
548            .expect("measure map");
549        assert_eq!(measure.get("usd"), Some(&dec("10.00")));
550        assert!(measure.contains_key("eur"));
551    }
552
553    #[test]
554    fn test_measure_rule_result_value_multi_unit() {
555        let money = test_money_type();
556        let family_units = family_catalog_for_money(&money);
557        let ten_eur = LiteralValue {
558            value: ValueKind::Measure(
559                crate::computation::rational::decimal_to_rational(Decimal::from(10)).expect("ten"),
560            ),
561        };
562        let result = RuleResult::from_operation_result(
563            dummy_evaluated_rule("total", &money),
564            &OperationResult::from_literal(ten_eur),
565            &money,
566            &family_units,
567            None,
568            Vec::new(),
569        );
570        let measure = result
571            .result
572            .as_ref()
573            .expect("value")
574            .measure
575            .clone()
576            .expect("measure map");
577        assert_eq!(measure.get("eur"), Some(&dec("10.00")));
578        assert_eq!(measure.get("usd"), Some(&dec("10.99")));
579    }
580
581    #[test]
582    fn measure_rule_result_value_respects_decimals_on_unit_conversion() {
583        let money = LemmaType::new(
584            "money".to_string(),
585            TypeSpecification::Measure {
586                minimum: None,
587                maximum: None,
588                decimals: Some(2),
589                units: MeasureUnits::from(vec![
590                    MeasureUnit {
591                        name: "eur".to_string(),
592                        factor: crate::computation::rational::rational_one(),
593                        derived_measure_factors: Vec::new(),
594                        decomposition: BaseMeasureVector::new(),
595                        minimum: None,
596                        maximum: None,
597                        suggestion_magnitude: None,
598                    },
599                    MeasureUnit {
600                        name: "usd".to_string(),
601                        factor: crate::computation::rational::decimal_to_rational(Decimal::new(
602                            84, 2,
603                        ))
604                        .expect("usd factor"),
605                        derived_measure_factors: Vec::new(),
606                        decomposition: BaseMeasureVector::new(),
607                        minimum: None,
608                        maximum: None,
609                        suggestion_magnitude: None,
610                    },
611                ]),
612                traits: Vec::new(),
613                decomposition: Some(BaseMeasureVector::new()),
614                help: String::new(),
615            },
616            TypeExtends::Primitive,
617        );
618        let family_units = family_catalog_for_money(&money);
619        let three_twelve_eur = LiteralValue {
620            value: ValueKind::Measure(
621                crate::computation::rational::decimal_to_rational(Decimal::new(312, 2))
622                    .expect("3.12 eur canonical"),
623            ),
624        };
625        let result = RuleResult::from_operation_result(
626            dummy_evaluated_rule("delivery_cost", &money),
627            &OperationResult::from_literal(three_twelve_eur),
628            &money,
629            &family_units,
630            None,
631            Vec::new(),
632        );
633        let measure = result
634            .result
635            .as_ref()
636            .expect("value")
637            .measure
638            .clone()
639            .expect("measure map");
640        assert_eq!(measure.get("eur"), Some(&dec("3.12")));
641        assert_eq!(measure.get("usd"), Some(&dec("3.71")));
642    }
643
644    #[test]
645    fn test_ratio_rule_result_value_multi_unit() {
646        let ratio_type = LemmaType::new(
647            "rate".to_string(),
648            TypeSpecification::Ratio {
649                minimum: None,
650                maximum: None,
651                decimals: None,
652                units: RatioUnits::from(vec![
653                    RatioUnit {
654                        name: "percent".to_string(),
655                        value: crate::computation::rational::decimal_to_rational(Decimal::from(
656                            100,
657                        ))
658                        .expect("percent"),
659                        minimum: None,
660                        maximum: None,
661                        suggestion_magnitude: None,
662                    },
663                    RatioUnit {
664                        name: "basis_points".to_string(),
665                        value: crate::computation::rational::decimal_to_rational(Decimal::from(
666                            10_000,
667                        ))
668                        .expect("bp"),
669                        minimum: None,
670                        maximum: None,
671                        suggestion_magnitude: None,
672                    },
673                ]),
674                help: String::new(),
675            },
676            TypeExtends::Primitive,
677        );
678        let family_units = family_catalog_for_ratio(&ratio_type);
679        let half = crate::computation::rational::rational_new(1, 2);
680        let lit = LiteralValue {
681            value: ValueKind::Ratio(half),
682        };
683        let result = RuleResult::from_operation_result(
684            dummy_evaluated_rule("rate_out", &ratio_type),
685            &OperationResult::from_literal(lit),
686            &ratio_type,
687            &family_units,
688            None,
689            Vec::new(),
690        );
691        let ratio = result
692            .result
693            .as_ref()
694            .expect("value")
695            .ratio
696            .clone()
697            .expect("ratio map");
698        assert_eq!(ratio.get("percent"), Some(&dec("50")));
699        assert_eq!(ratio.get("basis_points"), Some(&dec("5000")));
700    }
701
702    #[test]
703    fn test_measure_rule_result_value_cross_spec_import() {
704        use crate::parsing::source::SourceType;
705        use crate::Engine;
706
707        let mut engine = Engine::new();
708        engine
709            .load([(
710                SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("t.lemma"))),
711                r#"
712spec consumer 2025-01-01
713uses d: dep 2025-10-01
714rule out: d.doubled
715
716spec dep 2025-01-01
717uses c: child 2025-06-01
718data money: c.money
719data p: 5 usd
720rule doubled: p * 2
721
722spec child 2025-01-01
723data money: measure
724 -> unit eur: 1.00
725 -> decimals 2
726
727spec child 2025-06-01
728data money: measure
729 -> unit eur: 1.00
730 -> unit usd: 0.91
731 -> decimals 2
732"#
733                .to_string(),
734            )])
735            .expect("load");
736        let effective = crate::literals::DateTimeValue {
737            year: 2025,
738            month: 3,
739            day: 1,
740            hour: 0,
741            minute: 0,
742            second: 0,
743            microsecond: 0,
744            timezone: None,
745
746            granularity: DateGranularity::Full,
747        };
748        let response = engine
749            .run(
750                None,
751                "consumer",
752                Some(&effective),
753                std::collections::HashMap::new(),
754                None,
755                false,
756            )
757            .expect("run");
758        let out = response.results.get("out").expect("out rule");
759        assert!(!out.vetoed);
760        let measure = out
761            .result
762            .as_ref()
763            .expect("value")
764            .measure
765            .as_ref()
766            .expect("measure map");
767        assert!(measure.contains_key("usd"));
768        assert!(measure.contains_key("eur"));
769    }
770
771    #[test]
772    fn to_literal_roundtrips_number() {
773        let literal = LiteralValue::number_from_decimal(Decimal::from(42));
774        let rule_result = RuleResult::from_operation_result(
775            dummy_evaluated_rule("answer", primitive_number_arc().as_ref()),
776            &OperationResult::from_literal(literal.clone()),
777            primitive_number_arc().as_ref(),
778            &empty_family_catalog(),
779            None,
780            Vec::new(),
781        );
782        assert_eq!(rule_result.to_literal(), literal);
783    }
784
785    #[test]
786    fn to_literal_roundtrips_measure() {
787        let money = test_money_type();
788        let family_units = family_catalog_for_money(&money);
789        let literal = LiteralValue::measure_with_type(
790            crate::computation::rational::rational_new(60, 1),
791            Arc::new(money.clone()),
792        );
793        let rule_result = RuleResult::from_operation_result(
794            dummy_evaluated_rule("pay", &money),
795            &OperationResult::from_literal(literal.clone()),
796            &money,
797            &family_units,
798            None,
799            Vec::new(),
800        );
801        assert_eq!(rule_result.to_literal(), literal);
802    }
803}