1use crate::computation::{OperationResult, VetoType};
2use crate::evaluation::explanations::Explanation;
3
4use crate::parsing::ast::DateTimeValue;
5use crate::planning::execution_plan::ExecutionPlan;
6use crate::planning::semantics::{
7 DataPath, LemmaType, LiteralValue, PathSegment, RulePath, Source, SpecInstance, ValueKind,
8};
9use crate::planning::unit_family::FamilyUnitCatalog;
10use crate::result_value::{
11 rule_result_value_failure_message, rule_result_value_from_literal, RuleResultValue,
12 RuleResultValueFailure, SpecResult, SpecRuleResult,
13};
14use indexmap::IndexMap;
15use std::collections::BTreeMap;
16use std::sync::Arc;
17
18#[derive(Debug, Clone)]
21pub struct EvaluatedRule {
22 pub name: String,
23 pub path: RulePath,
24 pub source_location: Source,
25 pub rule_type: Arc<LemmaType>,
26}
27
28#[derive(Debug, Clone)]
30pub struct Response {
31 pub spec_name: String,
32 pub effective: String,
33 pub spec_effective_from: Option<DateTimeValue>,
37 pub spec_effective_to: Option<DateTimeValue>,
38 pub results: IndexMap<String, RuleResult>,
39}
40
41#[derive(Debug, Clone)]
43pub struct RuleResult {
44 pub rule: EvaluatedRule,
45 pub veto_detail: Option<VetoType>,
46
47 pub vetoed: bool,
48 pub veto_reason: Option<String>,
49 pub rule_type: String,
50
51 pub result: Option<RuleResultValue>,
53 pub explanation: Option<Explanation>,
54 missing_data: Vec<String>,
59}
60
61impl RuleResult {
62 #[must_use]
64 pub fn result(&self) -> Option<&str> {
65 self.result
66 .as_ref()
67 .and_then(|value| value.result.as_deref())
68 }
69
70 #[must_use]
75 pub fn is_missing_data(&self) -> bool {
76 matches!(
77 self.veto_detail.as_ref(),
78 Some(VetoType::MissingData { .. })
79 )
80 }
81
82 #[must_use]
85 pub fn missing_data(&self) -> &[String] {
86 &self.missing_data
87 }
88
89 pub(crate) fn from_operation_result(
93 rule: EvaluatedRule,
94 operation_result: &OperationResult,
95 rule_type: &LemmaType,
96 family_units: &FamilyUnitCatalog,
97 explanation: Option<Explanation>,
98 missing_data: Vec<String>,
99 ) -> Self {
100 match operation_result {
101 OperationResult::Veto(VetoType::MissingData { data, .. }) => {
102 let key = data.input_key();
103 if !missing_data.iter().any(|listed| listed == &key) {
104 panic!("BUG: MissingData path {key} not in missing_data {missing_data:?}");
105 }
106 }
107 _ => {
108 if !missing_data.is_empty() {
109 panic!(
110 "BUG: missing_data must be empty when result is not MissingData: {missing_data:?}"
111 );
112 }
113 }
114 }
115 let rule_type_name = rule_type.name().to_string();
116 match operation_result {
117 OperationResult::Veto(veto) => Self {
118 rule,
119 veto_detail: Some(veto.clone()),
120 vetoed: true,
121 veto_reason: match &veto {
122 VetoType::UserDefined { message: None } => None,
123 _ => Some(veto.to_string()),
124 },
125 rule_type: rule_type_name,
126 result: None,
127 explanation,
128 missing_data,
129 },
130 OperationResult::Value(bound) => {
131 match rule_result_value_from_literal(&bound.to_literal(), rule_type, family_units) {
132 Ok(value) => Self {
133 rule,
134 veto_detail: None,
135 vetoed: false,
136 veto_reason: None,
137 rule_type: rule_type_name,
138 result: Some(value),
139 explanation,
140 missing_data,
141 },
142 Err(failure) => vetoed_rule_result_for_rule_result_value_failure(
143 rule,
144 rule_type,
145 family_units,
146 explanation,
147 failure,
148 missing_data,
149 ),
150 }
151 }
152 }
153 }
154
155 pub fn to_literal(&self) -> LiteralValue {
159 assert!(
160 !self.vetoed,
161 "BUG: to_literal called on vetoed rule '{}'",
162 self.rule.name
163 );
164 let value = self
165 .result
166 .as_ref()
167 .unwrap_or_else(|| panic!("BUG: non-vetoed rule '{}' missing result", self.rule.name));
168 value.to_literal(&self.rule.rule_type)
169 }
170}
171
172fn vetoed_rule_result_for_rule_result_value_failure(
173 rule: EvaluatedRule,
174 rule_type: &LemmaType,
175 family_units: &FamilyUnitCatalog,
176 explanation: Option<Explanation>,
177 failure: RuleResultValueFailure,
178 missing_data: Vec<String>,
179) -> RuleResult {
180 RuleResult::from_operation_result(
181 rule,
182 &OperationResult::Veto(VetoType::computation(
183 rule_result_value_failure_message(failure).to_string(),
184 )),
185 rule_type,
186 family_units,
187 explanation,
188 missing_data,
189 )
190}
191
192impl Response {
193 pub fn get(&self, rule_name: &str) -> Result<&RuleResult, crate::error::Error> {
197 self.results
198 .get(rule_name)
199 .ok_or_else(|| crate::error::Error::rule_not_found(rule_name, None::<String>))
200 }
201
202 pub fn add_result(&mut self, result: RuleResult) {
203 self.results.insert(result.rule.name.clone(), result);
204 }
205}
206
207pub(crate) fn filled_spec_result(
209 result: &OperationResult,
210 plan: &ExecutionPlan,
211 ctx: &mut crate::evaluation::EvaluationContext,
212) -> Option<SpecResult> {
213 let OperationResult::Value(bound) = result else {
214 return None;
215 };
216 let ValueKind::Spec(instance) = &bound.value else {
217 return None;
218 };
219 let mut stack = Vec::new();
220 Some(spec_result_for_instance(instance, plan, ctx, &mut stack))
221}
222
223fn spec_result_for_instance(
224 instance: &SpecInstance,
225 plan: &ExecutionPlan,
226 ctx: &mut crate::evaluation::EvaluationContext,
227 stack: &mut Vec<SpecInstance>,
228) -> SpecResult {
229 if stack.iter().any(|seen| seen == instance) {
230 panic!("BUG: spec result walked instance '{instance}' twice");
231 }
232 stack.push(instance.clone());
233 let identity = instance_identity(plan, instance);
234 let data = instance_data(plan, ctx, instance);
235 let mut rules = BTreeMap::new();
236 let rule_paths: Vec<RulePath> = plan
237 .rules
238 .keys()
239 .filter(|path| path_under_prefix(&path.segments, &instance.prefix))
240 .cloned()
241 .collect();
242 for path in rule_paths {
243 let exec = plan
244 .rules
245 .get(&path)
246 .expect("BUG: rule path missing from plan");
247 crate::evaluation::tree::ensure_rule_values(plan.rule_index(&path), plan, ctx);
248 let operation = ctx.rule_value(plan, &path).clone();
249 let missing_data = match &operation {
250 OperationResult::Veto(VetoType::MissingData { .. }) => {
251 ctx.missing_data_for_rule(plan, exec.normal_form)
252 }
253 _ => Vec::new(),
254 };
255 let mut entry = spec_entry(
256 &operation,
257 path.input_key(),
258 path.clone(),
259 exec.source.clone(),
260 exec.rule_type.as_ref(),
261 &plan.family_units,
262 missing_data,
263 );
264 if let OperationResult::Value(bound) = &operation {
265 if let ValueKind::Spec(nested) = &bound.value {
266 let nested_result = spec_result_for_instance(nested, plan, ctx, stack);
267 if let Some(value) = entry.result.as_mut() {
268 value.spec = Some(Box::new(nested_result));
269 }
270 }
271 }
272 let relative = strip_prefix(&path.input_key(), &instance.prefix).unwrap_or_else(|| {
273 panic!(
274 "BUG: rule '{}' is under instance '{instance}' but its key does not start with that path",
275 path.input_key()
276 )
277 });
278 rules.insert(relative, entry);
279 }
280 stack.pop();
281 SpecResult {
282 repository: identity.0,
283 spec: identity.1,
284 effective: identity.2,
285 instance: instance.to_string(),
286 data,
287 rules,
288 }
289}
290
291fn instance_identity(
292 plan: &ExecutionPlan,
293 instance: &SpecInstance,
294) -> (Option<String>, String, String) {
295 let last = instance
296 .prefix
297 .last()
298 .unwrap_or_else(|| panic!("BUG: spec instance '{instance}' has no uses hop"));
299 let import_path = DataPath {
300 segments: instance.prefix[..instance.prefix.len() - 1].to_vec(),
301 data: last.uses.clone(),
302 };
303 let Some(crate::planning::semantics::DataDefinition::Import {
304 repository,
305 target_name,
306 effective,
307 ..
308 }) = plan.data.get(&import_path)
309 else {
310 panic!(
311 "BUG: spec instance '{instance}' is not an import at '{}'",
312 import_path.input_key()
313 );
314 };
315 (
316 repository.clone(),
317 target_name.clone(),
318 effective.to_string(),
319 )
320}
321
322fn spec_entry(
323 operation: &OperationResult,
324 name: String,
325 path: RulePath,
326 source: Source,
327 display_type: &LemmaType,
328 family_units: &FamilyUnitCatalog,
329 missing_data: Vec<String>,
330) -> SpecRuleResult {
331 let built = RuleResult::from_operation_result(
332 EvaluatedRule {
333 name,
334 path,
335 source_location: source,
336 rule_type: Arc::new(display_type.clone()),
337 },
338 operation,
339 display_type,
340 family_units,
341 None,
342 missing_data,
343 );
344 SpecRuleResult {
345 vetoed: built.vetoed,
346 veto_reason: built.veto_reason.clone(),
347 rule_type: built.rule_type.clone(),
348 result: built.result.clone(),
349 }
350}
351
352fn instance_data(
353 plan: &ExecutionPlan,
354 ctx: &crate::evaluation::EvaluationContext,
355 instance: &SpecInstance,
356) -> BTreeMap<String, SpecRuleResult> {
357 let mut data = BTreeMap::new();
358 for (path, definition) in &plan.data {
359 if !path_under_prefix(&path.segments, &instance.prefix) {
360 continue;
361 }
362 if definition.schema_type().is_none() {
363 continue;
364 }
365 let relative = strip_prefix(&path.input_key(), &instance.prefix)
366 .unwrap_or_else(|| panic!("BUG: data '{}' lost its instance prefix", path.input_key()));
367 let Some(slot) = ctx.data_slot(plan, path) else {
368 continue;
369 };
370 if matches!(slot, OperationResult::Veto(VetoType::MissingData { .. })) {
371 continue;
372 }
373 let display_type = ctx.data_display_type(plan, path);
374 let entry = spec_entry(
375 slot,
376 path.input_key(),
377 RulePath {
378 segments: path.segments.clone(),
379 rule: path.data.clone(),
380 },
381 definition.source().clone(),
382 display_type.as_ref(),
383 &plan.family_units,
384 Vec::new(),
385 );
386 data.insert(relative, entry);
387 }
388 data
389}
390
391fn path_under_prefix(segments: &[PathSegment], prefix: &[PathSegment]) -> bool {
392 segments.len() >= prefix.len() && segments[..prefix.len()] == prefix[..]
393}
394
395fn strip_prefix(key: &str, prefix: &[PathSegment]) -> Option<String> {
396 let mut head = String::new();
397 for segment in prefix {
398 head.push_str(&segment.uses);
399 head.push('.');
400 }
401 key.strip_prefix(&head).map(str::to_string)
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::literals::DateGranularity;
408 use crate::parsing::ast::Span;
409 use crate::planning::semantics::{
410 primitive_number_arc, BaseMeasureVector, DataPath, LemmaType, LiteralValue, MeasureUnit,
411 MeasureUnits, RatioUnit, RatioUnits, RulePath, TypeExtends, TypeSpecification, ValueKind,
412 };
413 use crate::planning::unit_family::FamilyUnitCatalog;
414 use crate::planning::unit_index::UnitIndex;
415 use rust_decimal::Decimal;
416 use std::str::FromStr;
417 use std::sync::Arc;
418
419 fn dec(s: &str) -> Decimal {
420 Decimal::from_str(s).expect("BUG: test decimal literal")
421 }
422
423 fn empty_family_catalog() -> FamilyUnitCatalog {
424 FamilyUnitCatalog::default()
425 }
426
427 fn family_catalog_for_index(index: &UnitIndex) -> FamilyUnitCatalog {
428 FamilyUnitCatalog::build(index)
429 }
430
431 fn family_catalog_for_money(money: &LemmaType) -> FamilyUnitCatalog {
432 family_catalog_for_index(&unit_index_for_money(money))
433 }
434
435 fn family_catalog_for_ratio(ratio_type: &LemmaType) -> FamilyUnitCatalog {
436 family_catalog_for_index(&unit_index_for_ratio(ratio_type))
437 }
438
439 fn unit_index_for_money(money: &LemmaType) -> UnitIndex {
440 let mut index = UnitIndex::new();
441 let arc = Arc::new(money.clone());
442 for unit_name in money.measure_unit_names().into_iter().flatten() {
443 index
444 .merge_measure_unit(unit_name.to_string(), &arc, "money", None, "money")
445 .expect("test money unit index");
446 }
447 index
448 }
449
450 fn unit_index_for_ratio(ratio_type: &LemmaType) -> UnitIndex {
451 use crate::planning::semantics::primitive_ratio_arc;
452 let mut index = UnitIndex::new();
453 let arc = Arc::new(ratio_type.clone());
454 let type_name = ratio_type.name();
455 for unit_name in ratio_type.ratio_unit_names().into_iter().flatten() {
456 index
457 .merge_ratio_unit(
458 unit_name.to_string(),
459 &arc,
460 &type_name,
461 None,
462 primitive_ratio_arc(),
463 )
464 .expect("test ratio unit index");
465 }
466 index
467 }
468
469 fn dummy_source() -> Source {
470 Source::new(
471 crate::parsing::source::SourceType::Volatile,
472 Span {
473 start: 0,
474 end: 0,
475 line: 1,
476 col: 1,
477 },
478 )
479 }
480
481 fn dummy_evaluated_rule(name: &str, rule_type: &LemmaType) -> EvaluatedRule {
482 EvaluatedRule {
483 name: name.to_string(),
484 path: RulePath::new(vec![], name.to_string()),
485 source_location: dummy_source(),
486 rule_type: Arc::new(rule_type.clone()),
487 }
488 }
489
490 #[test]
491 fn test_response_serialization() {
492 let mut results = IndexMap::new();
493 results.insert(
494 "test_rule".to_string(),
495 RuleResult::from_operation_result(
496 dummy_evaluated_rule("test_rule", primitive_number_arc().as_ref()),
497 &OperationResult::from_literal(LiteralValue::number_from_decimal(Decimal::from(
498 42,
499 ))),
500 primitive_number_arc().as_ref(),
501 &empty_family_catalog(),
502 None,
503 Vec::new(),
504 ),
505 );
506 let response = Response {
507 spec_name: "test_spec".to_string(),
508 effective: "2026-01-01".to_string(),
509 spec_effective_from: None,
510 spec_effective_to: None,
511 results,
512 };
513
514 let json = serde_json::to_string(&crate::api::Response::from(&response)).unwrap();
515 assert!(json.contains("test_spec"));
516 assert!(json.contains("test_rule"));
517 assert!(json.contains("\"number\":\"42\""));
518 assert!(!json.contains("lemma_type"));
519 }
520
521 #[test]
522 fn response_number_json_never_uses_fraction_notation() {
523 use crate::computation::rational::decimal_to_rational;
524
525 let rational = decimal_to_rational(Decimal::new(1, 1) / Decimal::new(3, 1)).unwrap();
526 let decimal_string = rational.try_to_decimal().unwrap().to_string();
527 let mut results = IndexMap::new();
528 results.insert(
529 "third".to_string(),
530 RuleResult::from_operation_result(
531 dummy_evaluated_rule("third", primitive_number_arc().as_ref()),
532 &OperationResult::from_literal(LiteralValue::number_from_decimal(
533 rational.try_to_decimal().unwrap(),
534 )),
535 primitive_number_arc().as_ref(),
536 &empty_family_catalog(),
537 None,
538 Vec::new(),
539 ),
540 );
541 if let Some(rule) = results.get_mut("third") {
543 let decimal = rational.try_to_decimal().unwrap();
544 rule.result = Some(crate::result_value::RuleResultValue {
545 result: Some(decimal_string.clone()),
546 number: Some(decimal),
547 ..Default::default()
548 });
549 }
550
551 let response = Response {
552 spec_name: "test".to_string(),
553 effective: "test".to_string(),
554 spec_effective_from: None,
555 spec_effective_to: None,
556 results,
557 };
558
559 let json: serde_json::Value = serde_json::from_str(
560 &serde_json::to_string(&crate::api::Response::from(&response)).unwrap(),
561 )
562 .unwrap();
563 let number = json["results"]["third"]["number"]
564 .as_str()
565 .expect("number must be a JSON string");
566 assert!(
567 !number.contains('/'),
568 "API decimal string must not use fraction notation, got {number}"
569 );
570 }
571
572 #[test]
573 fn test_rule_result_veto() {
574 let missing = RuleResult::from_operation_result(
575 dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
576 &OperationResult::Veto(VetoType::missing_data(
577 DataPath::new(vec![], "data1".to_string()),
578 None,
579 )),
580 &LemmaType::veto_type(),
581 &empty_family_catalog(),
582 None,
583 vec!["data1".to_string()],
584 );
585 assert!(missing.vetoed);
586 assert!(missing.veto_reason.as_ref().unwrap().contains("data1"));
587
588 let veto = RuleResult::from_operation_result(
589 dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
590 &OperationResult::Veto(VetoType::UserDefined {
591 message: Some("Vetoed".to_string()),
592 }),
593 &LemmaType::veto_type(),
594 &empty_family_catalog(),
595 None,
596 Vec::new(),
597 );
598 assert_eq!(veto.veto_reason.as_deref(), Some("Vetoed"));
599 }
600
601 #[test]
603 #[should_panic(expected = "BUG: MissingData")]
604 fn missing_data_veto_must_not_attach_empty_missing_data_list() {
605 RuleResult::from_operation_result(
606 dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
607 &OperationResult::Veto(VetoType::missing_data(
608 DataPath::new(vec![], "data1".to_string()),
609 None,
610 )),
611 &LemmaType::veto_type(),
612 &empty_family_catalog(),
613 None,
614 Vec::new(),
615 );
616 }
617
618 #[test]
619 #[should_panic(expected = "BUG: MissingData")]
620 fn missing_data_veto_must_include_veto_path_in_list() {
621 RuleResult::from_operation_result(
622 dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
623 &OperationResult::Veto(VetoType::missing_data(
624 DataPath::new(vec![], "data1".to_string()),
625 None,
626 )),
627 &LemmaType::veto_type(),
628 &empty_family_catalog(),
629 None,
630 vec!["other".to_string()],
631 );
632 }
633
634 #[test]
635 #[should_panic(expected = "BUG: missing_data must be empty")]
636 fn non_missing_data_result_must_not_attach_leftover_missing_data() {
637 RuleResult::from_operation_result(
638 dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
639 &OperationResult::Veto(VetoType::UserDefined {
640 message: Some("Vetoed".to_string()),
641 }),
642 &LemmaType::veto_type(),
643 &empty_family_catalog(),
644 None,
645 vec!["leftover".to_string()],
646 );
647 }
648
649 #[test]
650 fn rule_result_value_out_of_memory_is_not_decimal_limit_veto() {
651 let result = vetoed_rule_result_for_rule_result_value_failure(
652 dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
653 primitive_number_arc().as_ref(),
654 &empty_family_catalog(),
655 None,
656 RuleResultValueFailure::OutOfMemory,
657 Vec::new(),
658 );
659 assert_eq!(result.veto_reason.as_deref(), Some("out of memory"));
660 assert_ne!(
661 result.veto_reason.as_deref(),
662 Some("Calculated result exceeds decimal value limit")
663 );
664 }
665
666 #[test]
667 fn rule_result_value_decimal_limit_uses_commit_message() {
668 let result = vetoed_rule_result_for_rule_result_value_failure(
669 dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
670 primitive_number_arc().as_ref(),
671 &empty_family_catalog(),
672 None,
673 RuleResultValueFailure::DecimalLimit,
674 Vec::new(),
675 );
676 assert_eq!(
677 result.veto_reason.as_deref(),
678 Some("Calculated result exceeds decimal value limit")
679 );
680 }
681
682 fn test_money_type() -> LemmaType {
683 LemmaType::new(
684 "money".to_string(),
685 TypeSpecification::Measure {
686 minimum: None,
687 maximum: None,
688 decimals: Some(2),
689 units: MeasureUnits::from(vec![
690 MeasureUnit {
691 name: "eur".to_string(),
692 factor: crate::computation::rational::rational_one(),
693 derived_measure_factors: Vec::new(),
694 decomposition: BaseMeasureVector::new(),
695 minimum: None,
696 maximum: None,
697 suggestion_magnitude: None,
698 },
699 MeasureUnit {
700 name: "usd".to_string(),
701 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
702 91, 2,
703 ))
704 .expect("factor"),
705 derived_measure_factors: Vec::new(),
706 decomposition: BaseMeasureVector::new(),
707 minimum: None,
708 maximum: None,
709 suggestion_magnitude: None,
710 },
711 ]),
712 traits: Vec::new(),
713 decomposition: Some(BaseMeasureVector::new()),
714 unit: None,
715 help: String::new(),
716 },
717 TypeExtends::Primitive,
718 )
719 }
720
721 #[test]
722 fn measure_rule_result_value_uses_rule_type_when_expression_index_empty() {
723 let money = test_money_type();
724 let family_units = family_catalog_for_money(&money);
725 let ten_usd = LiteralValue {
726 value: ValueKind::Measure(
727 crate::computation::rational::checked_mul(
728 &crate::computation::rational::decimal_to_rational(Decimal::from(10))
729 .expect("ten"),
730 &crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
731 .expect("usd factor"),
732 )
733 .expect("canonical usd"),
734 ),
735 };
736 let result = RuleResult::from_operation_result(
737 dummy_evaluated_rule("total", &money),
738 &OperationResult::from_literal(ten_usd),
739 &money,
740 &family_units,
741 None,
742 Vec::new(),
743 );
744 let measure = result
745 .result
746 .as_ref()
747 .expect("value")
748 .measure
749 .clone()
750 .expect("measure map");
751 assert_eq!(measure.get("usd"), Some(&dec("10.00")));
752 assert!(measure.contains_key("eur"));
753 }
754
755 #[test]
756 fn test_measure_rule_result_value_multi_unit() {
757 let money = test_money_type();
758 let family_units = family_catalog_for_money(&money);
759 let ten_eur = LiteralValue {
760 value: ValueKind::Measure(
761 crate::computation::rational::decimal_to_rational(Decimal::from(10)).expect("ten"),
762 ),
763 };
764 let result = RuleResult::from_operation_result(
765 dummy_evaluated_rule("total", &money),
766 &OperationResult::from_literal(ten_eur),
767 &money,
768 &family_units,
769 None,
770 Vec::new(),
771 );
772 let measure = result
773 .result
774 .as_ref()
775 .expect("value")
776 .measure
777 .clone()
778 .expect("measure map");
779 assert_eq!(measure.get("eur"), Some(&dec("10.00")));
780 assert_eq!(measure.get("usd"), Some(&dec("10.99")));
781 }
782
783 #[test]
784 fn measure_rule_result_value_respects_decimals_on_unit_conversion() {
785 let money = LemmaType::new(
786 "money".to_string(),
787 TypeSpecification::Measure {
788 minimum: None,
789 maximum: None,
790 decimals: Some(2),
791 units: MeasureUnits::from(vec![
792 MeasureUnit {
793 name: "eur".to_string(),
794 factor: crate::computation::rational::rational_one(),
795 derived_measure_factors: Vec::new(),
796 decomposition: BaseMeasureVector::new(),
797 minimum: None,
798 maximum: None,
799 suggestion_magnitude: None,
800 },
801 MeasureUnit {
802 name: "usd".to_string(),
803 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
804 84, 2,
805 ))
806 .expect("usd factor"),
807 derived_measure_factors: Vec::new(),
808 decomposition: BaseMeasureVector::new(),
809 minimum: None,
810 maximum: None,
811 suggestion_magnitude: None,
812 },
813 ]),
814 traits: Vec::new(),
815 decomposition: Some(BaseMeasureVector::new()),
816 unit: None,
817 help: String::new(),
818 },
819 TypeExtends::Primitive,
820 );
821 let family_units = family_catalog_for_money(&money);
822 let three_twelve_eur = LiteralValue {
823 value: ValueKind::Measure(
824 crate::computation::rational::decimal_to_rational(Decimal::new(312, 2))
825 .expect("3.12 eur canonical"),
826 ),
827 };
828 let result = RuleResult::from_operation_result(
829 dummy_evaluated_rule("delivery_cost", &money),
830 &OperationResult::from_literal(three_twelve_eur),
831 &money,
832 &family_units,
833 None,
834 Vec::new(),
835 );
836 let measure = result
837 .result
838 .as_ref()
839 .expect("value")
840 .measure
841 .clone()
842 .expect("measure map");
843 assert_eq!(measure.get("eur"), Some(&dec("3.12")));
844 assert_eq!(measure.get("usd"), Some(&dec("3.71")));
845 }
846
847 #[test]
848 fn test_ratio_rule_result_value_multi_unit() {
849 let ratio_type = LemmaType::new(
850 "rate".to_string(),
851 TypeSpecification::Ratio {
852 minimum: None,
853 maximum: None,
854 decimals: None,
855 units: RatioUnits::from(vec![
856 RatioUnit {
857 name: "percent".to_string(),
858 value: crate::computation::rational::decimal_to_rational(Decimal::from(
859 100,
860 ))
861 .expect("percent"),
862 minimum: None,
863 maximum: None,
864 suggestion_magnitude: None,
865 },
866 RatioUnit {
867 name: "basis_points".to_string(),
868 value: crate::computation::rational::decimal_to_rational(Decimal::from(
869 10_000,
870 ))
871 .expect("bp"),
872 minimum: None,
873 maximum: None,
874 suggestion_magnitude: None,
875 },
876 ]),
877 unit: None,
878 help: String::new(),
879 },
880 TypeExtends::Primitive,
881 );
882 let family_units = family_catalog_for_ratio(&ratio_type);
883 let half = crate::computation::rational::rational_new(1, 2);
884 let lit = LiteralValue {
885 value: ValueKind::Ratio(half),
886 };
887 let result = RuleResult::from_operation_result(
888 dummy_evaluated_rule("rate_out", &ratio_type),
889 &OperationResult::from_literal(lit),
890 &ratio_type,
891 &family_units,
892 None,
893 Vec::new(),
894 );
895 let ratio = result
896 .result
897 .as_ref()
898 .expect("value")
899 .ratio
900 .clone()
901 .expect("ratio map");
902 assert_eq!(ratio.get("percent"), Some(&dec("50")));
903 assert_eq!(ratio.get("basis_points"), Some(&dec("5000")));
904 }
905
906 #[test]
907 fn test_measure_rule_result_value_cross_spec_import() {
908 use crate::parsing::source::SourceType;
909 use crate::Engine;
910
911 let mut engine = Engine::new();
912 engine
913 .load([(
914 SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("t.lemma"))),
915 r#"
916spec consumer 2025-01-01
917uses d: dep 2025-10-01
918rule out: d.doubled
919
920spec dep 2025-01-01
921uses c: child 2025-06-01
922data money: c.money
923data p: 5 usd
924rule doubled: p * 2
925
926spec child 2025-01-01
927data money: measure
928 -> unit eur: 1.00
929 -> decimals 2
930
931spec child 2025-06-01
932data money: measure
933 -> unit eur: 1.00
934 -> unit usd: 0.91
935 -> decimals 2
936"#
937 .to_string(),
938 )])
939 .expect("load");
940 let effective = crate::literals::DateTimeValue {
941 year: 2025,
942 month: 3,
943 day: 1,
944 hour: 0,
945 minute: 0,
946 second: 0,
947 microsecond: 0,
948 timezone: None,
949
950 granularity: DateGranularity::Full,
951 };
952 let response = engine
953 .run(
954 None,
955 "consumer",
956 Some(&effective),
957 std::collections::HashMap::new(),
958 None,
959 false,
960 )
961 .expect("run");
962 let out = response.results.get("out").expect("out rule");
963 assert!(!out.vetoed);
964 let measure = out
965 .result
966 .as_ref()
967 .expect("value")
968 .measure
969 .as_ref()
970 .expect("measure map");
971 assert!(measure.contains_key("usd"));
972 assert!(measure.contains_key("eur"));
973 }
974
975 #[test]
976 fn to_literal_roundtrips_number() {
977 let literal = LiteralValue::number_from_decimal(Decimal::from(42));
978 let rule_result = RuleResult::from_operation_result(
979 dummy_evaluated_rule("answer", primitive_number_arc().as_ref()),
980 &OperationResult::from_literal(literal.clone()),
981 primitive_number_arc().as_ref(),
982 &empty_family_catalog(),
983 None,
984 Vec::new(),
985 );
986 assert_eq!(rule_result.to_literal(), literal);
987 }
988
989 #[test]
990 fn to_literal_roundtrips_measure() {
991 let money = test_money_type();
992 let family_units = family_catalog_for_money(&money);
993 let literal = LiteralValue::measure_with_type(
994 crate::computation::rational::rational_new(60, 1),
995 Arc::new(money.clone()),
996 );
997 let rule_result = RuleResult::from_operation_result(
998 dummy_evaluated_rule("pay", &money),
999 &OperationResult::from_literal(literal.clone()),
1000 &money,
1001 &family_units,
1002 None,
1003 Vec::new(),
1004 );
1005 assert_eq!(rule_result.to_literal(), literal);
1006 }
1007}