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::result_value::{
7 rule_result_value_failure_message, rule_result_value_from_literal, RuleResultValue,
8 RuleResultValueFailure,
9};
10use indexmap::IndexMap;
11use serde::Serialize;
12
13#[derive(Debug, Clone, Serialize)]
16pub struct EvaluatedRule {
17 pub name: String,
18 pub path: RulePath,
19 pub source_location: Source,
20 pub rule_type: LemmaType,
21}
22
23#[derive(Debug, Clone, Serialize)]
25pub struct Response {
26 #[serde(rename = "spec")]
27 pub spec_name: String,
28 pub effective: String,
29 #[serde(skip_serializing_if = "Option::is_none")]
33 pub spec_effective_from: Option<DateTimeValue>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub spec_effective_to: Option<DateTimeValue>,
36 pub results: IndexMap<String, RuleResult>,
37}
38
39#[derive(Debug, Clone, Serialize)]
41pub struct RuleResult {
42 #[serde(skip)]
43 pub rule: EvaluatedRule,
44 #[serde(skip)]
45 pub veto_detail: Option<VetoType>,
46
47 pub vetoed: bool,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub veto_reason: Option<String>,
50 pub rule_type: String,
51
52 #[serde(flatten)]
54 pub value: Option<RuleResultValue>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub explanation: Option<Explanation>,
57 #[serde(skip_serializing_if = "Vec::is_empty")]
60 missing_data: Vec<String>,
61}
62
63impl RuleResult {
64 #[must_use]
66 pub fn display(&self) -> Option<&str> {
67 self.value
68 .as_ref()
69 .and_then(|value| value.display.as_deref())
70 }
71
72 #[must_use]
77 pub fn awaits_missing_data(&self) -> bool {
78 matches!(
79 self.veto_detail.as_ref(),
80 Some(VetoType::MissingData { .. })
81 )
82 }
83
84 #[must_use]
86 pub fn missing_data(&self) -> &[String] {
87 &self.missing_data
88 }
89
90 pub fn from_operation_result(
94 rule: EvaluatedRule,
95 operation_result: &OperationResult,
96 rule_type: &LemmaType,
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 value: None,
127 explanation,
128 missing_data,
129 },
130 OperationResult::Value(literal) => {
131 match rule_result_value_from_literal(literal, rule_type) {
132 Ok(value) => Self {
133 rule,
134 veto_detail: None,
135 vetoed: false,
136 veto_reason: None,
137 rule_type: rule_type_name,
138 value: 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 explanation,
146 failure,
147 missing_data,
148 ),
149 }
150 }
151 }
152 }
153
154 pub fn to_literal(&self) -> LiteralValue {
158 assert!(
159 !self.vetoed,
160 "BUG: to_literal called on vetoed rule '{}'",
161 self.rule.name
162 );
163 let value = self
164 .value
165 .as_ref()
166 .unwrap_or_else(|| panic!("BUG: non-vetoed rule '{}' missing value", self.rule.name));
167 value.to_literal(&self.rule.rule_type)
168 }
169}
170
171fn vetoed_rule_result_for_rule_result_value_failure(
172 rule: EvaluatedRule,
173 rule_type: &LemmaType,
174 explanation: Option<Explanation>,
175 failure: RuleResultValueFailure,
176 missing_data: Vec<String>,
177) -> RuleResult {
178 RuleResult::from_operation_result(
179 rule,
180 &OperationResult::Veto(VetoType::computation(
181 rule_result_value_failure_message(failure).to_string(),
182 )),
183 rule_type,
184 explanation,
185 missing_data,
186 )
187}
188
189impl Response {
190 pub fn get(&self, rule_name: &str) -> Result<&RuleResult, crate::error::Error> {
194 self.results
195 .get(rule_name)
196 .ok_or_else(|| crate::error::Error::rule_not_found(rule_name, None::<String>))
197 }
198
199 pub fn add_result(&mut self, result: RuleResult) {
200 self.results.insert(result.rule.name.clone(), result);
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::literals::DateGranularity;
208 use crate::parsing::ast::Span;
209 use crate::planning::semantics::{
210 primitive_number_arc, BaseMeasureVector, DataPath, LemmaType, LiteralValue, MeasureUnit,
211 MeasureUnits, RatioUnit, RatioUnits, RulePath, TypeExtends, TypeSpecification, ValueKind,
212 };
213 use rust_decimal::Decimal;
214 use std::sync::Arc;
215
216 fn dummy_source() -> Source {
217 Source::new(
218 crate::parsing::source::SourceType::Volatile,
219 Span {
220 start: 0,
221 end: 0,
222 line: 1,
223 col: 1,
224 },
225 )
226 }
227
228 fn dummy_evaluated_rule(name: &str, rule_type: &LemmaType) -> EvaluatedRule {
229 EvaluatedRule {
230 name: name.to_string(),
231 path: RulePath::new(vec![], name.to_string()),
232 source_location: dummy_source(),
233 rule_type: rule_type.clone(),
234 }
235 }
236
237 #[test]
238 fn test_response_serialization() {
239 let mut results = IndexMap::new();
240 results.insert(
241 "test_rule".to_string(),
242 RuleResult::from_operation_result(
243 dummy_evaluated_rule("test_rule", primitive_number_arc().as_ref()),
244 &OperationResult::from_literal(LiteralValue::number_from_decimal(Decimal::from(
245 42,
246 ))),
247 primitive_number_arc().as_ref(),
248 None,
249 Vec::new(),
250 ),
251 );
252 let response = Response {
253 spec_name: "test_spec".to_string(),
254 effective: "2026-01-01".to_string(),
255 spec_effective_from: None,
256 spec_effective_to: None,
257 results,
258 };
259
260 let json = serde_json::to_string(&response).unwrap();
261 assert!(json.contains("test_spec"));
262 assert!(json.contains("test_rule"));
263 assert!(json.contains("\"number\":\"42\""));
264 assert!(!json.contains("lemma_type"));
265 }
266
267 #[test]
268 fn response_number_json_never_uses_fraction_notation() {
269 use crate::computation::rational::decimal_to_rational;
270
271 let rational = decimal_to_rational(Decimal::new(1, 1) / Decimal::new(3, 1)).unwrap();
272 let decimal_string = rational.try_to_decimal().unwrap().to_string();
273 let mut results = IndexMap::new();
274 results.insert(
275 "third".to_string(),
276 RuleResult::from_operation_result(
277 dummy_evaluated_rule("third", primitive_number_arc().as_ref()),
278 &OperationResult::from_literal(LiteralValue::number_from_decimal(
279 rational.try_to_decimal().unwrap(),
280 )),
281 primitive_number_arc().as_ref(),
282 None,
283 Vec::new(),
284 ),
285 );
286 if let Some(rule) = results.get_mut("third") {
288 rule.value = Some(crate::result_value::RuleResultValue {
289 display: Some(decimal_string.clone()),
290 number: Some(decimal_string.clone()),
291 ..Default::default()
292 });
293 }
294
295 let response = Response {
296 spec_name: "test".to_string(),
297 effective: "test".to_string(),
298 spec_effective_from: None,
299 spec_effective_to: None,
300 results,
301 };
302
303 let json: serde_json::Value =
304 serde_json::from_str(&serde_json::to_string(&response).unwrap()).unwrap();
305 let number = json["results"]["third"]["number"]
306 .as_str()
307 .expect("number must be a JSON string");
308 assert!(
309 !number.contains('/'),
310 "API decimal string must not use fraction notation, got {number}"
311 );
312 }
313
314 #[test]
315 fn test_rule_result_veto() {
316 let missing = RuleResult::from_operation_result(
317 dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
318 &OperationResult::Veto(VetoType::missing_data(
319 DataPath::new(vec![], "data1".to_string()),
320 None,
321 )),
322 &LemmaType::veto_type(),
323 None,
324 vec!["data1".to_string()],
325 );
326 assert!(missing.vetoed);
327 assert!(missing.veto_reason.as_ref().unwrap().contains("data1"));
328
329 let veto = RuleResult::from_operation_result(
330 dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
331 &OperationResult::Veto(VetoType::UserDefined {
332 message: Some("Vetoed".to_string()),
333 }),
334 &LemmaType::veto_type(),
335 None,
336 Vec::new(),
337 );
338 assert_eq!(veto.veto_reason.as_deref(), Some("Vetoed"));
339 }
340
341 #[test]
343 #[should_panic(expected = "BUG: MissingData")]
344 fn missing_data_veto_must_not_attach_empty_missing_data_list() {
345 RuleResult::from_operation_result(
346 dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
347 &OperationResult::Veto(VetoType::missing_data(
348 DataPath::new(vec![], "data1".to_string()),
349 None,
350 )),
351 &LemmaType::veto_type(),
352 None,
353 Vec::new(),
354 );
355 }
356
357 #[test]
358 #[should_panic(expected = "BUG: MissingData")]
359 fn missing_data_veto_must_include_veto_path_in_list() {
360 RuleResult::from_operation_result(
361 dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
362 &OperationResult::Veto(VetoType::missing_data(
363 DataPath::new(vec![], "data1".to_string()),
364 None,
365 )),
366 &LemmaType::veto_type(),
367 None,
368 vec!["other".to_string()],
369 );
370 }
371
372 #[test]
373 #[should_panic(expected = "BUG: missing_data must be empty")]
374 fn non_missing_data_result_must_not_attach_leftover_missing_data() {
375 RuleResult::from_operation_result(
376 dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
377 &OperationResult::Veto(VetoType::UserDefined {
378 message: Some("Vetoed".to_string()),
379 }),
380 &LemmaType::veto_type(),
381 None,
382 vec!["leftover".to_string()],
383 );
384 }
385
386 #[test]
387 fn rule_result_value_out_of_memory_is_not_decimal_limit_veto() {
388 let result = vetoed_rule_result_for_rule_result_value_failure(
389 dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
390 primitive_number_arc().as_ref(),
391 None,
392 RuleResultValueFailure::OutOfMemory,
393 Vec::new(),
394 );
395 assert_eq!(result.veto_reason.as_deref(), Some("out of memory"));
396 assert_ne!(
397 result.veto_reason.as_deref(),
398 Some("Calculated result exceeds decimal value limit")
399 );
400 }
401
402 #[test]
403 fn rule_result_value_decimal_limit_uses_commit_message() {
404 let result = vetoed_rule_result_for_rule_result_value_failure(
405 dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
406 primitive_number_arc().as_ref(),
407 None,
408 RuleResultValueFailure::DecimalLimit,
409 Vec::new(),
410 );
411 assert_eq!(
412 result.veto_reason.as_deref(),
413 Some("Calculated result exceeds decimal value limit")
414 );
415 }
416
417 fn test_money_type() -> LemmaType {
418 LemmaType::new(
419 "money".to_string(),
420 TypeSpecification::Measure {
421 minimum: None,
422 maximum: None,
423 decimals: Some(2),
424 units: MeasureUnits::from(vec![
425 MeasureUnit {
426 name: "eur".to_string(),
427 factor: crate::computation::rational::rational_one(),
428 derived_measure_factors: Vec::new(),
429 decomposition: BaseMeasureVector::new(),
430 minimum: None,
431 maximum: None,
432 suggestion_magnitude: None,
433 },
434 MeasureUnit {
435 name: "usd".to_string(),
436 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
437 91, 2,
438 ))
439 .expect("factor"),
440 derived_measure_factors: Vec::new(),
441 decomposition: BaseMeasureVector::new(),
442 minimum: None,
443 maximum: None,
444 suggestion_magnitude: None,
445 },
446 ]),
447 traits: Vec::new(),
448 decomposition: Some(BaseMeasureVector::new()),
449 help: String::new(),
450 },
451 TypeExtends::Primitive,
452 )
453 }
454
455 #[test]
456 fn measure_rule_result_value_uses_rule_type_when_expression_index_empty() {
457 let money = test_money_type();
458 let ten_usd = LiteralValue {
459 value: ValueKind::Measure(
460 crate::computation::rational::checked_mul(
461 &crate::computation::rational::decimal_to_rational(Decimal::from(10))
462 .expect("ten"),
463 &crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
464 .expect("usd factor"),
465 )
466 .expect("canonical usd"),
467 vec![("usd".to_string(), 1)],
468 ),
469 lemma_type: Arc::new(money.clone()),
470 };
471 let result = RuleResult::from_operation_result(
472 dummy_evaluated_rule("total", &money),
473 &OperationResult::from_literal(ten_usd),
474 &money,
475 None,
476 Vec::new(),
477 );
478 let measure = result
479 .value
480 .as_ref()
481 .expect("value")
482 .measure
483 .clone()
484 .expect("measure map");
485 assert_eq!(measure.get("usd"), Some(&"10.00".to_string()));
486 assert!(measure.contains_key("eur"));
487 }
488
489 #[test]
490 fn test_measure_rule_result_value_multi_unit() {
491 let money = test_money_type();
492 let ten_eur = LiteralValue {
493 value: ValueKind::Measure(
494 crate::computation::rational::decimal_to_rational(Decimal::from(10)).expect("ten"),
495 vec![("eur".to_string(), 1)],
496 ),
497 lemma_type: Arc::new(money.clone()),
498 };
499 let result = RuleResult::from_operation_result(
500 dummy_evaluated_rule("total", &money),
501 &OperationResult::from_literal(ten_eur),
502 &money,
503 None,
504 Vec::new(),
505 );
506 let measure = result
507 .value
508 .as_ref()
509 .expect("value")
510 .measure
511 .clone()
512 .expect("measure map");
513 assert_eq!(measure.get("eur"), Some(&"10.00".to_string()));
514 assert_eq!(measure.get("usd"), Some(&"10.99".to_string()));
515 }
516
517 #[test]
518 fn measure_rule_result_value_respects_decimals_on_unit_conversion() {
519 let money = LemmaType::new(
520 "money".to_string(),
521 TypeSpecification::Measure {
522 minimum: None,
523 maximum: None,
524 decimals: Some(2),
525 units: MeasureUnits::from(vec![
526 MeasureUnit {
527 name: "eur".to_string(),
528 factor: crate::computation::rational::rational_one(),
529 derived_measure_factors: Vec::new(),
530 decomposition: BaseMeasureVector::new(),
531 minimum: None,
532 maximum: None,
533 suggestion_magnitude: None,
534 },
535 MeasureUnit {
536 name: "usd".to_string(),
537 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
538 84, 2,
539 ))
540 .expect("usd factor"),
541 derived_measure_factors: Vec::new(),
542 decomposition: BaseMeasureVector::new(),
543 minimum: None,
544 maximum: None,
545 suggestion_magnitude: None,
546 },
547 ]),
548 traits: Vec::new(),
549 decomposition: Some(BaseMeasureVector::new()),
550 help: String::new(),
551 },
552 TypeExtends::Primitive,
553 );
554 let three_twelve_eur = LiteralValue {
555 value: ValueKind::Measure(
556 crate::computation::rational::decimal_to_rational(Decimal::new(312, 2))
557 .expect("3.12 eur canonical"),
558 vec![("eur".to_string(), 1)],
559 ),
560 lemma_type: Arc::new(money.clone()),
561 };
562 let result = RuleResult::from_operation_result(
563 dummy_evaluated_rule("delivery_cost", &money),
564 &OperationResult::from_literal(three_twelve_eur),
565 &money,
566 None,
567 Vec::new(),
568 );
569 let measure = result
570 .value
571 .as_ref()
572 .expect("value")
573 .measure
574 .clone()
575 .expect("measure map");
576 assert_eq!(measure.get("eur"), Some(&"3.12".to_string()));
577 assert_eq!(measure.get("usd"), Some(&"3.71".to_string()));
578 }
579
580 #[test]
581 fn test_ratio_rule_result_value_multi_unit() {
582 let ratio_type = LemmaType::new(
583 "rate".to_string(),
584 TypeSpecification::Ratio {
585 minimum: None,
586 maximum: None,
587 decimals: None,
588 units: RatioUnits::from(vec![
589 RatioUnit {
590 name: "percent".to_string(),
591 value: crate::computation::rational::decimal_to_rational(Decimal::from(
592 100,
593 ))
594 .expect("percent"),
595 minimum: None,
596 maximum: None,
597 suggestion_magnitude: None,
598 },
599 RatioUnit {
600 name: "basis_points".to_string(),
601 value: crate::computation::rational::decimal_to_rational(Decimal::from(
602 10_000,
603 ))
604 .expect("bp"),
605 minimum: None,
606 maximum: None,
607 suggestion_magnitude: None,
608 },
609 ]),
610 help: String::new(),
611 },
612 TypeExtends::Primitive,
613 );
614 let half = crate::computation::rational::rational_new(1, 2);
615 let lit = LiteralValue {
616 value: ValueKind::Ratio(half, Some("percent".to_string())),
617 lemma_type: Arc::new(ratio_type.clone()),
618 };
619 let result = RuleResult::from_operation_result(
620 dummy_evaluated_rule("rate_out", &ratio_type),
621 &OperationResult::from_literal(lit),
622 &ratio_type,
623 None,
624 Vec::new(),
625 );
626 let ratio = result
627 .value
628 .as_ref()
629 .expect("value")
630 .ratio
631 .clone()
632 .expect("ratio map");
633 assert_eq!(ratio.get("percent"), Some(&"50".to_string()));
634 assert_eq!(ratio.get("basis_points"), Some(&"5000".to_string()));
635 }
636
637 #[test]
638 fn test_measure_rule_result_value_cross_spec_import() {
639 use crate::parsing::source::SourceType;
640 use crate::Engine;
641
642 let mut engine = Engine::new();
643 engine
644 .load([(
645 SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("t.lemma"))),
646 r#"
647spec consumer 2025-01-01
648uses d: dep 2025-10-01
649rule out: d.doubled
650
651spec dep 2025-01-01
652uses c: child 2025-06-01
653data money: c.money
654data p: 5 usd
655rule doubled: p * 2
656
657spec child 2025-01-01
658data money: measure
659 -> unit eur: 1.00
660 -> decimals 2
661
662spec child 2025-06-01
663data money: measure
664 -> unit eur: 1.00
665 -> unit usd: 0.91
666 -> decimals 2
667"#
668 .to_string(),
669 )])
670 .expect("load");
671 let effective = crate::literals::DateTimeValue {
672 year: 2025,
673 month: 3,
674 day: 1,
675 hour: 0,
676 minute: 0,
677 second: 0,
678 microsecond: 0,
679 timezone: None,
680
681 granularity: DateGranularity::Full,
682 };
683 let response = engine
684 .run(
685 None,
686 "consumer",
687 Some(&effective),
688 std::collections::HashMap::new(),
689 None,
690 false,
691 )
692 .expect("run");
693 let out = response.results.get("out").expect("out rule");
694 assert!(!out.vetoed);
695 let measure = out
696 .value
697 .as_ref()
698 .expect("value")
699 .measure
700 .as_ref()
701 .expect("measure map");
702 assert!(measure.contains_key("usd"));
703 assert!(measure.contains_key("eur"));
704 }
705
706 #[test]
707 fn to_literal_roundtrips_number() {
708 let literal = LiteralValue::number_from_decimal(Decimal::from(42));
709 let rule_result = RuleResult::from_operation_result(
710 dummy_evaluated_rule("answer", primitive_number_arc().as_ref()),
711 &OperationResult::from_literal(literal.clone()),
712 primitive_number_arc().as_ref(),
713 None,
714 Vec::new(),
715 );
716 assert_eq!(rule_result.to_literal(), literal);
717 }
718
719 #[test]
720 fn to_literal_roundtrips_measure() {
721 let money = test_money_type();
722 let literal = LiteralValue::measure_with_type(
723 crate::computation::rational::rational_new(60, 1),
724 "eur".into(),
725 Arc::new(money.clone()),
726 );
727 let rule_result = RuleResult::from_operation_result(
728 dummy_evaluated_rule("pay", &money),
729 &OperationResult::from_literal(literal.clone()),
730 &money,
731 None,
732 Vec::new(),
733 );
734 assert_eq!(rule_result.to_literal(), literal);
735 }
736}