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 `display` when the rule is not vetoed.
48    pub value: 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 display string from the flattened [`RuleResultValue`].
59    #[must_use]
60    pub fn display(&self) -> Option<&str> {
61        self.value
62            .as_ref()
63            .and_then(|value| value.display.as_deref())
64    }
65
66    /// True when this rule still waits on unbound inputs (`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 awaits_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                value: None,
123                explanation,
124                missing_data,
125            },
126            OperationResult::Value(literal) => {
127                match rule_result_value_from_literal(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                        value: 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            .value
162            .as_ref()
163            .unwrap_or_else(|| panic!("BUG: non-vetoed rule '{}' missing value", 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::sync::Arc;
216
217    fn empty_family_catalog() -> FamilyUnitCatalog {
218        FamilyUnitCatalog::default()
219    }
220
221    fn family_catalog_for_index(index: &UnitIndex) -> FamilyUnitCatalog {
222        FamilyUnitCatalog::build(index)
223    }
224
225    fn family_catalog_for_money(money: &LemmaType) -> FamilyUnitCatalog {
226        family_catalog_for_index(&unit_index_for_money(money))
227    }
228
229    fn family_catalog_for_ratio(ratio_type: &LemmaType) -> FamilyUnitCatalog {
230        family_catalog_for_index(&unit_index_for_ratio(ratio_type))
231    }
232
233    fn unit_index_for_money(money: &LemmaType) -> UnitIndex {
234        let mut index = UnitIndex::new();
235        let arc = Arc::new(money.clone());
236        for unit_name in money.measure_unit_names().into_iter().flatten() {
237            index
238                .merge_measure_unit(unit_name.to_string(), &arc, "money", None, "money")
239                .expect("test money unit index");
240        }
241        index
242    }
243
244    fn unit_index_for_ratio(ratio_type: &LemmaType) -> UnitIndex {
245        use crate::planning::semantics::primitive_ratio_arc;
246        let mut index = UnitIndex::new();
247        let arc = Arc::new(ratio_type.clone());
248        let type_name = ratio_type.name();
249        for unit_name in ratio_type.ratio_unit_names().into_iter().flatten() {
250            index
251                .merge_ratio_unit(
252                    unit_name.to_string(),
253                    &arc,
254                    &type_name,
255                    None,
256                    primitive_ratio_arc(),
257                )
258                .expect("test ratio unit index");
259        }
260        index
261    }
262
263    fn dummy_source() -> Source {
264        Source::new(
265            crate::parsing::source::SourceType::Volatile,
266            Span {
267                start: 0,
268                end: 0,
269                line: 1,
270                col: 1,
271            },
272        )
273    }
274
275    fn dummy_evaluated_rule(name: &str, rule_type: &LemmaType) -> EvaluatedRule {
276        EvaluatedRule {
277            name: name.to_string(),
278            path: RulePath::new(vec![], name.to_string()),
279            source_location: dummy_source(),
280            rule_type: Arc::new(rule_type.clone()),
281        }
282    }
283
284    #[test]
285    fn test_response_serialization() {
286        let mut results = IndexMap::new();
287        results.insert(
288            "test_rule".to_string(),
289            RuleResult::from_operation_result(
290                dummy_evaluated_rule("test_rule", primitive_number_arc().as_ref()),
291                &OperationResult::from_literal(LiteralValue::number_from_decimal(Decimal::from(
292                    42,
293                ))),
294                primitive_number_arc().as_ref(),
295                &empty_family_catalog(),
296                None,
297                Vec::new(),
298            ),
299        );
300        let response = Response {
301            spec_name: "test_spec".to_string(),
302            effective: "2026-01-01".to_string(),
303            spec_effective_from: None,
304            spec_effective_to: None,
305            results,
306        };
307
308        let json = serde_json::to_string(&crate::api::Response::from(&response)).unwrap();
309        assert!(json.contains("test_spec"));
310        assert!(json.contains("test_rule"));
311        assert!(json.contains("\"number\":\"42\""));
312        assert!(!json.contains("lemma_type"));
313    }
314
315    #[test]
316    fn response_number_json_never_uses_fraction_notation() {
317        use crate::computation::rational::decimal_to_rational;
318
319        let rational = decimal_to_rational(Decimal::new(1, 1) / Decimal::new(3, 1)).unwrap();
320        let decimal_string = rational.try_to_decimal().unwrap().to_string();
321        let mut results = IndexMap::new();
322        results.insert(
323            "third".to_string(),
324            RuleResult::from_operation_result(
325                dummy_evaluated_rule("third", primitive_number_arc().as_ref()),
326                &OperationResult::from_literal(LiteralValue::number_from_decimal(
327                    rational.try_to_decimal().unwrap(),
328                )),
329                primitive_number_arc().as_ref(),
330                &empty_family_catalog(),
331                None,
332                Vec::new(),
333            ),
334        );
335        // Override committed decimal number field to match serialization path under test
336        if let Some(rule) = results.get_mut("third") {
337            rule.value = Some(crate::result_value::RuleResultValue {
338                display: Some(decimal_string.clone()),
339                number: Some(decimal_string.clone()),
340                ..Default::default()
341            });
342        }
343
344        let response = Response {
345            spec_name: "test".to_string(),
346            effective: "test".to_string(),
347            spec_effective_from: None,
348            spec_effective_to: None,
349            results,
350        };
351
352        let json: serde_json::Value = serde_json::from_str(
353            &serde_json::to_string(&crate::api::Response::from(&response)).unwrap(),
354        )
355        .unwrap();
356        let number = json["results"]["third"]["number"]
357            .as_str()
358            .expect("number must be a JSON string");
359        assert!(
360            !number.contains('/'),
361            "API decimal string must not use fraction notation, got {number}"
362        );
363    }
364
365    #[test]
366    fn test_rule_result_veto() {
367        let missing = RuleResult::from_operation_result(
368            dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
369            &OperationResult::Veto(VetoType::missing_data(
370                DataPath::new(vec![], "data1".to_string()),
371                None,
372            )),
373            &LemmaType::veto_type(),
374            &empty_family_catalog(),
375            None,
376            vec!["data1".to_string()],
377        );
378        assert!(missing.vetoed);
379        assert!(missing.veto_reason.as_ref().unwrap().contains("data1"));
380
381        let veto = RuleResult::from_operation_result(
382            dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
383            &OperationResult::Veto(VetoType::UserDefined {
384                message: Some("Vetoed".to_string()),
385            }),
386            &LemmaType::veto_type(),
387            &empty_family_catalog(),
388            None,
389            Vec::new(),
390        );
391        assert_eq!(veto.veto_reason.as_deref(), Some("Vetoed"));
392    }
393
394    /// Attach hole: MissingData + empty missing_data is a BUG, not a silent stall.
395    #[test]
396    #[should_panic(expected = "BUG: MissingData")]
397    fn missing_data_veto_must_not_attach_empty_missing_data_list() {
398        RuleResult::from_operation_result(
399            dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
400            &OperationResult::Veto(VetoType::missing_data(
401                DataPath::new(vec![], "data1".to_string()),
402                None,
403            )),
404            &LemmaType::veto_type(),
405            &empty_family_catalog(),
406            None,
407            Vec::new(),
408        );
409    }
410
411    #[test]
412    #[should_panic(expected = "BUG: MissingData")]
413    fn missing_data_veto_must_include_veto_path_in_list() {
414        RuleResult::from_operation_result(
415            dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
416            &OperationResult::Veto(VetoType::missing_data(
417                DataPath::new(vec![], "data1".to_string()),
418                None,
419            )),
420            &LemmaType::veto_type(),
421            &empty_family_catalog(),
422            None,
423            vec!["other".to_string()],
424        );
425    }
426
427    #[test]
428    #[should_panic(expected = "BUG: missing_data must be empty")]
429    fn non_missing_data_result_must_not_attach_leftover_missing_data() {
430        RuleResult::from_operation_result(
431            dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
432            &OperationResult::Veto(VetoType::UserDefined {
433                message: Some("Vetoed".to_string()),
434            }),
435            &LemmaType::veto_type(),
436            &empty_family_catalog(),
437            None,
438            vec!["leftover".to_string()],
439        );
440    }
441
442    #[test]
443    fn rule_result_value_out_of_memory_is_not_decimal_limit_veto() {
444        let result = vetoed_rule_result_for_rule_result_value_failure(
445            dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
446            primitive_number_arc().as_ref(),
447            &empty_family_catalog(),
448            None,
449            RuleResultValueFailure::OutOfMemory,
450            Vec::new(),
451        );
452        assert_eq!(result.veto_reason.as_deref(), Some("out of memory"));
453        assert_ne!(
454            result.veto_reason.as_deref(),
455            Some("Calculated result exceeds decimal value limit")
456        );
457    }
458
459    #[test]
460    fn rule_result_value_decimal_limit_uses_commit_message() {
461        let result = vetoed_rule_result_for_rule_result_value_failure(
462            dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
463            primitive_number_arc().as_ref(),
464            &empty_family_catalog(),
465            None,
466            RuleResultValueFailure::DecimalLimit,
467            Vec::new(),
468        );
469        assert_eq!(
470            result.veto_reason.as_deref(),
471            Some("Calculated result exceeds decimal value limit")
472        );
473    }
474
475    fn test_money_type() -> LemmaType {
476        LemmaType::new(
477            "money".to_string(),
478            TypeSpecification::Measure {
479                minimum: None,
480                maximum: None,
481                decimals: Some(2),
482                units: MeasureUnits::from(vec![
483                    MeasureUnit {
484                        name: "eur".to_string(),
485                        factor: crate::computation::rational::rational_one(),
486                        derived_measure_factors: Vec::new(),
487                        decomposition: BaseMeasureVector::new(),
488                        minimum: None,
489                        maximum: None,
490                        suggestion_magnitude: None,
491                    },
492                    MeasureUnit {
493                        name: "usd".to_string(),
494                        factor: crate::computation::rational::decimal_to_rational(Decimal::new(
495                            91, 2,
496                        ))
497                        .expect("factor"),
498                        derived_measure_factors: Vec::new(),
499                        decomposition: BaseMeasureVector::new(),
500                        minimum: None,
501                        maximum: None,
502                        suggestion_magnitude: None,
503                    },
504                ]),
505                traits: Vec::new(),
506                decomposition: Some(BaseMeasureVector::new()),
507                help: String::new(),
508            },
509            TypeExtends::Primitive,
510        )
511    }
512
513    #[test]
514    fn measure_rule_result_value_uses_rule_type_when_expression_index_empty() {
515        let money = test_money_type();
516        let family_units = family_catalog_for_money(&money);
517        let ten_usd = LiteralValue {
518            value: ValueKind::Measure(
519                crate::computation::rational::checked_mul(
520                    &crate::computation::rational::decimal_to_rational(Decimal::from(10))
521                        .expect("ten"),
522                    &crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
523                        .expect("usd factor"),
524                )
525                .expect("canonical usd"),
526            ),
527        };
528        let result = RuleResult::from_operation_result(
529            dummy_evaluated_rule("total", &money),
530            &OperationResult::from_literal(ten_usd),
531            &money,
532            &family_units,
533            None,
534            Vec::new(),
535        );
536        let measure = result
537            .value
538            .as_ref()
539            .expect("value")
540            .measure
541            .clone()
542            .expect("measure map");
543        assert_eq!(measure.get("usd"), Some(&"10.00".to_string()));
544        assert!(measure.contains_key("eur"));
545    }
546
547    #[test]
548    fn test_measure_rule_result_value_multi_unit() {
549        let money = test_money_type();
550        let family_units = family_catalog_for_money(&money);
551        let ten_eur = LiteralValue {
552            value: ValueKind::Measure(
553                crate::computation::rational::decimal_to_rational(Decimal::from(10)).expect("ten"),
554            ),
555        };
556        let result = RuleResult::from_operation_result(
557            dummy_evaluated_rule("total", &money),
558            &OperationResult::from_literal(ten_eur),
559            &money,
560            &family_units,
561            None,
562            Vec::new(),
563        );
564        let measure = result
565            .value
566            .as_ref()
567            .expect("value")
568            .measure
569            .clone()
570            .expect("measure map");
571        assert_eq!(measure.get("eur"), Some(&"10.00".to_string()));
572        assert_eq!(measure.get("usd"), Some(&"10.99".to_string()));
573    }
574
575    #[test]
576    fn measure_rule_result_value_respects_decimals_on_unit_conversion() {
577        let money = LemmaType::new(
578            "money".to_string(),
579            TypeSpecification::Measure {
580                minimum: None,
581                maximum: None,
582                decimals: Some(2),
583                units: MeasureUnits::from(vec![
584                    MeasureUnit {
585                        name: "eur".to_string(),
586                        factor: crate::computation::rational::rational_one(),
587                        derived_measure_factors: Vec::new(),
588                        decomposition: BaseMeasureVector::new(),
589                        minimum: None,
590                        maximum: None,
591                        suggestion_magnitude: None,
592                    },
593                    MeasureUnit {
594                        name: "usd".to_string(),
595                        factor: crate::computation::rational::decimal_to_rational(Decimal::new(
596                            84, 2,
597                        ))
598                        .expect("usd factor"),
599                        derived_measure_factors: Vec::new(),
600                        decomposition: BaseMeasureVector::new(),
601                        minimum: None,
602                        maximum: None,
603                        suggestion_magnitude: None,
604                    },
605                ]),
606                traits: Vec::new(),
607                decomposition: Some(BaseMeasureVector::new()),
608                help: String::new(),
609            },
610            TypeExtends::Primitive,
611        );
612        let family_units = family_catalog_for_money(&money);
613        let three_twelve_eur = LiteralValue {
614            value: ValueKind::Measure(
615                crate::computation::rational::decimal_to_rational(Decimal::new(312, 2))
616                    .expect("3.12 eur canonical"),
617            ),
618        };
619        let result = RuleResult::from_operation_result(
620            dummy_evaluated_rule("delivery_cost", &money),
621            &OperationResult::from_literal(three_twelve_eur),
622            &money,
623            &family_units,
624            None,
625            Vec::new(),
626        );
627        let measure = result
628            .value
629            .as_ref()
630            .expect("value")
631            .measure
632            .clone()
633            .expect("measure map");
634        assert_eq!(measure.get("eur"), Some(&"3.12".to_string()));
635        assert_eq!(measure.get("usd"), Some(&"3.71".to_string()));
636    }
637
638    #[test]
639    fn test_ratio_rule_result_value_multi_unit() {
640        let ratio_type = LemmaType::new(
641            "rate".to_string(),
642            TypeSpecification::Ratio {
643                minimum: None,
644                maximum: None,
645                decimals: None,
646                units: RatioUnits::from(vec![
647                    RatioUnit {
648                        name: "percent".to_string(),
649                        value: crate::computation::rational::decimal_to_rational(Decimal::from(
650                            100,
651                        ))
652                        .expect("percent"),
653                        minimum: None,
654                        maximum: None,
655                        suggestion_magnitude: None,
656                    },
657                    RatioUnit {
658                        name: "basis_points".to_string(),
659                        value: crate::computation::rational::decimal_to_rational(Decimal::from(
660                            10_000,
661                        ))
662                        .expect("bp"),
663                        minimum: None,
664                        maximum: None,
665                        suggestion_magnitude: None,
666                    },
667                ]),
668                help: String::new(),
669            },
670            TypeExtends::Primitive,
671        );
672        let family_units = family_catalog_for_ratio(&ratio_type);
673        let half = crate::computation::rational::rational_new(1, 2);
674        let lit = LiteralValue {
675            value: ValueKind::Ratio(half),
676        };
677        let result = RuleResult::from_operation_result(
678            dummy_evaluated_rule("rate_out", &ratio_type),
679            &OperationResult::from_literal(lit),
680            &ratio_type,
681            &family_units,
682            None,
683            Vec::new(),
684        );
685        let ratio = result
686            .value
687            .as_ref()
688            .expect("value")
689            .ratio
690            .clone()
691            .expect("ratio map");
692        assert_eq!(ratio.get("percent"), Some(&"50".to_string()));
693        assert_eq!(ratio.get("basis_points"), Some(&"5000".to_string()));
694    }
695
696    #[test]
697    fn test_measure_rule_result_value_cross_spec_import() {
698        use crate::parsing::source::SourceType;
699        use crate::Engine;
700
701        let mut engine = Engine::new();
702        engine
703            .load([(
704                SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("t.lemma"))),
705                r#"
706spec consumer 2025-01-01
707uses d: dep 2025-10-01
708rule out: d.doubled
709
710spec dep 2025-01-01
711uses c: child 2025-06-01
712data money: c.money
713data p: 5 usd
714rule doubled: p * 2
715
716spec child 2025-01-01
717data money: measure
718 -> unit eur: 1.00
719 -> decimals 2
720
721spec child 2025-06-01
722data money: measure
723 -> unit eur: 1.00
724 -> unit usd: 0.91
725 -> decimals 2
726"#
727                .to_string(),
728            )])
729            .expect("load");
730        let effective = crate::literals::DateTimeValue {
731            year: 2025,
732            month: 3,
733            day: 1,
734            hour: 0,
735            minute: 0,
736            second: 0,
737            microsecond: 0,
738            timezone: None,
739
740            granularity: DateGranularity::Full,
741        };
742        let response = engine
743            .run(
744                None,
745                "consumer",
746                Some(&effective),
747                std::collections::HashMap::new(),
748                None,
749                false,
750            )
751            .expect("run");
752        let out = response.results.get("out").expect("out rule");
753        assert!(!out.vetoed);
754        let measure = out
755            .value
756            .as_ref()
757            .expect("value")
758            .measure
759            .as_ref()
760            .expect("measure map");
761        assert!(measure.contains_key("usd"));
762        assert!(measure.contains_key("eur"));
763    }
764
765    #[test]
766    fn to_literal_roundtrips_number() {
767        let literal = LiteralValue::number_from_decimal(Decimal::from(42));
768        let rule_result = RuleResult::from_operation_result(
769            dummy_evaluated_rule("answer", primitive_number_arc().as_ref()),
770            &OperationResult::from_literal(literal.clone()),
771            primitive_number_arc().as_ref(),
772            &empty_family_catalog(),
773            None,
774            Vec::new(),
775        );
776        assert_eq!(rule_result.to_literal(), literal);
777    }
778
779    #[test]
780    fn to_literal_roundtrips_measure() {
781        let money = test_money_type();
782        let family_units = family_catalog_for_money(&money);
783        let literal = LiteralValue::measure_with_type(
784            crate::computation::rational::rational_new(60, 1),
785            Arc::new(money.clone()),
786        );
787        let rule_result = RuleResult::from_operation_result(
788            dummy_evaluated_rule("pay", &money),
789            &OperationResult::from_literal(literal.clone()),
790            &money,
791            &family_units,
792            None,
793            Vec::new(),
794        );
795        assert_eq!(rule_result.to_literal(), literal);
796    }
797}