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