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#[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#[derive(Debug, Clone)]
26pub struct Response {
27 pub spec_name: String,
28 pub effective: String,
29 pub spec_effective_from: Option<DateTimeValue>,
33 pub spec_effective_to: Option<DateTimeValue>,
34 pub results: IndexMap<String, RuleResult>,
35}
36
37#[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 pub value: Option<RuleResultValue>,
49 pub explanation: Option<Explanation>,
50 missing_data: Vec<String>,
55}
56
57impl RuleResult {
58 #[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 #[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 #[must_use]
81 pub fn missing_data(&self) -> &[String] {
82 &self.missing_data
83 }
84
85 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 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 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 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 #[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}