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 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
631 self.data
632 .keys()
633 .find(|path| path.input_key() == canonical_name)
634 }
635
636 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
638 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
639 self.rules
640 .iter()
641 .find(|r| r.name == canonical_name && r.path.segments.is_empty())
642 }
643
644 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
646 self.rules.iter().find(|r| &r.path == rule_path)
647 }
648
649 pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
651 self.data.get(path).and_then(|d| d.value())
652 }
653
654 pub fn set_data_values(
658 mut self,
659 values: std::collections::HashMap<String, serde_json::Value>,
660 limits: &ResourceLimits,
661 ) -> Result<Self, Error> {
662 for (name, raw_value) in values {
663 let data_path = self.get_data_path_by_str(&name).ok_or_else(|| {
664 let available: Vec<String> = self.data.keys().map(|p| p.input_key()).collect();
665 Error::request(
666 format!(
667 "Data '{}' not found. Available data: {}",
668 name,
669 available.join(", ")
670 ),
671 None::<String>,
672 )
673 })?;
674 let data_path = data_path.clone();
675
676 let data_definition = self
677 .data
678 .get(&data_path)
679 .expect("BUG: data_path was just resolved from self.data, must exist");
680
681 let data_source = data_definition.source().clone();
682 let expected_type = data_definition.schema_type().cloned().ok_or_else(|| {
683 Error::request(
684 format!(
685 "Data '{}' is a spec reference; cannot provide a value.",
686 name
687 ),
688 None::<String>,
689 )
690 })?;
691
692 let literal_value = crate::planning::semantics::parse_data_value_from_json(
693 &raw_value,
694 &expected_type.specifications,
695 &expected_type,
696 &data_source,
697 )
698 .map_err(|e| e.with_related_data(&name))?;
699
700 let size = literal_value.byte_size();
701 if size > limits.max_data_value_bytes {
702 return Err(Error::resource_limit_exceeded(
703 "max_data_value_bytes",
704 limits.max_data_value_bytes.to_string(),
705 size.to_string(),
706 format!(
707 "Reduce the size of data values to {} bytes or less",
708 limits.max_data_value_bytes
709 ),
710 Some(data_source.clone()),
711 None,
712 None,
713 )
714 .with_related_data(&name));
715 }
716
717 validate_value_against_type(&expected_type, &literal_value).map_err(|msg| {
718 Error::validation(msg, Some(data_source.clone()), None::<String>)
719 .with_related_data(&name)
720 })?;
721
722 self.data.insert(
723 data_path,
724 DataDefinition::Value {
725 value: literal_value,
726 source: data_source,
727 },
728 );
729 }
730
731 Ok(self)
732 }
733
734 #[must_use]
738 pub fn with_defaults(mut self) -> Self {
739 let promotions: Vec<(DataPath, DataDefinition)> = self
740 .data
741 .iter()
742 .filter_map(|(path, def)| {
743 if let DataDefinition::TypeDeclaration {
744 declared_default: Some(dv),
745 resolved_type,
746 source,
747 } = def
748 {
749 Some((
750 path.clone(),
751 DataDefinition::Value {
752 value: LiteralValue {
753 value: dv.clone(),
754 lemma_type: resolved_type.clone(),
755 },
756 source: source.clone(),
757 },
758 ))
759 } else {
760 None
761 }
762 })
763 .collect();
764
765 for (path, def) in promotions {
766 self.data.insert(path, def);
767 }
768 self
769 }
770}
771
772pub(crate) fn validate_value_against_type(
773 expected_type: &LemmaType,
774 value: &LiteralValue,
775) -> Result<(), String> {
776 use crate::computation::rational::{commit_rational_to_decimal, RationalInteger};
777 use crate::planning::semantics::TypeSpecification;
778
779 fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
780 match commit_rational_to_decimal(magnitude) {
781 Ok(decimal) => decimal.scale() > u32::from(max_decimals),
782 Err(_) => true,
783 }
784 }
785
786 fn format_rational(r: &RationalInteger, decimals: Option<u8>) -> String {
787 use crate::computation::rational::rational_to_display_str;
788 match commit_rational_to_decimal(r) {
789 Ok(decimal) => match decimals {
790 Some(dp) => {
791 let rounded = decimal.round_dp(u32::from(dp));
792 format!("{:.prec$}", rounded, prec = dp as usize)
793 }
794 None => decimal.normalize().to_string(),
795 },
796 Err(_) => rational_to_display_str(r),
797 }
798 }
799
800 match (&expected_type.specifications, &value.value) {
801 (
802 TypeSpecification::Number {
803 minimum,
804 maximum,
805 decimals,
806 ..
807 },
808 ValueKind::Number(n),
809 ) => {
810 if let Some(d) = decimals {
811 if exceeds_decimal_places(n, *d) {
812 return Err(format!(
813 "{} exceeds decimals constraint {d}",
814 format_rational(n, *decimals)
815 ));
816 }
817 }
818 if let Some(min) = minimum {
819 if n < min {
820 return Err(format!(
821 "{} is below minimum {}",
822 format_rational(n, *decimals),
823 format_rational(min, *decimals)
824 ));
825 }
826 }
827 if let Some(max) = maximum {
828 if n > max {
829 return Err(format!(
830 "{} is above maximum {}",
831 format_rational(n, *decimals),
832 format_rational(max, *decimals)
833 ));
834 }
835 }
836 Ok(())
837 }
838 (
839 TypeSpecification::Quantity {
840 minimum,
841 maximum,
842 decimals,
843 units,
844 ..
845 },
846 ValueKind::Quantity(magnitude, unit, _),
847 ) => {
848 if let Some(d) = decimals {
849 if exceeds_decimal_places(magnitude, *d) {
850 return Err(format!(
851 "{} {unit} exceeds decimals constraint {d}",
852 format_rational(magnitude, *decimals)
853 ));
854 }
855 }
856 let quantity_unit = units.get(unit)?;
857 if minimum.is_some() {
858 let unit_minimum = quantity_unit.minimum.expect(
859 "BUG: QuantityUnit.minimum missing after type minimum set by sync_quantity_units_from_canonical",
860 );
861 if magnitude < &unit_minimum {
862 let value_display =
863 format!("{} {}", format_rational(magnitude, *decimals), unit);
864 let bound_display = format!(
865 "{} {}",
866 format_rational(&unit_minimum, *decimals),
867 quantity_unit.name
868 );
869 return Err(format!("{value_display} is below minimum {bound_display}"));
870 }
871 }
872 if maximum.is_some() {
873 let unit_maximum = quantity_unit.maximum.expect(
874 "BUG: QuantityUnit.maximum missing after type maximum set by sync_quantity_units_from_canonical",
875 );
876 if magnitude > &unit_maximum {
877 let value_display =
878 format!("{} {}", format_rational(magnitude, *decimals), unit);
879 let bound_display = format!(
880 "{} {}",
881 format_rational(&unit_maximum, *decimals),
882 quantity_unit.name
883 );
884 return Err(format!("{value_display} is above maximum {bound_display}"));
885 }
886 }
887 Ok(())
888 }
889 (
890 TypeSpecification::Text {
891 length, options, ..
892 },
893 ValueKind::Text(s),
894 ) => {
895 let len = s.chars().count();
896 if let Some(exact) = length {
897 if len != *exact {
898 return Err(format!(
899 "'{}' has length {} but required length is {}",
900 s, len, exact
901 ));
902 }
903 }
904 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
905 return Err(format!(
906 "'{}' is not in allowed options: {}",
907 s,
908 options.join(", ")
909 ));
910 }
911 Ok(())
912 }
913 (
914 TypeSpecification::Ratio {
915 minimum,
916 maximum,
917 decimals,
918 units,
919 ..
920 },
921 ValueKind::Ratio(r, unit_name),
922 ) => {
923 use crate::computation::rational::checked_mul;
924
925 if let Some(d) = decimals {
926 if exceeds_decimal_places(r, *d) {
927 return Err(format!(
928 "{} exceeds decimals constraint {d}",
929 format_rational(r, *decimals)
930 ));
931 }
932 }
933 if let Some(type_minimum) = minimum {
934 if r < type_minimum {
935 let message = match unit_name.as_deref() {
936 Some(unit) => {
937 let ratio_unit = units.get(unit)?;
938 let value_per_unit = checked_mul(r, &ratio_unit.value)
939 .map_err(|failure| failure.to_string())?;
940 let bound_per_unit = ratio_unit.minimum.expect(
941 "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
942 );
943 format!(
944 "{} {unit} is below minimum {} {unit}",
945 format_rational(&value_per_unit, *decimals),
946 format_rational(&bound_per_unit, *decimals),
947 )
948 }
949 None => format!(
950 "{} is below minimum {}",
951 format_rational(r, *decimals),
952 format_rational(type_minimum, *decimals),
953 ),
954 };
955 return Err(message);
956 }
957 }
958 if let Some(type_maximum) = maximum {
959 if r > type_maximum {
960 let message = match unit_name.as_deref() {
961 Some(unit) => {
962 let ratio_unit = units.get(unit)?;
963 let value_per_unit = checked_mul(r, &ratio_unit.value)
964 .map_err(|failure| failure.to_string())?;
965 let bound_per_unit = ratio_unit.maximum.expect(
966 "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
967 );
968 format!(
969 "{} {unit} is above maximum {} {unit}",
970 format_rational(&value_per_unit, *decimals),
971 format_rational(&bound_per_unit, *decimals),
972 )
973 }
974 None => format!(
975 "{} is above maximum {}",
976 format_rational(r, *decimals),
977 format_rational(type_maximum, *decimals),
978 ),
979 };
980 return Err(message);
981 }
982 }
983 Ok(())
984 }
985 (
986 TypeSpecification::Date {
987 minimum, maximum, ..
988 },
989 ValueKind::Date(dt),
990 ) => {
991 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
992 use std::cmp::Ordering;
993 if let Some(min) = minimum {
994 let min_sem = date_time_to_semantic(min);
995 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
996 return Err(format!("{} is below minimum {}", dt, min));
997 }
998 }
999 if let Some(max) = maximum {
1000 let max_sem = date_time_to_semantic(max);
1001 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1002 return Err(format!("{} is above maximum {}", dt, max));
1003 }
1004 }
1005 Ok(())
1006 }
1007 (
1008 TypeSpecification::Calendar {
1009 minimum, maximum, ..
1010 },
1011 ValueKind::Calendar(value, unit),
1012 ) => {
1013 let value_months = crate::computation::units::convert_calendar_magnitude(
1014 *value,
1015 unit,
1016 &SemanticCalendarUnit::Month,
1017 );
1018 if let Some((min_val, min_unit)) = minimum {
1019 let min_months = crate::computation::units::convert_calendar_magnitude(
1020 *min_val,
1021 min_unit,
1022 &SemanticCalendarUnit::Month,
1023 );
1024 if value_months < min_months {
1025 return Err(format!(
1026 "{value} {unit} is below minimum {min_val} {min_unit}"
1027 ));
1028 }
1029 }
1030 if let Some((max_val, max_unit)) = maximum {
1031 let max_months = crate::computation::units::convert_calendar_magnitude(
1032 *max_val,
1033 max_unit,
1034 &SemanticCalendarUnit::Month,
1035 );
1036 if value_months > max_months {
1037 return Err(format!(
1038 "{value} {unit} is above maximum {max_val} {max_unit}"
1039 ));
1040 }
1041 }
1042 Ok(())
1043 }
1044 (
1045 TypeSpecification::Time {
1046 minimum, maximum, ..
1047 },
1048 ValueKind::Time(t),
1049 ) => {
1050 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1051 use std::cmp::Ordering;
1052 if let Some(min) = minimum {
1053 let min_sem = time_to_semantic(min);
1054 if compare_semantic_times(t, &min_sem) == Ordering::Less {
1055 return Err(format!("{} is below minimum {}", t, min));
1056 }
1057 }
1058 if let Some(max) = maximum {
1059 let max_sem = time_to_semantic(max);
1060 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1061 return Err(format!("{} is above maximum {}", t, max));
1062 }
1063 }
1064 Ok(())
1065 }
1066 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1067 | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1068 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1069 | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1070 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1071 | (TypeSpecification::CalendarRange { .. }, ValueKind::Range(_, _))
1072 | (TypeSpecification::Veto { .. }, _)
1073 | (TypeSpecification::Undetermined, _) => Ok(()),
1074 (spec, value_kind) => unreachable!(
1075 "BUG: validate_value_against_type called with mismatched type/value: \
1076 spec={:?}, value={:?} — typing must be enforced before validation",
1077 spec, value_kind
1078 ),
1079 }
1080}
1081
1082pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1083 let mut errors = Vec::new();
1084
1085 for (data_path, data_definition) in &plan.data {
1086 let (expected_type, lit) = match data_definition {
1087 DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1088 DataDefinition::TypeDeclaration { .. }
1089 | DataDefinition::Import { .. }
1090 | DataDefinition::Reference { .. } => continue,
1091 };
1092
1093 if let Err(msg) = validate_value_against_type(expected_type, lit) {
1094 let source = data_definition.source().clone();
1095 errors.push(Error::validation(
1096 format!(
1097 "Invalid value for data {} (expected {}): {}",
1098 data_path,
1099 expected_type.name(),
1100 msg
1101 ),
1102 Some(source),
1103 None::<String>,
1104 ));
1105 }
1106 }
1107
1108 errors
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use super::*;
1114 use crate::computation::rational::{rational_zero, RationalInteger};
1115 use crate::parsing::ast::DateTimeValue;
1116 use crate::planning::semantics::{
1117 primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
1118 };
1119 use crate::Engine;
1120 use serde_json;
1121 use std::str::FromStr;
1122 use std::sync::Arc;
1123
1124 fn default_limits() -> ResourceLimits {
1125 ResourceLimits::default()
1126 }
1127
1128 fn json_data(pairs: &[(&str, &str)]) -> HashMap<String, serde_json::Value> {
1129 pairs
1130 .iter()
1131 .map(|(k, v)| (k.to_string(), serde_json::Value::String((*v).to_string())))
1132 .collect()
1133 }
1134
1135 #[test]
1136 fn test_with_raw_values() {
1137 let mut engine = Engine::new();
1138 engine
1139 .load(
1140 r#"
1141 spec test
1142 data age: number -> default 25
1143 "#,
1144 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1145 "test.lemma",
1146 ))),
1147 )
1148 .unwrap();
1149
1150 let now = DateTimeValue::now();
1151 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1152 let data_path = DataPath::new(vec![], "age".to_string());
1153
1154 let values = json_data(&[("age", "30")]);
1155
1156 let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1157 let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1158 match &updated_value.value {
1159 crate::planning::semantics::ValueKind::Number(n) => {
1160 assert_eq!(*n, RationalInteger::new(30, 1));
1161 }
1162 other => panic!("Expected number literal, got {:?}", other),
1163 }
1164 }
1165
1166 #[test]
1167 fn test_with_raw_values_type_mismatch() {
1168 let mut engine = Engine::new();
1169 engine
1170 .load(
1171 r#"
1172 spec test
1173 data age: number
1174 "#,
1175 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1176 "test.lemma",
1177 ))),
1178 )
1179 .unwrap();
1180
1181 let now = DateTimeValue::now();
1182 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1183
1184 let values = json_data(&[("age", "thirty")]);
1185
1186 assert!(plan.set_data_values(values, &default_limits()).is_err());
1187 }
1188
1189 #[test]
1190 fn test_with_raw_values_unknown_data() {
1191 let mut engine = Engine::new();
1192 engine
1193 .load(
1194 r#"
1195 spec test
1196 data known: number
1197 "#,
1198 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1199 "test.lemma",
1200 ))),
1201 )
1202 .unwrap();
1203
1204 let now = DateTimeValue::now();
1205 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1206
1207 let values = json_data(&[("unknown", "30")]);
1208
1209 assert!(plan.set_data_values(values, &default_limits()).is_err());
1210 }
1211
1212 #[test]
1213 fn test_with_raw_values_nested() {
1214 let mut engine = Engine::new();
1215 engine
1216 .load(
1217 r#"
1218 spec private
1219 data base_price: number
1220
1221 spec test
1222 uses rules: private
1223 "#,
1224 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1225 "test.lemma",
1226 ))),
1227 )
1228 .unwrap();
1229
1230 let now = DateTimeValue::now();
1231 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1232
1233 let values = json_data(&[("rules.base_price", "100")]);
1234
1235 let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1236 let data_path = DataPath {
1237 segments: vec![PathSegment {
1238 data: "rules".to_string(),
1239 spec: "private".to_string(),
1240 }],
1241 data: "base_price".to_string(),
1242 };
1243 let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1244 match &updated_value.value {
1245 crate::planning::semantics::ValueKind::Number(n) => {
1246 assert_eq!(*n, RationalInteger::new(100, 1));
1247 }
1248 other => panic!("Expected number literal, got {:?}", other),
1249 }
1250 }
1251
1252 fn test_source() -> Source {
1253 use crate::parsing::ast::Span;
1254 Source::new(
1255 crate::parsing::source::SourceType::Volatile,
1256 Span {
1257 start: 0,
1258 end: 0,
1259 line: 1,
1260 col: 0,
1261 },
1262 )
1263 }
1264
1265 fn create_literal_expr(value: LiteralValue) -> Expression {
1266 Expression::new(
1267 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
1268 test_source(),
1269 )
1270 }
1271
1272 fn create_data_path_expr(path: DataPath) -> Expression {
1273 Expression::new(
1274 crate::planning::semantics::ExpressionKind::DataPath(path),
1275 test_source(),
1276 )
1277 }
1278
1279 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
1280 LiteralValue::number_from_decimal(n)
1281 }
1282
1283 fn create_boolean_literal(b: bool) -> LiteralValue {
1284 LiteralValue::from_bool(b)
1285 }
1286
1287 fn create_text_literal(s: String) -> LiteralValue {
1288 LiteralValue::text(s)
1289 }
1290
1291 #[test]
1292 fn with_values_should_enforce_number_maximum_constraint() {
1293 let data_path = DataPath::new(vec![], "x".to_string());
1296
1297 let max10 = crate::planning::semantics::LemmaType::primitive(
1298 crate::planning::semantics::TypeSpecification::Number {
1299 minimum: None,
1300 maximum: Some(RationalInteger::new(10, 1)),
1301 decimals: None,
1302 help: String::new(),
1303 },
1304 );
1305 let source = Source::new(
1306 crate::parsing::source::SourceType::Volatile,
1307 crate::parsing::ast::Span {
1308 start: 0,
1309 end: 0,
1310 line: 1,
1311 col: 0,
1312 },
1313 );
1314 let mut data = IndexMap::new();
1315 data.insert(
1316 data_path.clone(),
1317 crate::planning::semantics::DataDefinition::Value {
1318 value: crate::planning::semantics::LiteralValue::number_with_type(
1319 0.into(),
1320 max10.clone(),
1321 ),
1322 source: source.clone(),
1323 },
1324 );
1325
1326 let plan = ExecutionPlan {
1327 spec_name: "test".to_string(),
1328 data,
1329 rules: Vec::new(),
1330 reference_evaluation_order: Vec::new(),
1331 meta: HashMap::new(),
1332 unit_index: HashMap::new(),
1333 effective: EffectiveDate::Origin,
1334 sources: Vec::new(),
1335 };
1336
1337 let values = json_data(&[("x", "11")]);
1338
1339 assert!(
1340 plan.set_data_values(values, &default_limits()).is_err(),
1341 "Providing x=11 should fail due to maximum 10"
1342 );
1343 }
1344
1345 #[test]
1346 fn with_values_should_enforce_text_enum_options() {
1347 let data_path = DataPath::new(vec![], "tier".to_string());
1349
1350 let tier = crate::planning::semantics::LemmaType::primitive(
1351 crate::planning::semantics::TypeSpecification::Text {
1352 length: None,
1353 options: vec!["silver".to_string(), "gold".to_string()],
1354 help: String::new(),
1355 },
1356 );
1357 let source = Source::new(
1358 crate::parsing::source::SourceType::Volatile,
1359 crate::parsing::ast::Span {
1360 start: 0,
1361 end: 0,
1362 line: 1,
1363 col: 0,
1364 },
1365 );
1366 let mut data = IndexMap::new();
1367 data.insert(
1368 data_path.clone(),
1369 crate::planning::semantics::DataDefinition::Value {
1370 value: crate::planning::semantics::LiteralValue::text_with_type(
1371 "silver".to_string(),
1372 tier.clone(),
1373 ),
1374 source,
1375 },
1376 );
1377
1378 let plan = ExecutionPlan {
1379 spec_name: "test".to_string(),
1380 data,
1381 rules: Vec::new(),
1382 reference_evaluation_order: Vec::new(),
1383 meta: HashMap::new(),
1384 unit_index: HashMap::new(),
1385 effective: EffectiveDate::Origin,
1386 sources: Vec::new(),
1387 };
1388
1389 let values = json_data(&[("tier", "platinum")]);
1390
1391 assert!(
1392 plan.set_data_values(values, &default_limits()).is_err(),
1393 "Invalid enum value should be rejected (tier='platinum')"
1394 );
1395 }
1396
1397 #[test]
1398 fn with_values_should_enforce_quantity_decimals() {
1399 let data_path = DataPath::new(vec![], "price".to_string());
1402
1403 let money = crate::planning::semantics::LemmaType::primitive(
1404 crate::planning::semantics::TypeSpecification::Quantity {
1405 minimum: None,
1406 maximum: None,
1407 decimals: Some(2),
1408 units: crate::planning::semantics::QuantityUnits::from(vec![
1409 crate::planning::semantics::QuantityUnit::from_decimal_factor(
1410 "eur".to_string(),
1411 rust_decimal::Decimal::from_str("1.0").unwrap(),
1412 Vec::new(),
1413 )
1414 .expect("eur unit factor must be exact decimal"),
1415 ]),
1416 traits: Vec::new(),
1417 decomposition: crate::literals::BaseQuantityVector::new(),
1418 canonical_unit: "eur".to_string(),
1419 help: String::new(),
1420 },
1421 );
1422 let source = Source::new(
1423 crate::parsing::source::SourceType::Volatile,
1424 crate::parsing::ast::Span {
1425 start: 0,
1426 end: 0,
1427 line: 1,
1428 col: 0,
1429 },
1430 );
1431 let mut data = IndexMap::new();
1432 data.insert(
1433 data_path.clone(),
1434 crate::planning::semantics::DataDefinition::Value {
1435 value: crate::planning::semantics::LiteralValue::quantity_with_type(
1436 rational_zero(),
1437 "eur".to_string(),
1438 money.clone(),
1439 ),
1440 source,
1441 },
1442 );
1443
1444 let plan = ExecutionPlan {
1445 spec_name: "test".to_string(),
1446 data,
1447 rules: Vec::new(),
1448 reference_evaluation_order: Vec::new(),
1449 meta: HashMap::new(),
1450 unit_index: HashMap::new(),
1451 effective: EffectiveDate::Origin,
1452 sources: Vec::new(),
1453 };
1454
1455 let values = json_data(&[("price", "1.234 eur")]);
1456
1457 assert!(
1458 plan.set_data_values(values, &default_limits()).is_err(),
1459 "Quantity decimals=2 should reject 1.234 eur"
1460 );
1461 }
1462
1463 #[test]
1464 fn test_serialize_deserialize_execution_plan() {
1465 let data_path = DataPath {
1466 segments: vec![],
1467 data: "age".to_string(),
1468 };
1469 let mut data = IndexMap::new();
1470 data.insert(
1471 data_path.clone(),
1472 crate::planning::semantics::DataDefinition::Value {
1473 value: create_number_literal(0.into()),
1474 source: test_source(),
1475 },
1476 );
1477 let plan = ExecutionPlan {
1478 spec_name: "test".to_string(),
1479 data,
1480 rules: Vec::new(),
1481 reference_evaluation_order: Vec::new(),
1482 meta: HashMap::new(),
1483 unit_index: HashMap::new(),
1484 effective: EffectiveDate::Origin,
1485 sources: Vec::new(),
1486 };
1487
1488 let json = serde_json::to_string(&plan).expect("Should serialize");
1489 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1490
1491 assert_eq!(deserialized.spec_name, plan.spec_name);
1492 assert_eq!(deserialized.data.len(), plan.data.len());
1493 assert_eq!(deserialized.rules.len(), plan.rules.len());
1494 }
1495
1496 #[test]
1497 fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
1498 let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
1499 let imported_type = crate::planning::semantics::LemmaType::new(
1500 "salary".to_string(),
1501 TypeSpecification::quantity(),
1502 crate::planning::semantics::TypeExtends::Custom {
1503 parent: "money".to_string(),
1504 family: "money".to_string(),
1505 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
1506 spec: Arc::clone(&dep_spec),
1507 },
1508 },
1509 );
1510
1511 let salary_path = DataPath::new(vec![], "salary".to_string());
1512 let mut data = IndexMap::new();
1513 data.insert(
1514 salary_path,
1515 crate::planning::semantics::DataDefinition::TypeDeclaration {
1516 resolved_type: imported_type,
1517 declared_default: None,
1518 source: test_source(),
1519 },
1520 );
1521
1522 let plan = ExecutionPlan {
1523 spec_name: "test".to_string(),
1524 data,
1525 rules: Vec::new(),
1526 reference_evaluation_order: Vec::new(),
1527 meta: HashMap::new(),
1528 unit_index: HashMap::new(),
1529 effective: EffectiveDate::Origin,
1530 sources: Vec::new(),
1531 };
1532
1533 let json = serde_json::to_string(&plan).expect("Should serialize");
1534 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1535
1536 let recovered = deserialized
1537 .data
1538 .get(&DataPath::new(vec![], "salary".to_string()))
1539 .and_then(|d| d.schema_type())
1540 .expect("salary type should be present in plan.data");
1541 match &recovered.extends {
1542 crate::planning::semantics::TypeExtends::Custom {
1543 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
1544 ..
1545 } => {
1546 assert_eq!(spec.name, "examples");
1547 }
1548 other => panic!(
1549 "Expected imported defining_spec after round-trip, got {:?}",
1550 other
1551 ),
1552 }
1553 }
1554
1555 #[test]
1556 fn test_serialize_deserialize_plan_with_rules() {
1557 use crate::planning::semantics::ExpressionKind;
1558
1559 let age_path = DataPath::new(vec![], "age".to_string());
1560 let mut data = IndexMap::new();
1561 data.insert(
1562 age_path.clone(),
1563 crate::planning::semantics::DataDefinition::Value {
1564 value: create_number_literal(0.into()),
1565 source: test_source(),
1566 },
1567 );
1568 let mut plan = ExecutionPlan {
1569 spec_name: "test".to_string(),
1570 data,
1571 rules: Vec::new(),
1572 reference_evaluation_order: Vec::new(),
1573 meta: HashMap::new(),
1574 unit_index: HashMap::new(),
1575 effective: EffectiveDate::Origin,
1576 sources: Vec::new(),
1577 };
1578
1579 let rule = ExecutableRule {
1580 path: RulePath::new(vec![], "can_drive".to_string()),
1581 name: "can_drive".to_string(),
1582 branches: vec![{
1583 let result = create_literal_expr(create_boolean_literal(true));
1584 Branch {
1585 condition: Some(Expression::new(
1586 ExpressionKind::Comparison(
1587 Arc::new(create_data_path_expr(age_path.clone())),
1588 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1589 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1590 ),
1591 test_source(),
1592 )),
1593 normalized_condition: None,
1594 result: result.clone(),
1595 normalized_result: result,
1596 source: test_source(),
1597 }
1598 }],
1599 needs_data: BTreeSet::from([age_path]),
1600 source: test_source(),
1601 rule_type: primitive_boolean().clone(),
1602 };
1603
1604 plan.rules.push(rule);
1605
1606 let json = serde_json::to_string(&plan).expect("Should serialize");
1607 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1608
1609 assert_eq!(deserialized.spec_name, plan.spec_name);
1610 assert_eq!(deserialized.data.len(), plan.data.len());
1611 assert_eq!(deserialized.rules.len(), plan.rules.len());
1612 assert_eq!(deserialized.rules[0].name, "can_drive");
1613 assert_eq!(deserialized.rules[0].branches.len(), 1);
1614 assert_eq!(deserialized.rules[0].needs_data.len(), 1);
1615 }
1616
1617 #[test]
1618 fn test_serialize_deserialize_plan_with_nested_data_paths() {
1619 use crate::planning::semantics::PathSegment;
1620 let data_path = DataPath {
1621 segments: vec![PathSegment {
1622 data: "employee".to_string(),
1623 spec: "private".to_string(),
1624 }],
1625 data: "salary".to_string(),
1626 };
1627
1628 let mut data = IndexMap::new();
1629 data.insert(
1630 data_path.clone(),
1631 crate::planning::semantics::DataDefinition::Value {
1632 value: create_number_literal(0.into()),
1633 source: test_source(),
1634 },
1635 );
1636 let plan = ExecutionPlan {
1637 spec_name: "test".to_string(),
1638 data,
1639 rules: Vec::new(),
1640 reference_evaluation_order: Vec::new(),
1641 meta: HashMap::new(),
1642 unit_index: HashMap::new(),
1643 effective: EffectiveDate::Origin,
1644 sources: Vec::new(),
1645 };
1646
1647 let json = serde_json::to_string(&plan).expect("Should serialize");
1648 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1649
1650 assert_eq!(deserialized.data.len(), 1);
1651 let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
1652 assert_eq!(deserialized_path.segments.len(), 1);
1653 assert_eq!(deserialized_path.segments[0].data, "employee");
1654 assert_eq!(deserialized_path.data, "salary");
1655 }
1656
1657 #[test]
1658 fn test_serialize_deserialize_plan_with_multiple_data_types() {
1659 let name_path = DataPath::new(vec![], "name".to_string());
1660 let age_path = DataPath::new(vec![], "age".to_string());
1661 let active_path = DataPath::new(vec![], "active".to_string());
1662
1663 let mut data = IndexMap::new();
1664 data.insert(
1665 name_path.clone(),
1666 crate::planning::semantics::DataDefinition::Value {
1667 value: create_text_literal("Alice".to_string()),
1668 source: test_source(),
1669 },
1670 );
1671 data.insert(
1672 age_path.clone(),
1673 crate::planning::semantics::DataDefinition::Value {
1674 value: create_number_literal(30.into()),
1675 source: test_source(),
1676 },
1677 );
1678 data.insert(
1679 active_path.clone(),
1680 crate::planning::semantics::DataDefinition::Value {
1681 value: create_boolean_literal(true),
1682 source: test_source(),
1683 },
1684 );
1685
1686 let plan = ExecutionPlan {
1687 spec_name: "test".to_string(),
1688 data,
1689 rules: Vec::new(),
1690 reference_evaluation_order: Vec::new(),
1691 meta: HashMap::new(),
1692 unit_index: HashMap::new(),
1693 effective: EffectiveDate::Origin,
1694 sources: Vec::new(),
1695 };
1696
1697 let json = serde_json::to_string(&plan).expect("Should serialize");
1698 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1699
1700 assert_eq!(deserialized.data.len(), 3);
1701
1702 assert_eq!(
1703 deserialized.get_data_value(&name_path).unwrap().value,
1704 crate::planning::semantics::ValueKind::Text("Alice".to_string())
1705 );
1706 assert_eq!(
1707 deserialized.get_data_value(&age_path).unwrap().value,
1708 crate::planning::semantics::ValueKind::Number(30.into())
1709 );
1710 assert_eq!(
1711 deserialized.get_data_value(&active_path).unwrap().value,
1712 crate::planning::semantics::ValueKind::Boolean(true)
1713 );
1714 }
1715
1716 #[test]
1717 fn test_serialize_deserialize_plan_with_multiple_branches() {
1718 use crate::planning::semantics::ExpressionKind;
1719
1720 let points_path = DataPath::new(vec![], "points".to_string());
1721 let mut data = IndexMap::new();
1722 data.insert(
1723 points_path.clone(),
1724 crate::planning::semantics::DataDefinition::Value {
1725 value: create_number_literal(0.into()),
1726 source: test_source(),
1727 },
1728 );
1729 let mut plan = ExecutionPlan {
1730 spec_name: "test".to_string(),
1731 data,
1732 rules: Vec::new(),
1733 reference_evaluation_order: Vec::new(),
1734 meta: HashMap::new(),
1735 unit_index: HashMap::new(),
1736 effective: EffectiveDate::Origin,
1737 sources: Vec::new(),
1738 };
1739
1740 let rule = ExecutableRule {
1741 path: RulePath::new(vec![], "tier".to_string()),
1742 name: "tier".to_string(),
1743 branches: vec![
1744 {
1745 let result = create_literal_expr(create_text_literal("bronze".to_string()));
1746 Branch {
1747 condition: None,
1748 normalized_condition: None,
1749 result: result.clone(),
1750 normalized_result: result,
1751 source: test_source(),
1752 }
1753 },
1754 {
1755 let result = create_literal_expr(create_text_literal("silver".to_string()));
1756 Branch {
1757 condition: Some(Expression::new(
1758 ExpressionKind::Comparison(
1759 Arc::new(create_data_path_expr(points_path.clone())),
1760 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1761 Arc::new(create_literal_expr(create_number_literal(100.into()))),
1762 ),
1763 test_source(),
1764 )),
1765 normalized_condition: None,
1766 result: result.clone(),
1767 normalized_result: result,
1768 source: test_source(),
1769 }
1770 },
1771 {
1772 let result = create_literal_expr(create_text_literal("gold".to_string()));
1773 Branch {
1774 condition: Some(Expression::new(
1775 ExpressionKind::Comparison(
1776 Arc::new(create_data_path_expr(points_path.clone())),
1777 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1778 Arc::new(create_literal_expr(create_number_literal(500.into()))),
1779 ),
1780 test_source(),
1781 )),
1782 normalized_condition: None,
1783 result: result.clone(),
1784 normalized_result: result,
1785 source: test_source(),
1786 }
1787 },
1788 ],
1789 needs_data: BTreeSet::from([points_path]),
1790 source: test_source(),
1791 rule_type: primitive_text().clone(),
1792 };
1793
1794 plan.rules.push(rule);
1795
1796 let json = serde_json::to_string(&plan).expect("Should serialize");
1797 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1798
1799 assert_eq!(deserialized.rules.len(), 1);
1800 assert_eq!(deserialized.rules[0].branches.len(), 3);
1801 assert!(deserialized.rules[0].branches[0].condition.is_none());
1802 assert!(deserialized.rules[0].branches[1].condition.is_some());
1803 assert!(deserialized.rules[0].branches[2].condition.is_some());
1804 }
1805
1806 #[test]
1807 fn test_serialize_deserialize_empty_plan() {
1808 let plan = ExecutionPlan {
1809 spec_name: "empty".to_string(),
1810 data: IndexMap::new(),
1811 rules: Vec::new(),
1812 reference_evaluation_order: Vec::new(),
1813 meta: HashMap::new(),
1814 unit_index: HashMap::new(),
1815 effective: EffectiveDate::Origin,
1816 sources: Vec::new(),
1817 };
1818
1819 let json = serde_json::to_string(&plan).expect("Should serialize");
1820 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1821
1822 assert_eq!(deserialized.spec_name, "empty");
1823 assert_eq!(deserialized.data.len(), 0);
1824 assert_eq!(deserialized.rules.len(), 0);
1825 }
1826
1827 #[test]
1828 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
1829 use crate::planning::semantics::ExpressionKind;
1830
1831 let x_path = DataPath::new(vec![], "x".to_string());
1832 let mut data = IndexMap::new();
1833 data.insert(
1834 x_path.clone(),
1835 crate::planning::semantics::DataDefinition::Value {
1836 value: create_number_literal(0.into()),
1837 source: test_source(),
1838 },
1839 );
1840 let mut plan = ExecutionPlan {
1841 spec_name: "test".to_string(),
1842 data,
1843 rules: Vec::new(),
1844 reference_evaluation_order: Vec::new(),
1845 meta: HashMap::new(),
1846 unit_index: HashMap::new(),
1847 effective: EffectiveDate::Origin,
1848 sources: Vec::new(),
1849 };
1850
1851 let rule = ExecutableRule {
1852 path: RulePath::new(vec![], "doubled".to_string()),
1853 name: "doubled".to_string(),
1854 branches: vec![{
1855 let result = Expression::new(
1856 ExpressionKind::Arithmetic(
1857 Arc::new(create_data_path_expr(x_path.clone())),
1858 crate::parsing::ast::ArithmeticComputation::Multiply,
1859 Arc::new(create_literal_expr(create_number_literal(2.into()))),
1860 ),
1861 test_source(),
1862 );
1863 Branch {
1864 condition: None,
1865 normalized_condition: None,
1866 result: result.clone(),
1867 normalized_result: result,
1868 source: test_source(),
1869 }
1870 }],
1871 needs_data: BTreeSet::from([x_path]),
1872 source: test_source(),
1873 rule_type: crate::planning::semantics::primitive_number().clone(),
1874 };
1875
1876 plan.rules.push(rule);
1877
1878 let json = serde_json::to_string(&plan).expect("Should serialize");
1879 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1880
1881 assert_eq!(deserialized.rules.len(), 1);
1882 match &deserialized.rules[0].branches[0].result.kind {
1883 ExpressionKind::Arithmetic(left, op, right) => {
1884 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
1885 match &left.kind {
1886 ExpressionKind::DataPath(_) => {}
1887 _ => panic!("Expected DataPath in left operand"),
1888 }
1889 match &right.kind {
1890 ExpressionKind::Literal(_) => {}
1891 _ => panic!("Expected Literal in right operand"),
1892 }
1893 }
1894 _ => panic!("Expected Arithmetic expression"),
1895 }
1896 }
1897
1898 #[test]
1899 fn test_serialize_deserialize_round_trip_equality() {
1900 use crate::planning::semantics::ExpressionKind;
1901
1902 let age_path = DataPath::new(vec![], "age".to_string());
1903 let mut data = IndexMap::new();
1904 data.insert(
1905 age_path.clone(),
1906 crate::planning::semantics::DataDefinition::Value {
1907 value: create_number_literal(0.into()),
1908 source: test_source(),
1909 },
1910 );
1911 let mut plan = ExecutionPlan {
1912 spec_name: "test".to_string(),
1913 data,
1914 rules: Vec::new(),
1915 reference_evaluation_order: Vec::new(),
1916 meta: HashMap::new(),
1917 unit_index: HashMap::new(),
1918 effective: EffectiveDate::Origin,
1919 sources: Vec::new(),
1920 };
1921
1922 let rule = ExecutableRule {
1923 path: RulePath::new(vec![], "is_adult".to_string()),
1924 name: "is_adult".to_string(),
1925 branches: vec![{
1926 let result = create_literal_expr(create_boolean_literal(true));
1927 Branch {
1928 condition: Some(Expression::new(
1929 ExpressionKind::Comparison(
1930 Arc::new(create_data_path_expr(age_path.clone())),
1931 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1932 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1933 ),
1934 test_source(),
1935 )),
1936 normalized_condition: None,
1937 result: result.clone(),
1938 normalized_result: result,
1939 source: test_source(),
1940 }
1941 }],
1942 needs_data: BTreeSet::from([age_path]),
1943 source: test_source(),
1944 rule_type: primitive_boolean().clone(),
1945 };
1946
1947 plan.rules.push(rule);
1948
1949 let json = serde_json::to_string(&plan).expect("Should serialize");
1950 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1951
1952 let json2 = serde_json::to_string(&deserialized).expect("Should serialize again");
1953 let deserialized2: ExecutionPlan =
1954 serde_json::from_str(&json2).expect("Should deserialize again");
1955
1956 assert_eq!(deserialized2.spec_name, plan.spec_name);
1957 assert_eq!(deserialized2.data.len(), plan.data.len());
1958 assert_eq!(deserialized2.rules.len(), plan.rules.len());
1959 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
1960 assert_eq!(
1961 deserialized2.rules[0].branches.len(),
1962 plan.rules[0].branches.len()
1963 );
1964 }
1965
1966 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
1967 ExecutionPlan {
1968 spec_name: "s".into(),
1969 data: IndexMap::new(),
1970 rules: Vec::new(),
1971 reference_evaluation_order: Vec::new(),
1972 meta: HashMap::new(),
1973 unit_index: HashMap::new(),
1974 effective,
1975 sources: Vec::new(),
1976 }
1977 }
1978
1979 #[test]
1980 fn plan_at_exact_boundary_selects_later_slice() {
1981 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
1982
1983 let june = DateTimeValue {
1984 year: 2025,
1985 month: 6,
1986 day: 1,
1987 hour: 0,
1988 minute: 0,
1989 second: 0,
1990 microsecond: 0,
1991 timezone: None,
1992 };
1993 let dec = DateTimeValue {
1994 year: 2025,
1995 month: 12,
1996 day: 1,
1997 hour: 0,
1998 minute: 0,
1999 second: 0,
2000 microsecond: 0,
2001 timezone: None,
2002 };
2003
2004 let set = ExecutionPlanSet {
2005 spec_name: "s".into(),
2006 plans: vec![
2007 empty_plan(EffectiveDate::Origin),
2008 empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2009 empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2010 ],
2011 };
2012
2013 assert!(std::ptr::eq(
2014 set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2015 .expect("boundary instant"),
2016 &set.plans[1]
2017 ));
2018 assert!(std::ptr::eq(
2019 set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2020 .expect("dec boundary"),
2021 &set.plans[2]
2022 ));
2023 }
2024
2025 #[test]
2026 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2027 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2028
2029 let june = DateTimeValue {
2030 year: 2025,
2031 month: 6,
2032 day: 1,
2033 hour: 0,
2034 minute: 0,
2035 second: 0,
2036 microsecond: 0,
2037 timezone: None,
2038 };
2039 let may_end = DateTimeValue {
2040 year: 2025,
2041 month: 5,
2042 day: 31,
2043 hour: 23,
2044 minute: 59,
2045 second: 59,
2046 microsecond: 0,
2047 timezone: None,
2048 };
2049
2050 let set = ExecutionPlanSet {
2051 spec_name: "s".into(),
2052 plans: vec![
2053 empty_plan(EffectiveDate::Origin),
2054 empty_plan(EffectiveDate::DateTimeValue(june)),
2055 ],
2056 };
2057
2058 assert!(std::ptr::eq(
2059 set.plan_at(&EffectiveDate::DateTimeValue(may_end))
2060 .expect("may 31"),
2061 &set.plans[0]
2062 ));
2063 }
2064
2065 #[test]
2066 fn plan_at_single_plan_matches_any_instant_after_start() {
2067 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2068
2069 let t = DateTimeValue {
2070 year: 2025,
2071 month: 3,
2072 day: 1,
2073 hour: 0,
2074 minute: 0,
2075 second: 0,
2076 microsecond: 0,
2077 timezone: None,
2078 };
2079 let set = ExecutionPlanSet {
2080 spec_name: "s".into(),
2081 plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
2082 year: 2025,
2083 month: 1,
2084 day: 1,
2085 hour: 0,
2086 minute: 0,
2087 second: 0,
2088 microsecond: 0,
2089 timezone: None,
2090 }))],
2091 };
2092 assert!(std::ptr::eq(
2093 set.plan_at(&EffectiveDate::DateTimeValue(t))
2094 .expect("inside single slice"),
2095 &set.plans[0]
2096 ));
2097 }
2098
2099 #[test]
2102 fn schema_json_shape_contract() {
2103 let mut engine = Engine::new();
2104 engine
2105 .load(
2106 r#"
2107 spec pricing
2108 data bridge_height: quantity
2109 -> unit meter 1
2110 -> default 100 meter
2111 data quantity: number -> minimum 0
2112 rule cost: bridge_height * quantity
2113 "#,
2114 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2115 "test.lemma",
2116 ))),
2117 )
2118 .unwrap();
2119 let now = DateTimeValue::now();
2120 let schema = engine
2121 .get_plan(None, "pricing", Some(&now))
2122 .unwrap()
2123 .schema();
2124
2125 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2126
2127 let bh = &value["data"]["bridge_height"];
2128 assert!(
2129 bh.is_object(),
2130 "data entry must be a named object, not tuple"
2131 );
2132 assert!(
2133 bh.get("type").is_some(),
2134 "data entry must expose `type` field"
2135 );
2136 assert!(
2137 bh.get("default").is_some(),
2138 "bridge_height exposes `-> default` as schema default suggestion"
2139 );
2140 assert!(
2141 bh.get("bound_value").is_none(),
2142 "bridge_height is not a spec-bound literal"
2143 );
2144
2145 let ty = &bh["type"];
2146 assert_eq!(
2147 ty["kind"], "quantity",
2148 "kind tag sits on the type object itself"
2149 );
2150 assert!(
2151 ty["units"].is_array(),
2152 "quantity-only fields flatten up to top level"
2153 );
2154 assert!(
2155 ty.get("options").is_none(),
2156 "text-only fields must not leak"
2157 );
2158
2159 let qty = &value["data"]["quantity"];
2160 assert_eq!(qty["type"]["kind"], "number");
2161 assert!(
2162 qty.get("default").is_none(),
2163 "quantity has no default suggestion"
2164 );
2165 assert!(
2166 qty.get("bound_value").is_none(),
2167 "quantity has no bound literal"
2168 );
2169
2170 let cost = &value["rules"]["cost"];
2171 assert_eq!(
2172 cost["kind"], "quantity",
2173 "rule types use the same flat shape"
2174 );
2175 assert!(
2176 cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
2177 "quantity rule result types expose declared units"
2178 );
2179 assert!(
2180 cost["units"][0].get("factor").is_some(),
2181 "quantity rule units use factor field"
2182 );
2183 }
2184
2185 #[test]
2186 fn schema_rule_result_units_contract() {
2187 let mut engine = Engine::new();
2188 engine
2189 .load(
2190 r#"
2191 spec units_contract
2192 data money: quantity
2193 -> unit eur 1
2194 -> unit usd 0.91
2195 data rate: ratio
2196 -> unit basis_points 10000
2197 -> unit percent 100
2198 -> default 500 basis_points
2199 rule total: money
2200 rule rate_out: rate
2201 "#,
2202 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2203 "units_contract.lemma",
2204 ))),
2205 )
2206 .unwrap();
2207 let now = DateTimeValue::now();
2208 let schema = engine
2209 .get_plan(None, "units_contract", Some(&now))
2210 .unwrap()
2211 .schema();
2212 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2213
2214 let money_units = &value["data"]["money"]["type"]["units"];
2215 assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
2216 assert!(money_units[0].get("name").is_some());
2217 assert!(money_units[0].get("factor").is_some());
2218 assert!(money_units[0]["factor"].get("numer").is_some());
2219 assert!(money_units[0]["factor"].get("denom").is_some());
2220
2221 let rate_units = &value["data"]["rate"]["type"]["units"];
2222 assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
2223 assert!(rate_units[0].get("name").is_some());
2224 assert!(rate_units[0].get("value").is_some());
2225 assert!(rate_units[0]["value"].get("numer").is_some());
2226 assert!(rate_units[0]["value"].get("denom").is_some());
2227
2228 let total_rule_units = &value["rules"]["total"]["units"];
2229 let money_unit_names: Vec<_> = money_units
2230 .as_array()
2231 .unwrap()
2232 .iter()
2233 .map(|u| u["name"].as_str().unwrap())
2234 .collect();
2235 let total_rule_unit_names: Vec<_> = total_rule_units
2236 .as_array()
2237 .unwrap()
2238 .iter()
2239 .map(|u| u["name"].as_str().unwrap())
2240 .collect();
2241 assert_eq!(total_rule_unit_names, money_unit_names);
2242
2243 let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
2244 let rate_unit_names: Vec<_> = rate_units
2245 .as_array()
2246 .unwrap()
2247 .iter()
2248 .map(|u| u["name"].as_str().unwrap())
2249 .collect();
2250 let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
2251 .as_array()
2252 .unwrap()
2253 .iter()
2254 .map(|u| u["name"].as_str().unwrap())
2255 .collect();
2256 assert_eq!(rate_out_rule_unit_names, rate_unit_names);
2257 }
2258
2259 #[test]
2260 fn schema_json_round_trip_preserves_shape() {
2261 let mut engine = Engine::new();
2262 engine
2263 .load(
2264 r#"
2265 spec s
2266 data age: number -> minimum 0 -> default 18
2267 data grade: text -> options "A" "B" "C"
2268 rule adult: age >= 18
2269 "#,
2270 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
2271 )
2272 .unwrap();
2273 let now = DateTimeValue::now();
2274 let schema = engine.get_plan(None, "s", Some(&now)).unwrap().schema();
2275
2276 let json = serde_json::to_string(&schema).unwrap();
2277 let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
2278 assert_eq!(schema, round_tripped);
2279 }
2280}
2281
2282