1use crate::computation::UnitResolutionContext;
12use crate::parsing::ast::{EffectiveDate, LemmaRepository, LemmaSpec, MetaValue};
13use crate::parsing::source::Source;
14use crate::planning::graph::Graph;
15use crate::planning::graph::ResolvedSpecTypes;
16use crate::planning::normalize::{build_unless_chain, inline_rule_refs, normalize_expression};
17use crate::planning::semantics::{
18 DataDefinition, DataPath, Expression, LemmaType, LiteralValue, RulePath, SemanticCalendarUnit,
19 TypeSpecification, ValueKind,
20};
21use crate::Error;
22use crate::ResourceLimits;
23use indexmap::IndexMap;
24use serde::{Deserialize, Serialize};
25use std::collections::{BTreeSet, HashMap, HashSet};
26use std::sync::Arc;
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SpecSource {
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub repository: Option<String>,
38 pub name: String,
39 pub effective_from: EffectiveDate,
40 pub source: String,
41}
42
43pub type SpecSources = Vec<SpecSource>;
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ExecutionPlan {
51 pub spec_name: String,
53
54 #[serde(serialize_with = "crate::serialization::serialize_resolved_data_value_map")]
56 #[serde(deserialize_with = "crate::serialization::deserialize_resolved_data_value_map")]
57 pub data: IndexMap<DataPath, DataDefinition>,
58
59 pub rules: Vec<ExecutableRule>,
61
62 #[serde(default, alias = "alias_evaluation_order")]
67 pub reference_evaluation_order: Vec<DataPath>,
68
69 pub meta: HashMap<String, MetaValue>,
71
72 #[serde(default)]
75 pub unit_index: HashMap<String, LemmaType>,
76
77 pub effective: EffectiveDate,
78
79 #[serde(default)]
82 pub sources: SpecSources,
83}
84
85#[derive(Debug, Clone)]
88pub struct ExecutionPlanSet {
89 pub spec_name: String,
90 pub plans: Vec<ExecutionPlan>,
91}
92
93impl ExecutionPlanSet {
94 #[must_use]
96 pub fn plan_at(&self, effective: &EffectiveDate) -> Option<&ExecutionPlan> {
97 for (i, plan) in self.plans.iter().enumerate() {
98 let from_ok = *effective >= plan.effective;
99 let to_ok = self
100 .plans
101 .get(i + 1)
102 .map(|next| *effective < next.effective)
103 .unwrap_or(true);
104 if from_ok && to_ok {
105 return Some(plan);
106 }
107 }
108 None
109 }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ExecutableRule {
117 pub path: RulePath,
119
120 pub name: String,
122
123 pub branches: Vec<Branch>,
128
129 pub needs_data: BTreeSet<DataPath>,
131
132 pub source: Source,
134
135 pub rule_type: LemmaType,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct Branch {
143 pub condition: Option<Expression>,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub normalized_condition: Option<Expression>,
149
150 pub result: Expression,
152
153 pub normalized_result: Expression,
155
156 pub source: Source,
158}
159
160fn build_rule_normalized_result_expression(branches: &[Branch]) -> Expression {
163 let pairs: Vec<(Option<Expression>, Expression)> = branches
164 .iter()
165 .map(|b| {
166 let condition = b.condition.as_ref().map(|_| {
167 b.normalized_condition
168 .clone()
169 .expect("BUG: normalized_condition must exist when condition exists")
170 });
171 (condition, b.normalized_result.clone())
172 })
173 .collect();
174 build_unless_chain(&pairs)
175}
176
177pub(crate) fn build_execution_plan(
180 graph: &Graph,
181 resolved_types: &[(Arc<LemmaRepository>, Arc<LemmaSpec>, ResolvedSpecTypes)],
182 effective: &EffectiveDate,
183) -> Result<ExecutionPlan, Vec<Error>> {
184 let data = graph.build_data();
185 let execution_order = graph.execution_order();
186
187 let main_spec = graph.main_spec();
188 let unit_index = resolved_types
189 .iter()
190 .find(|(_, spec, _)| Arc::ptr_eq(spec, main_spec))
191 .map(|(_, _, types)| types.unit_index.clone())
192 .unwrap_or_default();
193
194 let mut executable_rules: Vec<ExecutableRule> = Vec::new();
195 let mut path_to_index: HashMap<RulePath, usize> = HashMap::new();
196 let mut normalized_rule_results: HashMap<RulePath, Expression> = HashMap::new();
197
198 for rule_path in execution_order {
199 let rule_node = graph.rules().get(rule_path).expect(
200 "bug: rule from topological sort not in graph - validation should have caught this",
201 );
202
203 let mut direct_data = HashSet::new();
204 for (condition, result) in &rule_node.branches {
205 if let Some(cond) = condition {
206 cond.collect_data_paths(&mut direct_data);
207 }
208 result.collect_data_paths(&mut direct_data);
209 }
210 let mut needs_data: BTreeSet<DataPath> = direct_data.into_iter().collect();
211
212 for dep in &rule_node.depends_on_rules {
213 if let Some(&dep_idx) = path_to_index.get(dep) {
214 needs_data.extend(executable_rules[dep_idx].needs_data.iter().cloned());
215 }
216 }
217
218 let mut executable_branches = Vec::new();
219 let unit_ctx = UnitResolutionContext::WithIndex(&unit_index);
220 for (condition, result) in &rule_node.branches {
221 let inlined = inline_rule_refs(result, &normalized_rule_results);
222 let normalized_result =
223 normalize_expression(&inlined, Some(&unit_ctx)).map_err(|error| {
224 vec![Error::validation(
225 format!("failed to normalize rule result: {error}"),
226 Some(rule_node.source.clone()),
227 None::<String>,
228 )]
229 })?;
230 let normalized_condition = match condition {
231 Some(condition) => Some(normalize_expression(condition, Some(&unit_ctx)).map_err(
232 |error| {
233 vec![Error::validation(
234 format!("failed to normalize unless condition: {error}"),
235 Some(rule_node.source.clone()),
236 None::<String>,
237 )]
238 },
239 )?),
240 None => None,
241 };
242 executable_branches.push(Branch {
243 condition: condition.clone(),
244 normalized_condition,
245 result: result.clone(),
246 normalized_result,
247 source: rule_node.source.clone(),
248 });
249 }
250
251 normalized_rule_results.insert(
252 rule_path.clone(),
253 build_rule_normalized_result_expression(&executable_branches),
254 );
255
256 path_to_index.insert(rule_path.clone(), executable_rules.len());
257 executable_rules.push(ExecutableRule {
258 path: rule_path.clone(),
259 name: rule_path.rule.clone(),
260 branches: executable_branches,
261 source: rule_node.source.clone(),
262 needs_data,
263 rule_type: rule_node.rule_type.clone(),
264 });
265 }
266
267 let mut sources: SpecSources = Vec::new();
268 for (repo, spec, _) in resolved_types.iter() {
269 if !sources.iter().any(|e| {
270 e.repository == repo.name
271 && e.name == spec.name
272 && e.effective_from == spec.effective_from
273 }) {
274 sources.push(SpecSource {
275 repository: repo.name.clone(),
276 name: spec.name.clone(),
277 effective_from: spec.effective_from.clone(),
278 source: crate::formatting::format_specs(&[spec.as_ref().clone()]),
279 });
280 }
281 }
282
283 Ok(ExecutionPlan {
284 spec_name: main_spec.name.clone(),
285 data,
286 rules: executable_rules,
287 reference_evaluation_order: graph.reference_evaluation_order().to_vec(),
288 meta: main_spec
289 .meta_fields
290 .iter()
291 .map(|f| (f.key.clone(), f.value.clone()))
292 .collect(),
293 unit_index,
294 effective: effective.clone(),
295 sources,
296 })
297}
298
299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323pub struct DataEntry {
324 #[serde(rename = "type")]
325 pub lemma_type: LemmaType,
326 #[serde(skip_serializing_if = "Option::is_none", default)]
327 pub bound_value: Option<LiteralValue>,
328 #[serde(skip_serializing_if = "Option::is_none", default)]
329 pub default: Option<LiteralValue>,
330}
331
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
333pub struct SpecSchema {
334 pub spec: String,
336 pub data: indexmap::IndexMap<String, DataEntry>,
338 pub rules: indexmap::IndexMap<String, LemmaType>,
340 pub meta: HashMap<String, MetaValue>,
342}
343
344impl std::fmt::Display for SpecSchema {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 write!(f, "Spec: {}", self.spec)?;
347
348 if !self.meta.is_empty() {
349 write!(f, "\n\nMeta:")?;
350 let mut entries: Vec<(&String, &MetaValue)> = self.meta.iter().collect();
352 entries.sort_by_key(|(k, _)| *k);
353 for (key, value) in entries {
354 write!(f, "\n {}: {}", key, value)?;
355 }
356 }
357
358 if !self.data.is_empty() {
359 write!(f, "\n\nData:")?;
360 for (name, entry) in &self.data {
361 write!(f, "\n {} ({}", name, entry.lemma_type.name())?;
362 if let Some(constraints) = format_type_constraints(&entry.lemma_type.specifications)
363 {
364 write!(f, ", {}", constraints)?;
365 }
366 if let Some(val) = &entry.bound_value {
367 write!(f, ", value: {}", val)?;
368 }
369 if let Some(val) = &entry.default {
370 write!(f, ", default: {}", val)?;
371 }
372 write!(f, ")")?;
373 }
374 }
375
376 if !self.rules.is_empty() {
377 write!(f, "\n\nRules:")?;
378 for (name, rule_type) in &self.rules {
379 write!(f, "\n {} ({})", name, rule_type.name())?;
380 }
381 }
382
383 if self.data.is_empty() && self.rules.is_empty() {
384 write!(f, "\n (no data or rules)")?;
385 }
386
387 Ok(())
388 }
389}
390
391impl SpecSchema {
392 pub(crate) fn is_type_compatible(&self, other: &SpecSchema) -> bool {
397 for (name, entry) in &self.data {
398 if let Some(other_entry) = other.data.get(name) {
399 if entry.lemma_type != other_entry.lemma_type {
400 return false;
401 }
402 }
403 }
404 for (name, lt) in &self.rules {
405 if let Some(other_lt) = other.rules.get(name) {
406 if lt != other_lt {
407 return false;
408 }
409 }
410 }
411 true
412 }
413}
414
415fn format_type_constraints(spec: &TypeSpecification) -> Option<String> {
418 let mut parts = Vec::new();
419
420 match spec {
421 TypeSpecification::Number {
422 minimum, maximum, ..
423 } => {
424 if let Some(v) = minimum {
425 parts.push(format!("minimum: {}", v));
426 }
427 if let Some(v) = maximum {
428 parts.push(format!("maximum: {}", v));
429 }
430 }
431 TypeSpecification::Quantity {
432 minimum,
433 maximum,
434 decimals,
435 units,
436 ..
437 } => {
438 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
439 if !unit_names.is_empty() {
440 parts.push(format!("units: {}", unit_names.join(", ")));
441 }
442 if let Some((magnitude, unit_name)) = minimum {
443 parts.push(format!("minimum: {} {}", magnitude, unit_name));
444 }
445 if let Some((magnitude, unit_name)) = maximum {
446 parts.push(format!("maximum: {} {}", magnitude, unit_name));
447 }
448 if let Some(d) = decimals {
449 parts.push(format!("decimals: {}", d));
450 }
451 }
452 TypeSpecification::Ratio {
453 minimum, maximum, ..
454 } => {
455 if let Some(v) = minimum {
456 parts.push(format!("minimum: {}", v));
457 }
458 if let Some(v) = maximum {
459 parts.push(format!("maximum: {}", v));
460 }
461 }
462 TypeSpecification::Text { options, .. } => {
463 if !options.is_empty() {
464 let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
465 parts.push(format!("options: {}", quoted.join(", ")));
466 }
467 }
468 TypeSpecification::Date {
469 minimum, maximum, ..
470 } => {
471 if let Some(v) = minimum {
472 parts.push(format!("minimum: {}", v));
473 }
474 if let Some(v) = maximum {
475 parts.push(format!("maximum: {}", v));
476 }
477 }
478 TypeSpecification::Time {
479 minimum, maximum, ..
480 } => {
481 if let Some(v) = minimum {
482 parts.push(format!("minimum: {}", v));
483 }
484 if let Some(v) = maximum {
485 parts.push(format!("maximum: {}", v));
486 }
487 }
488 TypeSpecification::Boolean { .. }
489 | TypeSpecification::NumberRange { .. }
490 | TypeSpecification::QuantityRange { .. }
491 | TypeSpecification::DateRange { .. }
492 | TypeSpecification::RatioRange { .. }
493 | TypeSpecification::CalendarRange { .. }
494 | TypeSpecification::Calendar { .. }
495 | TypeSpecification::Veto { .. }
496 | TypeSpecification::Undetermined => {}
497 }
498
499 if parts.is_empty() {
500 None
501 } else {
502 Some(parts.join(", "))
503 }
504}
505
506impl ExecutionPlan {
507 pub fn schema(&self) -> SpecSchema {
515 let all_local_rules: Vec<String> = self
516 .rules
517 .iter()
518 .filter(|r| r.path.segments.is_empty())
519 .map(|r| r.name.clone())
520 .collect();
521 self.schema_for_rules(&all_local_rules)
522 .expect("BUG: all_local_rules sourced from self.rules")
523 }
524
525 pub(crate) fn interface_schema(&self) -> SpecSchema {
527 let mut data_entries: Vec<(usize, String, DataEntry)> = self
528 .data
529 .iter()
530 .filter(|(_, data)| data.schema_type().is_some())
531 .map(|(path, data)| {
532 let lemma_type = data
533 .schema_type()
534 .expect("BUG: filter above ensured schema_type is Some")
535 .clone();
536 let bound_value = data.bound_value().cloned();
537 let default = data.default_suggestion();
538 (
539 data.source().span.start,
540 path.input_key(),
541 DataEntry {
542 lemma_type,
543 bound_value,
544 default,
545 },
546 )
547 })
548 .collect();
549 data_entries.sort_by_key(|(pos, _, _)| *pos);
550
551 let rule_entries: Vec<(String, LemmaType)> = self
552 .rules
553 .iter()
554 .filter(|r| r.path.segments.is_empty())
555 .map(|r| (r.name.clone(), r.rule_type.clone()))
556 .collect();
557
558 SpecSchema {
559 spec: self.spec_name.clone(),
560 data: data_entries
561 .into_iter()
562 .map(|(_, name, data)| (name, data))
563 .collect(),
564 rules: rule_entries.into_iter().collect(),
565 meta: self.meta.clone(),
566 }
567 }
568
569 pub fn schema_for_rules(&self, rule_names: &[String]) -> Result<SpecSchema, Error> {
578 let mut needed_data = HashSet::new();
579 let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
580
581 for rule_name in rule_names {
582 let rule = self.get_rule(rule_name).ok_or_else(|| {
583 Error::request(
584 format!(
585 "Rule '{}' not found in spec '{}'",
586 rule_name, self.spec_name
587 ),
588 None::<String>,
589 )
590 })?;
591 needed_data.extend(rule.needs_data.iter().cloned());
592 rule_entries.push((rule.name.clone(), rule.rule_type.clone()));
593 }
594
595 let mut data_entries: Vec<(usize, String, DataEntry)> = self
596 .data
597 .iter()
598 .filter(|(path, _)| needed_data.contains(path))
599 .filter_map(|(path, data)| {
600 let lemma_type = data.schema_type()?.clone();
601 let bound_value = data.bound_value().cloned();
602 let default = data.default_suggestion();
603 Some((
604 data.source().span.start,
605 path.input_key(),
606 DataEntry {
607 lemma_type,
608 bound_value,
609 default,
610 },
611 ))
612 })
613 .collect();
614 data_entries.sort_by_key(|(pos, _, _)| *pos);
615 let data_entries: Vec<(String, DataEntry)> = data_entries
616 .into_iter()
617 .map(|(_, name, data)| (name, data))
618 .collect();
619
620 Ok(SpecSchema {
621 spec: self.spec_name.clone(),
622 data: data_entries.into_iter().collect(),
623 rules: rule_entries.into_iter().collect(),
624 meta: self.meta.clone(),
625 })
626 }
627
628 pub fn get_data_path_by_str(&self, name: &str) -> Option<&DataPath> {
630 self.data.keys().find(|path| path.input_key() == name)
631 }
632
633 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
635 self.rules
636 .iter()
637 .find(|r| r.name == name && r.path.segments.is_empty())
638 }
639
640 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
642 self.rules.iter().find(|r| &r.path == rule_path)
643 }
644
645 pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
647 self.data.get(path).and_then(|d| d.value())
648 }
649
650 pub fn set_data_values(
654 mut self,
655 values: std::collections::HashMap<String, serde_json::Value>,
656 limits: &ResourceLimits,
657 ) -> Result<Self, Error> {
658 for (name, raw_value) in values {
659 let data_path = self.get_data_path_by_str(&name).ok_or_else(|| {
660 let available: Vec<String> = self.data.keys().map(|p| p.input_key()).collect();
661 Error::request(
662 format!(
663 "Data '{}' not found. Available data: {}",
664 name,
665 available.join(", ")
666 ),
667 None::<String>,
668 )
669 })?;
670 let data_path = data_path.clone();
671
672 let data_definition = self
673 .data
674 .get(&data_path)
675 .expect("BUG: data_path was just resolved from self.data, must exist");
676
677 let data_source = data_definition.source().clone();
678 let expected_type = data_definition.schema_type().cloned().ok_or_else(|| {
679 Error::request(
680 format!(
681 "Data '{}' is a spec reference; cannot provide a value.",
682 name
683 ),
684 None::<String>,
685 )
686 })?;
687
688 let literal_value = crate::planning::semantics::parse_data_value_from_json(
689 &raw_value,
690 &expected_type.specifications,
691 &expected_type,
692 &data_source,
693 )
694 .map_err(|e| e.with_related_data(&name))?;
695
696 let size = literal_value.byte_size();
697 if size > limits.max_data_value_bytes {
698 return Err(Error::resource_limit_exceeded(
699 "max_data_value_bytes",
700 limits.max_data_value_bytes.to_string(),
701 size.to_string(),
702 format!(
703 "Reduce the size of data values to {} bytes or less",
704 limits.max_data_value_bytes
705 ),
706 Some(data_source.clone()),
707 None,
708 None,
709 )
710 .with_related_data(&name));
711 }
712
713 validate_value_against_type(&expected_type, &literal_value).map_err(|msg| {
714 Error::validation(msg, Some(data_source.clone()), None::<String>)
715 .with_related_data(&name)
716 })?;
717
718 self.data.insert(
719 data_path,
720 DataDefinition::Value {
721 value: literal_value,
722 source: data_source,
723 },
724 );
725 }
726
727 Ok(self)
728 }
729
730 #[must_use]
734 pub fn with_defaults(mut self) -> Self {
735 let promotions: Vec<(DataPath, DataDefinition)> = self
736 .data
737 .iter()
738 .filter_map(|(path, def)| {
739 if let DataDefinition::TypeDeclaration {
740 declared_default: Some(dv),
741 resolved_type,
742 source,
743 } = def
744 {
745 Some((
746 path.clone(),
747 DataDefinition::Value {
748 value: LiteralValue {
749 value: dv.clone(),
750 lemma_type: resolved_type.clone(),
751 },
752 source: source.clone(),
753 },
754 ))
755 } else {
756 None
757 }
758 })
759 .collect();
760
761 for (path, def) in promotions {
762 self.data.insert(path, def);
763 }
764 self
765 }
766}
767
768pub(crate) fn validate_value_against_type(
769 expected_type: &LemmaType,
770 value: &LiteralValue,
771) -> Result<(), String> {
772 use crate::computation::rational::{commit_rational_to_decimal, RationalInteger};
773 use crate::planning::semantics::TypeSpecification;
774
775 fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
776 match commit_rational_to_decimal(magnitude) {
777 Ok(decimal) => decimal.scale() > u32::from(max_decimals),
778 Err(_) => true,
779 }
780 }
781
782 fn format_rational(r: &RationalInteger, decimals: Option<u8>) -> String {
783 use crate::computation::rational::rational_to_display_str;
784 match commit_rational_to_decimal(r) {
785 Ok(decimal) => match decimals {
786 Some(dp) => {
787 let rounded = decimal.round_dp(u32::from(dp));
788 format!("{:.prec$}", rounded, prec = dp as usize)
789 }
790 None => decimal.normalize().to_string(),
791 },
792 Err(_) => rational_to_display_str(r),
793 }
794 }
795
796 match (&expected_type.specifications, &value.value) {
797 (
798 TypeSpecification::Number {
799 minimum,
800 maximum,
801 decimals,
802 ..
803 },
804 ValueKind::Number(n),
805 ) => {
806 if let Some(d) = decimals {
807 if exceeds_decimal_places(n, *d) {
808 return Err(format!(
809 "{} exceeds decimals constraint {d}",
810 format_rational(n, *decimals)
811 ));
812 }
813 }
814 if let Some(min) = minimum {
815 if n < min {
816 return Err(format!(
817 "{} is below minimum {}",
818 format_rational(n, *decimals),
819 format_rational(min, *decimals)
820 ));
821 }
822 }
823 if let Some(max) = maximum {
824 if n > max {
825 return Err(format!(
826 "{} is above maximum {}",
827 format_rational(n, *decimals),
828 format_rational(max, *decimals)
829 ));
830 }
831 }
832 Ok(())
833 }
834 (
835 TypeSpecification::Quantity {
836 minimum,
837 maximum,
838 decimals,
839 units,
840 ..
841 },
842 ValueKind::Quantity(magnitude, unit, _),
843 ) => {
844 if let Some(d) = decimals {
845 if exceeds_decimal_places(magnitude, *d) {
846 return Err(format!(
847 "{} {unit} exceeds decimals constraint {d}",
848 format_rational(magnitude, *decimals)
849 ));
850 }
851 }
852 let quantity_unit = units.get(unit)?;
853 if minimum.is_some() {
854 let unit_minimum = quantity_unit.minimum.expect(
855 "BUG: QuantityUnit.minimum missing after type minimum set by sync_quantity_units_from_canonical",
856 );
857 if magnitude < &unit_minimum {
858 let value_display =
859 format!("{} {}", format_rational(magnitude, *decimals), unit);
860 let bound_display = format!(
861 "{} {}",
862 format_rational(&unit_minimum, *decimals),
863 quantity_unit.name
864 );
865 return Err(format!("{value_display} is below minimum {bound_display}"));
866 }
867 }
868 if maximum.is_some() {
869 let unit_maximum = quantity_unit.maximum.expect(
870 "BUG: QuantityUnit.maximum missing after type maximum set by sync_quantity_units_from_canonical",
871 );
872 if magnitude > &unit_maximum {
873 let value_display =
874 format!("{} {}", format_rational(magnitude, *decimals), unit);
875 let bound_display = format!(
876 "{} {}",
877 format_rational(&unit_maximum, *decimals),
878 quantity_unit.name
879 );
880 return Err(format!("{value_display} is above maximum {bound_display}"));
881 }
882 }
883 Ok(())
884 }
885 (
886 TypeSpecification::Text {
887 length, options, ..
888 },
889 ValueKind::Text(s),
890 ) => {
891 let len = s.chars().count();
892 if let Some(exact) = length {
893 if len != *exact {
894 return Err(format!(
895 "'{}' has length {} but required length is {}",
896 s, len, exact
897 ));
898 }
899 }
900 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
901 return Err(format!(
902 "'{}' is not in allowed options: {}",
903 s,
904 options.join(", ")
905 ));
906 }
907 Ok(())
908 }
909 (
910 TypeSpecification::Ratio {
911 minimum,
912 maximum,
913 decimals,
914 units,
915 ..
916 },
917 ValueKind::Ratio(r, unit_name),
918 ) => {
919 use crate::computation::rational::checked_mul;
920
921 if let Some(d) = decimals {
922 if exceeds_decimal_places(r, *d) {
923 return Err(format!(
924 "{} exceeds decimals constraint {d}",
925 format_rational(r, *decimals)
926 ));
927 }
928 }
929 if let Some(type_minimum) = minimum {
930 if r < type_minimum {
931 let message = match unit_name.as_deref() {
932 Some(unit) => {
933 let ratio_unit = units.get(unit)?;
934 let value_per_unit = checked_mul(r, &ratio_unit.value)
935 .map_err(|failure| failure.to_string())?;
936 let bound_per_unit = ratio_unit.minimum.expect(
937 "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
938 );
939 format!(
940 "{} {unit} is below minimum {} {unit}",
941 format_rational(&value_per_unit, *decimals),
942 format_rational(&bound_per_unit, *decimals),
943 )
944 }
945 None => format!(
946 "{} is below minimum {}",
947 format_rational(r, *decimals),
948 format_rational(type_minimum, *decimals),
949 ),
950 };
951 return Err(message);
952 }
953 }
954 if let Some(type_maximum) = maximum {
955 if r > type_maximum {
956 let message = match unit_name.as_deref() {
957 Some(unit) => {
958 let ratio_unit = units.get(unit)?;
959 let value_per_unit = checked_mul(r, &ratio_unit.value)
960 .map_err(|failure| failure.to_string())?;
961 let bound_per_unit = ratio_unit.maximum.expect(
962 "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
963 );
964 format!(
965 "{} {unit} is above maximum {} {unit}",
966 format_rational(&value_per_unit, *decimals),
967 format_rational(&bound_per_unit, *decimals),
968 )
969 }
970 None => format!(
971 "{} is above maximum {}",
972 format_rational(r, *decimals),
973 format_rational(type_maximum, *decimals),
974 ),
975 };
976 return Err(message);
977 }
978 }
979 Ok(())
980 }
981 (
982 TypeSpecification::Date {
983 minimum, maximum, ..
984 },
985 ValueKind::Date(dt),
986 ) => {
987 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
988 use std::cmp::Ordering;
989 if let Some(min) = minimum {
990 let min_sem = date_time_to_semantic(min);
991 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
992 return Err(format!("{} is below minimum {}", dt, min));
993 }
994 }
995 if let Some(max) = maximum {
996 let max_sem = date_time_to_semantic(max);
997 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
998 return Err(format!("{} is above maximum {}", dt, max));
999 }
1000 }
1001 Ok(())
1002 }
1003 (
1004 TypeSpecification::Calendar {
1005 minimum, maximum, ..
1006 },
1007 ValueKind::Calendar(value, unit),
1008 ) => {
1009 let value_months = crate::computation::units::convert_calendar_magnitude(
1010 *value,
1011 unit,
1012 &SemanticCalendarUnit::Month,
1013 );
1014 if let Some((min_val, min_unit)) = minimum {
1015 let min_months = crate::computation::units::convert_calendar_magnitude(
1016 *min_val,
1017 min_unit,
1018 &SemanticCalendarUnit::Month,
1019 );
1020 if value_months < min_months {
1021 return Err(format!(
1022 "{value} {unit} is below minimum {min_val} {min_unit}"
1023 ));
1024 }
1025 }
1026 if let Some((max_val, max_unit)) = maximum {
1027 let max_months = crate::computation::units::convert_calendar_magnitude(
1028 *max_val,
1029 max_unit,
1030 &SemanticCalendarUnit::Month,
1031 );
1032 if value_months > max_months {
1033 return Err(format!(
1034 "{value} {unit} is above maximum {max_val} {max_unit}"
1035 ));
1036 }
1037 }
1038 Ok(())
1039 }
1040 (
1041 TypeSpecification::Time {
1042 minimum, maximum, ..
1043 },
1044 ValueKind::Time(t),
1045 ) => {
1046 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1047 use std::cmp::Ordering;
1048 if let Some(min) = minimum {
1049 let min_sem = time_to_semantic(min);
1050 if compare_semantic_times(t, &min_sem) == Ordering::Less {
1051 return Err(format!("{} is below minimum {}", t, min));
1052 }
1053 }
1054 if let Some(max) = maximum {
1055 let max_sem = time_to_semantic(max);
1056 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1057 return Err(format!("{} is above maximum {}", t, max));
1058 }
1059 }
1060 Ok(())
1061 }
1062 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1063 | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1064 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1065 | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1066 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1067 | (TypeSpecification::CalendarRange { .. }, ValueKind::Range(_, _))
1068 | (TypeSpecification::Veto { .. }, _)
1069 | (TypeSpecification::Undetermined, _) => Ok(()),
1070 (spec, value_kind) => unreachable!(
1071 "BUG: validate_value_against_type called with mismatched type/value: \
1072 spec={:?}, value={:?} — typing must be enforced before validation",
1073 spec, value_kind
1074 ),
1075 }
1076}
1077
1078pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1079 let mut errors = Vec::new();
1080
1081 for (data_path, data_definition) in &plan.data {
1082 let (expected_type, lit) = match data_definition {
1083 DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1084 DataDefinition::TypeDeclaration { .. }
1085 | DataDefinition::Import { .. }
1086 | DataDefinition::Reference { .. } => continue,
1087 };
1088
1089 if let Err(msg) = validate_value_against_type(expected_type, lit) {
1090 let source = data_definition.source().clone();
1091 errors.push(Error::validation(
1092 format!(
1093 "Invalid value for data {} (expected {}): {}",
1094 data_path,
1095 expected_type.name(),
1096 msg
1097 ),
1098 Some(source),
1099 None::<String>,
1100 ));
1101 }
1102 }
1103
1104 errors
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use super::*;
1110 use crate::computation::rational::{rational_zero, RationalInteger};
1111 use crate::parsing::ast::DateTimeValue;
1112 use crate::planning::semantics::{
1113 primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
1114 };
1115 use crate::Engine;
1116 use serde_json;
1117 use std::str::FromStr;
1118 use std::sync::Arc;
1119
1120 fn default_limits() -> ResourceLimits {
1121 ResourceLimits::default()
1122 }
1123
1124 fn json_data(pairs: &[(&str, &str)]) -> HashMap<String, serde_json::Value> {
1125 pairs
1126 .iter()
1127 .map(|(k, v)| (k.to_string(), serde_json::Value::String((*v).to_string())))
1128 .collect()
1129 }
1130
1131 #[test]
1132 fn test_with_raw_values() {
1133 let mut engine = Engine::new();
1134 engine
1135 .load(
1136 r#"
1137 spec test
1138 data age: number -> default 25
1139 "#,
1140 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1141 "test.lemma",
1142 ))),
1143 )
1144 .unwrap();
1145
1146 let now = DateTimeValue::now();
1147 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1148 let data_path = DataPath::new(vec![], "age".to_string());
1149
1150 let values = json_data(&[("age", "30")]);
1151
1152 let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1153 let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1154 match &updated_value.value {
1155 crate::planning::semantics::ValueKind::Number(n) => {
1156 assert_eq!(*n, RationalInteger::new(30, 1));
1157 }
1158 other => panic!("Expected number literal, got {:?}", other),
1159 }
1160 }
1161
1162 #[test]
1163 fn test_with_raw_values_type_mismatch() {
1164 let mut engine = Engine::new();
1165 engine
1166 .load(
1167 r#"
1168 spec test
1169 data age: number
1170 "#,
1171 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1172 "test.lemma",
1173 ))),
1174 )
1175 .unwrap();
1176
1177 let now = DateTimeValue::now();
1178 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1179
1180 let values = json_data(&[("age", "thirty")]);
1181
1182 assert!(plan.set_data_values(values, &default_limits()).is_err());
1183 }
1184
1185 #[test]
1186 fn test_with_raw_values_unknown_data() {
1187 let mut engine = Engine::new();
1188 engine
1189 .load(
1190 r#"
1191 spec test
1192 data known: number
1193 "#,
1194 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1195 "test.lemma",
1196 ))),
1197 )
1198 .unwrap();
1199
1200 let now = DateTimeValue::now();
1201 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1202
1203 let values = json_data(&[("unknown", "30")]);
1204
1205 assert!(plan.set_data_values(values, &default_limits()).is_err());
1206 }
1207
1208 #[test]
1209 fn test_with_raw_values_nested() {
1210 let mut engine = Engine::new();
1211 engine
1212 .load(
1213 r#"
1214 spec private
1215 data base_price: number
1216
1217 spec test
1218 uses rules: private
1219 "#,
1220 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1221 "test.lemma",
1222 ))),
1223 )
1224 .unwrap();
1225
1226 let now = DateTimeValue::now();
1227 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1228
1229 let values = json_data(&[("rules.base_price", "100")]);
1230
1231 let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1232 let data_path = DataPath {
1233 segments: vec![PathSegment {
1234 data: "rules".to_string(),
1235 spec: "private".to_string(),
1236 }],
1237 data: "base_price".to_string(),
1238 };
1239 let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1240 match &updated_value.value {
1241 crate::planning::semantics::ValueKind::Number(n) => {
1242 assert_eq!(*n, RationalInteger::new(100, 1));
1243 }
1244 other => panic!("Expected number literal, got {:?}", other),
1245 }
1246 }
1247
1248 fn test_source() -> Source {
1249 use crate::parsing::ast::Span;
1250 Source::new(
1251 crate::parsing::source::SourceType::Volatile,
1252 Span {
1253 start: 0,
1254 end: 0,
1255 line: 1,
1256 col: 0,
1257 },
1258 )
1259 }
1260
1261 fn create_literal_expr(value: LiteralValue) -> Expression {
1262 Expression::new(
1263 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
1264 test_source(),
1265 )
1266 }
1267
1268 fn create_data_path_expr(path: DataPath) -> Expression {
1269 Expression::new(
1270 crate::planning::semantics::ExpressionKind::DataPath(path),
1271 test_source(),
1272 )
1273 }
1274
1275 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
1276 LiteralValue::number_from_decimal(n)
1277 }
1278
1279 fn create_boolean_literal(b: bool) -> LiteralValue {
1280 LiteralValue::from_bool(b)
1281 }
1282
1283 fn create_text_literal(s: String) -> LiteralValue {
1284 LiteralValue::text(s)
1285 }
1286
1287 #[test]
1288 fn with_values_should_enforce_number_maximum_constraint() {
1289 let data_path = DataPath::new(vec![], "x".to_string());
1292
1293 let max10 = crate::planning::semantics::LemmaType::primitive(
1294 crate::planning::semantics::TypeSpecification::Number {
1295 minimum: None,
1296 maximum: Some(RationalInteger::new(10, 1)),
1297 decimals: None,
1298 help: String::new(),
1299 },
1300 );
1301 let source = Source::new(
1302 crate::parsing::source::SourceType::Volatile,
1303 crate::parsing::ast::Span {
1304 start: 0,
1305 end: 0,
1306 line: 1,
1307 col: 0,
1308 },
1309 );
1310 let mut data = IndexMap::new();
1311 data.insert(
1312 data_path.clone(),
1313 crate::planning::semantics::DataDefinition::Value {
1314 value: crate::planning::semantics::LiteralValue::number_with_type(
1315 0.into(),
1316 max10.clone(),
1317 ),
1318 source: source.clone(),
1319 },
1320 );
1321
1322 let plan = ExecutionPlan {
1323 spec_name: "test".to_string(),
1324 data,
1325 rules: Vec::new(),
1326 reference_evaluation_order: Vec::new(),
1327 meta: HashMap::new(),
1328 unit_index: HashMap::new(),
1329 effective: EffectiveDate::Origin,
1330 sources: Vec::new(),
1331 };
1332
1333 let values = json_data(&[("x", "11")]);
1334
1335 assert!(
1336 plan.set_data_values(values, &default_limits()).is_err(),
1337 "Providing x=11 should fail due to maximum 10"
1338 );
1339 }
1340
1341 #[test]
1342 fn with_values_should_enforce_text_enum_options() {
1343 let data_path = DataPath::new(vec![], "tier".to_string());
1345
1346 let tier = crate::planning::semantics::LemmaType::primitive(
1347 crate::planning::semantics::TypeSpecification::Text {
1348 length: None,
1349 options: vec!["silver".to_string(), "gold".to_string()],
1350 help: String::new(),
1351 },
1352 );
1353 let source = Source::new(
1354 crate::parsing::source::SourceType::Volatile,
1355 crate::parsing::ast::Span {
1356 start: 0,
1357 end: 0,
1358 line: 1,
1359 col: 0,
1360 },
1361 );
1362 let mut data = IndexMap::new();
1363 data.insert(
1364 data_path.clone(),
1365 crate::planning::semantics::DataDefinition::Value {
1366 value: crate::planning::semantics::LiteralValue::text_with_type(
1367 "silver".to_string(),
1368 tier.clone(),
1369 ),
1370 source,
1371 },
1372 );
1373
1374 let plan = ExecutionPlan {
1375 spec_name: "test".to_string(),
1376 data,
1377 rules: Vec::new(),
1378 reference_evaluation_order: Vec::new(),
1379 meta: HashMap::new(),
1380 unit_index: HashMap::new(),
1381 effective: EffectiveDate::Origin,
1382 sources: Vec::new(),
1383 };
1384
1385 let values = json_data(&[("tier", "platinum")]);
1386
1387 assert!(
1388 plan.set_data_values(values, &default_limits()).is_err(),
1389 "Invalid enum value should be rejected (tier='platinum')"
1390 );
1391 }
1392
1393 #[test]
1394 fn with_values_should_enforce_quantity_decimals() {
1395 let data_path = DataPath::new(vec![], "price".to_string());
1398
1399 let money = crate::planning::semantics::LemmaType::primitive(
1400 crate::planning::semantics::TypeSpecification::Quantity {
1401 minimum: None,
1402 maximum: None,
1403 decimals: Some(2),
1404 units: crate::planning::semantics::QuantityUnits::from(vec![
1405 crate::planning::semantics::QuantityUnit::from_decimal_factor(
1406 "eur".to_string(),
1407 rust_decimal::Decimal::from_str("1.0").unwrap(),
1408 Vec::new(),
1409 )
1410 .expect("eur unit factor must be exact decimal"),
1411 ]),
1412 traits: Vec::new(),
1413 decomposition: crate::literals::BaseQuantityVector::new(),
1414 canonical_unit: "eur".to_string(),
1415 help: String::new(),
1416 },
1417 );
1418 let source = Source::new(
1419 crate::parsing::source::SourceType::Volatile,
1420 crate::parsing::ast::Span {
1421 start: 0,
1422 end: 0,
1423 line: 1,
1424 col: 0,
1425 },
1426 );
1427 let mut data = IndexMap::new();
1428 data.insert(
1429 data_path.clone(),
1430 crate::planning::semantics::DataDefinition::Value {
1431 value: crate::planning::semantics::LiteralValue::quantity_with_type(
1432 rational_zero(),
1433 "eur".to_string(),
1434 money.clone(),
1435 ),
1436 source,
1437 },
1438 );
1439
1440 let plan = ExecutionPlan {
1441 spec_name: "test".to_string(),
1442 data,
1443 rules: Vec::new(),
1444 reference_evaluation_order: Vec::new(),
1445 meta: HashMap::new(),
1446 unit_index: HashMap::new(),
1447 effective: EffectiveDate::Origin,
1448 sources: Vec::new(),
1449 };
1450
1451 let values = json_data(&[("price", "1.234 eur")]);
1452
1453 assert!(
1454 plan.set_data_values(values, &default_limits()).is_err(),
1455 "Quantity decimals=2 should reject 1.234 eur"
1456 );
1457 }
1458
1459 #[test]
1460 fn test_serialize_deserialize_execution_plan() {
1461 let data_path = DataPath {
1462 segments: vec![],
1463 data: "age".to_string(),
1464 };
1465 let mut data = IndexMap::new();
1466 data.insert(
1467 data_path.clone(),
1468 crate::planning::semantics::DataDefinition::Value {
1469 value: create_number_literal(0.into()),
1470 source: test_source(),
1471 },
1472 );
1473 let plan = ExecutionPlan {
1474 spec_name: "test".to_string(),
1475 data,
1476 rules: Vec::new(),
1477 reference_evaluation_order: Vec::new(),
1478 meta: HashMap::new(),
1479 unit_index: HashMap::new(),
1480 effective: EffectiveDate::Origin,
1481 sources: Vec::new(),
1482 };
1483
1484 let json = serde_json::to_string(&plan).expect("Should serialize");
1485 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1486
1487 assert_eq!(deserialized.spec_name, plan.spec_name);
1488 assert_eq!(deserialized.data.len(), plan.data.len());
1489 assert_eq!(deserialized.rules.len(), plan.rules.len());
1490 }
1491
1492 #[test]
1493 fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
1494 let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
1495 let imported_type = crate::planning::semantics::LemmaType::new(
1496 "salary".to_string(),
1497 TypeSpecification::quantity(),
1498 crate::planning::semantics::TypeExtends::Custom {
1499 parent: "money".to_string(),
1500 family: "money".to_string(),
1501 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
1502 spec: Arc::clone(&dep_spec),
1503 },
1504 },
1505 );
1506
1507 let salary_path = DataPath::new(vec![], "salary".to_string());
1508 let mut data = IndexMap::new();
1509 data.insert(
1510 salary_path,
1511 crate::planning::semantics::DataDefinition::TypeDeclaration {
1512 resolved_type: imported_type,
1513 declared_default: None,
1514 source: test_source(),
1515 },
1516 );
1517
1518 let plan = ExecutionPlan {
1519 spec_name: "test".to_string(),
1520 data,
1521 rules: Vec::new(),
1522 reference_evaluation_order: Vec::new(),
1523 meta: HashMap::new(),
1524 unit_index: HashMap::new(),
1525 effective: EffectiveDate::Origin,
1526 sources: Vec::new(),
1527 };
1528
1529 let json = serde_json::to_string(&plan).expect("Should serialize");
1530 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1531
1532 let recovered = deserialized
1533 .data
1534 .get(&DataPath::new(vec![], "salary".to_string()))
1535 .and_then(|d| d.schema_type())
1536 .expect("salary type should be present in plan.data");
1537 match &recovered.extends {
1538 crate::planning::semantics::TypeExtends::Custom {
1539 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
1540 ..
1541 } => {
1542 assert_eq!(spec.name, "examples");
1543 }
1544 other => panic!(
1545 "Expected imported defining_spec after round-trip, got {:?}",
1546 other
1547 ),
1548 }
1549 }
1550
1551 #[test]
1552 fn test_serialize_deserialize_plan_with_rules() {
1553 use crate::planning::semantics::ExpressionKind;
1554
1555 let age_path = DataPath::new(vec![], "age".to_string());
1556 let mut data = IndexMap::new();
1557 data.insert(
1558 age_path.clone(),
1559 crate::planning::semantics::DataDefinition::Value {
1560 value: create_number_literal(0.into()),
1561 source: test_source(),
1562 },
1563 );
1564 let mut plan = ExecutionPlan {
1565 spec_name: "test".to_string(),
1566 data,
1567 rules: Vec::new(),
1568 reference_evaluation_order: Vec::new(),
1569 meta: HashMap::new(),
1570 unit_index: HashMap::new(),
1571 effective: EffectiveDate::Origin,
1572 sources: Vec::new(),
1573 };
1574
1575 let rule = ExecutableRule {
1576 path: RulePath::new(vec![], "can_drive".to_string()),
1577 name: "can_drive".to_string(),
1578 branches: vec![{
1579 let result = create_literal_expr(create_boolean_literal(true));
1580 Branch {
1581 condition: Some(Expression::new(
1582 ExpressionKind::Comparison(
1583 Arc::new(create_data_path_expr(age_path.clone())),
1584 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1585 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1586 ),
1587 test_source(),
1588 )),
1589 normalized_condition: None,
1590 result: result.clone(),
1591 normalized_result: result,
1592 source: test_source(),
1593 }
1594 }],
1595 needs_data: BTreeSet::from([age_path]),
1596 source: test_source(),
1597 rule_type: primitive_boolean().clone(),
1598 };
1599
1600 plan.rules.push(rule);
1601
1602 let json = serde_json::to_string(&plan).expect("Should serialize");
1603 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1604
1605 assert_eq!(deserialized.spec_name, plan.spec_name);
1606 assert_eq!(deserialized.data.len(), plan.data.len());
1607 assert_eq!(deserialized.rules.len(), plan.rules.len());
1608 assert_eq!(deserialized.rules[0].name, "can_drive");
1609 assert_eq!(deserialized.rules[0].branches.len(), 1);
1610 assert_eq!(deserialized.rules[0].needs_data.len(), 1);
1611 }
1612
1613 #[test]
1614 fn test_serialize_deserialize_plan_with_nested_data_paths() {
1615 use crate::planning::semantics::PathSegment;
1616 let data_path = DataPath {
1617 segments: vec![PathSegment {
1618 data: "employee".to_string(),
1619 spec: "private".to_string(),
1620 }],
1621 data: "salary".to_string(),
1622 };
1623
1624 let mut data = IndexMap::new();
1625 data.insert(
1626 data_path.clone(),
1627 crate::planning::semantics::DataDefinition::Value {
1628 value: create_number_literal(0.into()),
1629 source: test_source(),
1630 },
1631 );
1632 let plan = ExecutionPlan {
1633 spec_name: "test".to_string(),
1634 data,
1635 rules: Vec::new(),
1636 reference_evaluation_order: Vec::new(),
1637 meta: HashMap::new(),
1638 unit_index: HashMap::new(),
1639 effective: EffectiveDate::Origin,
1640 sources: Vec::new(),
1641 };
1642
1643 let json = serde_json::to_string(&plan).expect("Should serialize");
1644 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1645
1646 assert_eq!(deserialized.data.len(), 1);
1647 let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
1648 assert_eq!(deserialized_path.segments.len(), 1);
1649 assert_eq!(deserialized_path.segments[0].data, "employee");
1650 assert_eq!(deserialized_path.data, "salary");
1651 }
1652
1653 #[test]
1654 fn test_serialize_deserialize_plan_with_multiple_data_types() {
1655 let name_path = DataPath::new(vec![], "name".to_string());
1656 let age_path = DataPath::new(vec![], "age".to_string());
1657 let active_path = DataPath::new(vec![], "active".to_string());
1658
1659 let mut data = IndexMap::new();
1660 data.insert(
1661 name_path.clone(),
1662 crate::planning::semantics::DataDefinition::Value {
1663 value: create_text_literal("Alice".to_string()),
1664 source: test_source(),
1665 },
1666 );
1667 data.insert(
1668 age_path.clone(),
1669 crate::planning::semantics::DataDefinition::Value {
1670 value: create_number_literal(30.into()),
1671 source: test_source(),
1672 },
1673 );
1674 data.insert(
1675 active_path.clone(),
1676 crate::planning::semantics::DataDefinition::Value {
1677 value: create_boolean_literal(true),
1678 source: test_source(),
1679 },
1680 );
1681
1682 let plan = ExecutionPlan {
1683 spec_name: "test".to_string(),
1684 data,
1685 rules: Vec::new(),
1686 reference_evaluation_order: Vec::new(),
1687 meta: HashMap::new(),
1688 unit_index: HashMap::new(),
1689 effective: EffectiveDate::Origin,
1690 sources: Vec::new(),
1691 };
1692
1693 let json = serde_json::to_string(&plan).expect("Should serialize");
1694 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1695
1696 assert_eq!(deserialized.data.len(), 3);
1697
1698 assert_eq!(
1699 deserialized.get_data_value(&name_path).unwrap().value,
1700 crate::planning::semantics::ValueKind::Text("Alice".to_string())
1701 );
1702 assert_eq!(
1703 deserialized.get_data_value(&age_path).unwrap().value,
1704 crate::planning::semantics::ValueKind::Number(30.into())
1705 );
1706 assert_eq!(
1707 deserialized.get_data_value(&active_path).unwrap().value,
1708 crate::planning::semantics::ValueKind::Boolean(true)
1709 );
1710 }
1711
1712 #[test]
1713 fn test_serialize_deserialize_plan_with_multiple_branches() {
1714 use crate::planning::semantics::ExpressionKind;
1715
1716 let points_path = DataPath::new(vec![], "points".to_string());
1717 let mut data = IndexMap::new();
1718 data.insert(
1719 points_path.clone(),
1720 crate::planning::semantics::DataDefinition::Value {
1721 value: create_number_literal(0.into()),
1722 source: test_source(),
1723 },
1724 );
1725 let mut plan = ExecutionPlan {
1726 spec_name: "test".to_string(),
1727 data,
1728 rules: Vec::new(),
1729 reference_evaluation_order: Vec::new(),
1730 meta: HashMap::new(),
1731 unit_index: HashMap::new(),
1732 effective: EffectiveDate::Origin,
1733 sources: Vec::new(),
1734 };
1735
1736 let rule = ExecutableRule {
1737 path: RulePath::new(vec![], "tier".to_string()),
1738 name: "tier".to_string(),
1739 branches: vec![
1740 {
1741 let result = create_literal_expr(create_text_literal("bronze".to_string()));
1742 Branch {
1743 condition: None,
1744 normalized_condition: None,
1745 result: result.clone(),
1746 normalized_result: result,
1747 source: test_source(),
1748 }
1749 },
1750 {
1751 let result = create_literal_expr(create_text_literal("silver".to_string()));
1752 Branch {
1753 condition: Some(Expression::new(
1754 ExpressionKind::Comparison(
1755 Arc::new(create_data_path_expr(points_path.clone())),
1756 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1757 Arc::new(create_literal_expr(create_number_literal(100.into()))),
1758 ),
1759 test_source(),
1760 )),
1761 normalized_condition: None,
1762 result: result.clone(),
1763 normalized_result: result,
1764 source: test_source(),
1765 }
1766 },
1767 {
1768 let result = create_literal_expr(create_text_literal("gold".to_string()));
1769 Branch {
1770 condition: Some(Expression::new(
1771 ExpressionKind::Comparison(
1772 Arc::new(create_data_path_expr(points_path.clone())),
1773 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1774 Arc::new(create_literal_expr(create_number_literal(500.into()))),
1775 ),
1776 test_source(),
1777 )),
1778 normalized_condition: None,
1779 result: result.clone(),
1780 normalized_result: result,
1781 source: test_source(),
1782 }
1783 },
1784 ],
1785 needs_data: BTreeSet::from([points_path]),
1786 source: test_source(),
1787 rule_type: primitive_text().clone(),
1788 };
1789
1790 plan.rules.push(rule);
1791
1792 let json = serde_json::to_string(&plan).expect("Should serialize");
1793 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1794
1795 assert_eq!(deserialized.rules.len(), 1);
1796 assert_eq!(deserialized.rules[0].branches.len(), 3);
1797 assert!(deserialized.rules[0].branches[0].condition.is_none());
1798 assert!(deserialized.rules[0].branches[1].condition.is_some());
1799 assert!(deserialized.rules[0].branches[2].condition.is_some());
1800 }
1801
1802 #[test]
1803 fn test_serialize_deserialize_empty_plan() {
1804 let plan = ExecutionPlan {
1805 spec_name: "empty".to_string(),
1806 data: IndexMap::new(),
1807 rules: Vec::new(),
1808 reference_evaluation_order: Vec::new(),
1809 meta: HashMap::new(),
1810 unit_index: HashMap::new(),
1811 effective: EffectiveDate::Origin,
1812 sources: Vec::new(),
1813 };
1814
1815 let json = serde_json::to_string(&plan).expect("Should serialize");
1816 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1817
1818 assert_eq!(deserialized.spec_name, "empty");
1819 assert_eq!(deserialized.data.len(), 0);
1820 assert_eq!(deserialized.rules.len(), 0);
1821 }
1822
1823 #[test]
1824 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
1825 use crate::planning::semantics::ExpressionKind;
1826
1827 let x_path = DataPath::new(vec![], "x".to_string());
1828 let mut data = IndexMap::new();
1829 data.insert(
1830 x_path.clone(),
1831 crate::planning::semantics::DataDefinition::Value {
1832 value: create_number_literal(0.into()),
1833 source: test_source(),
1834 },
1835 );
1836 let mut plan = ExecutionPlan {
1837 spec_name: "test".to_string(),
1838 data,
1839 rules: Vec::new(),
1840 reference_evaluation_order: Vec::new(),
1841 meta: HashMap::new(),
1842 unit_index: HashMap::new(),
1843 effective: EffectiveDate::Origin,
1844 sources: Vec::new(),
1845 };
1846
1847 let rule = ExecutableRule {
1848 path: RulePath::new(vec![], "doubled".to_string()),
1849 name: "doubled".to_string(),
1850 branches: vec![{
1851 let result = Expression::new(
1852 ExpressionKind::Arithmetic(
1853 Arc::new(create_data_path_expr(x_path.clone())),
1854 crate::parsing::ast::ArithmeticComputation::Multiply,
1855 Arc::new(create_literal_expr(create_number_literal(2.into()))),
1856 ),
1857 test_source(),
1858 );
1859 Branch {
1860 condition: None,
1861 normalized_condition: None,
1862 result: result.clone(),
1863 normalized_result: result,
1864 source: test_source(),
1865 }
1866 }],
1867 needs_data: BTreeSet::from([x_path]),
1868 source: test_source(),
1869 rule_type: crate::planning::semantics::primitive_number().clone(),
1870 };
1871
1872 plan.rules.push(rule);
1873
1874 let json = serde_json::to_string(&plan).expect("Should serialize");
1875 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1876
1877 assert_eq!(deserialized.rules.len(), 1);
1878 match &deserialized.rules[0].branches[0].result.kind {
1879 ExpressionKind::Arithmetic(left, op, right) => {
1880 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
1881 match &left.kind {
1882 ExpressionKind::DataPath(_) => {}
1883 _ => panic!("Expected DataPath in left operand"),
1884 }
1885 match &right.kind {
1886 ExpressionKind::Literal(_) => {}
1887 _ => panic!("Expected Literal in right operand"),
1888 }
1889 }
1890 _ => panic!("Expected Arithmetic expression"),
1891 }
1892 }
1893
1894 #[test]
1895 fn test_serialize_deserialize_round_trip_equality() {
1896 use crate::planning::semantics::ExpressionKind;
1897
1898 let age_path = DataPath::new(vec![], "age".to_string());
1899 let mut data = IndexMap::new();
1900 data.insert(
1901 age_path.clone(),
1902 crate::planning::semantics::DataDefinition::Value {
1903 value: create_number_literal(0.into()),
1904 source: test_source(),
1905 },
1906 );
1907 let mut plan = ExecutionPlan {
1908 spec_name: "test".to_string(),
1909 data,
1910 rules: Vec::new(),
1911 reference_evaluation_order: Vec::new(),
1912 meta: HashMap::new(),
1913 unit_index: HashMap::new(),
1914 effective: EffectiveDate::Origin,
1915 sources: Vec::new(),
1916 };
1917
1918 let rule = ExecutableRule {
1919 path: RulePath::new(vec![], "is_adult".to_string()),
1920 name: "is_adult".to_string(),
1921 branches: vec![{
1922 let result = create_literal_expr(create_boolean_literal(true));
1923 Branch {
1924 condition: Some(Expression::new(
1925 ExpressionKind::Comparison(
1926 Arc::new(create_data_path_expr(age_path.clone())),
1927 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1928 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1929 ),
1930 test_source(),
1931 )),
1932 normalized_condition: None,
1933 result: result.clone(),
1934 normalized_result: result,
1935 source: test_source(),
1936 }
1937 }],
1938 needs_data: BTreeSet::from([age_path]),
1939 source: test_source(),
1940 rule_type: primitive_boolean().clone(),
1941 };
1942
1943 plan.rules.push(rule);
1944
1945 let json = serde_json::to_string(&plan).expect("Should serialize");
1946 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1947
1948 let json2 = serde_json::to_string(&deserialized).expect("Should serialize again");
1949 let deserialized2: ExecutionPlan =
1950 serde_json::from_str(&json2).expect("Should deserialize again");
1951
1952 assert_eq!(deserialized2.spec_name, plan.spec_name);
1953 assert_eq!(deserialized2.data.len(), plan.data.len());
1954 assert_eq!(deserialized2.rules.len(), plan.rules.len());
1955 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
1956 assert_eq!(
1957 deserialized2.rules[0].branches.len(),
1958 plan.rules[0].branches.len()
1959 );
1960 }
1961
1962 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
1963 ExecutionPlan {
1964 spec_name: "s".into(),
1965 data: IndexMap::new(),
1966 rules: Vec::new(),
1967 reference_evaluation_order: Vec::new(),
1968 meta: HashMap::new(),
1969 unit_index: HashMap::new(),
1970 effective,
1971 sources: Vec::new(),
1972 }
1973 }
1974
1975 #[test]
1976 fn plan_at_exact_boundary_selects_later_slice() {
1977 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
1978
1979 let june = DateTimeValue {
1980 year: 2025,
1981 month: 6,
1982 day: 1,
1983 hour: 0,
1984 minute: 0,
1985 second: 0,
1986 microsecond: 0,
1987 timezone: None,
1988 };
1989 let dec = DateTimeValue {
1990 year: 2025,
1991 month: 12,
1992 day: 1,
1993 hour: 0,
1994 minute: 0,
1995 second: 0,
1996 microsecond: 0,
1997 timezone: None,
1998 };
1999
2000 let set = ExecutionPlanSet {
2001 spec_name: "s".into(),
2002 plans: vec![
2003 empty_plan(EffectiveDate::Origin),
2004 empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2005 empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2006 ],
2007 };
2008
2009 assert!(std::ptr::eq(
2010 set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2011 .expect("boundary instant"),
2012 &set.plans[1]
2013 ));
2014 assert!(std::ptr::eq(
2015 set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2016 .expect("dec boundary"),
2017 &set.plans[2]
2018 ));
2019 }
2020
2021 #[test]
2022 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2023 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2024
2025 let june = DateTimeValue {
2026 year: 2025,
2027 month: 6,
2028 day: 1,
2029 hour: 0,
2030 minute: 0,
2031 second: 0,
2032 microsecond: 0,
2033 timezone: None,
2034 };
2035 let may_end = DateTimeValue {
2036 year: 2025,
2037 month: 5,
2038 day: 31,
2039 hour: 23,
2040 minute: 59,
2041 second: 59,
2042 microsecond: 0,
2043 timezone: None,
2044 };
2045
2046 let set = ExecutionPlanSet {
2047 spec_name: "s".into(),
2048 plans: vec![
2049 empty_plan(EffectiveDate::Origin),
2050 empty_plan(EffectiveDate::DateTimeValue(june)),
2051 ],
2052 };
2053
2054 assert!(std::ptr::eq(
2055 set.plan_at(&EffectiveDate::DateTimeValue(may_end))
2056 .expect("may 31"),
2057 &set.plans[0]
2058 ));
2059 }
2060
2061 #[test]
2062 fn plan_at_single_plan_matches_any_instant_after_start() {
2063 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2064
2065 let t = DateTimeValue {
2066 year: 2025,
2067 month: 3,
2068 day: 1,
2069 hour: 0,
2070 minute: 0,
2071 second: 0,
2072 microsecond: 0,
2073 timezone: None,
2074 };
2075 let set = ExecutionPlanSet {
2076 spec_name: "s".into(),
2077 plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
2078 year: 2025,
2079 month: 1,
2080 day: 1,
2081 hour: 0,
2082 minute: 0,
2083 second: 0,
2084 microsecond: 0,
2085 timezone: None,
2086 }))],
2087 };
2088 assert!(std::ptr::eq(
2089 set.plan_at(&EffectiveDate::DateTimeValue(t))
2090 .expect("inside single slice"),
2091 &set.plans[0]
2092 ));
2093 }
2094
2095 #[test]
2098 fn schema_json_shape_contract() {
2099 let mut engine = Engine::new();
2100 engine
2101 .load(
2102 r#"
2103 spec pricing
2104 data bridge_height: quantity
2105 -> unit meter 1
2106 -> default 100 meter
2107 data quantity: number -> minimum 0
2108 rule cost: bridge_height * quantity
2109 "#,
2110 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2111 "test.lemma",
2112 ))),
2113 )
2114 .unwrap();
2115 let now = DateTimeValue::now();
2116 let schema = engine
2117 .get_plan(None, "pricing", Some(&now))
2118 .unwrap()
2119 .schema();
2120
2121 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2122
2123 let bh = &value["data"]["bridge_height"];
2124 assert!(
2125 bh.is_object(),
2126 "data entry must be a named object, not tuple"
2127 );
2128 assert!(
2129 bh.get("type").is_some(),
2130 "data entry must expose `type` field"
2131 );
2132 assert!(
2133 bh.get("default").is_some(),
2134 "bridge_height exposes `-> default` as schema default suggestion"
2135 );
2136 assert!(
2137 bh.get("bound_value").is_none(),
2138 "bridge_height is not a spec-bound literal"
2139 );
2140
2141 let ty = &bh["type"];
2142 assert_eq!(
2143 ty["kind"], "quantity",
2144 "kind tag sits on the type object itself"
2145 );
2146 assert!(
2147 ty["units"].is_array(),
2148 "quantity-only fields flatten up to top level"
2149 );
2150 assert!(
2151 ty.get("options").is_none(),
2152 "text-only fields must not leak"
2153 );
2154
2155 let qty = &value["data"]["quantity"];
2156 assert_eq!(qty["type"]["kind"], "number");
2157 assert!(
2158 qty.get("default").is_none(),
2159 "quantity has no default suggestion"
2160 );
2161 assert!(
2162 qty.get("bound_value").is_none(),
2163 "quantity has no bound literal"
2164 );
2165
2166 let cost = &value["rules"]["cost"];
2167 assert_eq!(
2168 cost["kind"], "quantity",
2169 "rule types use the same flat shape"
2170 );
2171 assert!(
2172 cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
2173 "quantity rule result types expose declared units"
2174 );
2175 assert!(
2176 cost["units"][0].get("factor").is_some(),
2177 "quantity rule units use factor field"
2178 );
2179 }
2180
2181 #[test]
2182 fn schema_rule_result_units_contract() {
2183 let mut engine = Engine::new();
2184 engine
2185 .load(
2186 r#"
2187 spec units_contract
2188 data money: quantity
2189 -> unit eur 1
2190 -> unit usd 0.91
2191 data rate: ratio
2192 -> unit basis_points 10000
2193 -> unit percent 100
2194 -> default 500 basis_points
2195 rule total: money
2196 rule rate_out: rate
2197 "#,
2198 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2199 "units_contract.lemma",
2200 ))),
2201 )
2202 .unwrap();
2203 let now = DateTimeValue::now();
2204 let schema = engine
2205 .get_plan(None, "units_contract", Some(&now))
2206 .unwrap()
2207 .schema();
2208 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2209
2210 let money_units = &value["data"]["money"]["type"]["units"];
2211 assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
2212 assert!(money_units[0].get("name").is_some());
2213 assert!(money_units[0].get("factor").is_some());
2214 assert!(money_units[0]["factor"].get("numer").is_some());
2215 assert!(money_units[0]["factor"].get("denom").is_some());
2216
2217 let rate_units = &value["data"]["rate"]["type"]["units"];
2218 assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
2219 assert!(rate_units[0].get("name").is_some());
2220 assert!(rate_units[0].get("value").is_some());
2221 assert!(rate_units[0]["value"].get("numer").is_some());
2222 assert!(rate_units[0]["value"].get("denom").is_some());
2223
2224 let total_rule_units = &value["rules"]["total"]["units"];
2225 let money_unit_names: Vec<_> = money_units
2226 .as_array()
2227 .unwrap()
2228 .iter()
2229 .map(|u| u["name"].as_str().unwrap())
2230 .collect();
2231 let total_rule_unit_names: Vec<_> = total_rule_units
2232 .as_array()
2233 .unwrap()
2234 .iter()
2235 .map(|u| u["name"].as_str().unwrap())
2236 .collect();
2237 assert_eq!(total_rule_unit_names, money_unit_names);
2238
2239 let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
2240 let rate_unit_names: Vec<_> = rate_units
2241 .as_array()
2242 .unwrap()
2243 .iter()
2244 .map(|u| u["name"].as_str().unwrap())
2245 .collect();
2246 let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
2247 .as_array()
2248 .unwrap()
2249 .iter()
2250 .map(|u| u["name"].as_str().unwrap())
2251 .collect();
2252 assert_eq!(rate_out_rule_unit_names, rate_unit_names);
2253 }
2254
2255 #[test]
2256 fn schema_json_round_trip_preserves_shape() {
2257 let mut engine = Engine::new();
2258 engine
2259 .load(
2260 r#"
2261 spec s
2262 data age: number -> minimum 0 -> default 18
2263 data grade: text -> options "A" "B" "C"
2264 rule adult: age >= 18
2265 "#,
2266 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
2267 )
2268 .unwrap();
2269 let now = DateTimeValue::now();
2270 let schema = engine.get_plan(None, "s", Some(&now)).unwrap().schema();
2271
2272 let json = serde_json::to_string(&schema).unwrap();
2273 let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
2274 assert_eq!(schema, round_tripped);
2275 }
2276}
2277
2278