1use crate::computation::UnitResolutionContext;
12use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, LemmaSpec, MetaValue};
13use crate::parsing::source::Source;
14use crate::planning::data_input::{parse_data_value, DataValueInput};
15use crate::planning::graph::Graph;
16use crate::planning::graph::ResolvedSpecTypes;
17use crate::planning::normalize::{
18 build_decision_table, is_literal_bool_expression, normalize_expression,
19};
20use crate::planning::semantics::{
21 value_kind_matches_spec, DataDefinition, DataPath, Expression, LemmaType, LiteralValue,
22 RulePath, TypeSpecification, ValueKind,
23};
24use crate::Error;
25use crate::ResourceLimits;
26use indexmap::IndexMap;
27use serde::{Deserialize, Deserializer, Serialize, Serializer};
28use std::collections::{BTreeSet, HashMap, HashSet};
29use std::sync::Arc;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SpecSource {
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub repository: Option<String>,
41 pub name: String,
42 pub effective_from: EffectiveDate,
43 pub source: String,
44}
45
46pub type SpecSources = Vec<SpecSource>;
47
48#[derive(Debug, Clone)]
53pub struct ExecutionPlan {
54 pub spec_name: String,
56
57 pub commentary: Option<String>,
59
60 pub data: IndexMap<DataPath, DataDefinition>,
62
63 pub rules: Vec<ExecutableRule>,
65
66 pub reference_evaluation_order: Vec<DataPath>,
71
72 pub meta: HashMap<String, MetaValue>,
74
75 pub resolved_types: ResolvedSpecTypes,
79
80 pub signature_index: crate::computation::arithmetic::SignatureIndex,
86
87 pub effective: EffectiveDate,
88
89 pub sources: SpecSources,
92}
93
94#[derive(Debug, Clone)]
97pub struct ExecutionPlanSet {
98 pub spec_name: String,
99 pub plans: Vec<ExecutionPlan>,
100}
101
102impl ExecutionPlanSet {
103 #[must_use]
105 pub fn plan_at(&self, effective: &EffectiveDate) -> Option<&ExecutionPlan> {
106 for (i, plan) in self.plans.iter().enumerate() {
107 let from_ok = *effective >= plan.effective;
108 let to_ok = self
109 .plans
110 .get(i + 1)
111 .map(|next| *effective < next.effective)
112 .unwrap_or(true);
113 if from_ok && to_ok {
114 return Some(plan);
115 }
116 }
117 None
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ExecutableRule {
126 pub path: RulePath,
128
129 pub name: String,
131
132 pub branches: Vec<Branch>,
136
137 pub normalized_branches: Vec<NormalizedBranch>,
139
140 pub needs_data: BTreeSet<DataPath>,
142
143 pub source: Source,
145
146 #[serde(with = "arc_lemma_type")]
149 pub rule_type: Arc<LemmaType>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct Branch {
155 pub condition: Option<Expression>,
157
158 pub result: Expression,
160
161 pub source: Source,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct NormalizedBranch {
168 pub condition: Expression,
170
171 pub result: Expression,
173}
174
175mod arc_lemma_type {
176 use super::LemmaType;
177 use serde::{Deserialize, Deserializer, Serialize, Serializer};
178 use std::sync::Arc;
179
180 pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
181 where
182 S: Serializer,
183 {
184 value.as_ref().serialize(serializer)
185 }
186
187 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
188 where
189 D: Deserializer<'de>,
190 {
191 LemmaType::deserialize(deserializer).map(Arc::new)
192 }
193}
194
195pub(crate) fn build_execution_plan(
198 graph: &Graph,
199 resolved_types: &mut Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>, ResolvedSpecTypes)>,
200 effective: &EffectiveDate,
201) -> Result<ExecutionPlan, Vec<Error>> {
202 let execution_order = graph.execution_order();
203
204 let main_spec = graph.main_spec();
205 let main_idx = resolved_types
206 .iter()
207 .position(|(_, spec, _)| Arc::ptr_eq(spec, main_spec));
208
209 let mut sources: SpecSources = Vec::new();
210 for (repo, spec, _) in resolved_types.iter() {
211 if !sources.iter().any(|e| {
212 e.repository == repo.name
213 && e.name == spec.name
214 && e.effective_from == spec.effective_from
215 }) {
216 sources.push(SpecSource {
217 repository: repo.name.clone(),
218 name: spec.name.clone(),
219 effective_from: spec.effective_from.clone(),
220 source: crate::formatting::format_specs(&[spec.as_ref().clone()]),
221 });
222 }
223 }
224
225 let main_resolved_types = main_idx
226 .map(|idx| resolved_types.remove(idx).2)
227 .unwrap_or_default();
228 let data = graph.build_data(&main_resolved_types.resolved);
229
230 let signature_index = crate::planning::graph::build_signature_index(
231 &main_spec.name,
232 &main_resolved_types.unit_index,
233 )
234 .expect("BUG: signature_index build already validated during resolve_and_validate");
235
236 let mut executable_rules: Vec<ExecutableRule> = Vec::new();
237 let mut path_to_index: HashMap<RulePath, usize> = HashMap::new();
238
239 for rule_path in execution_order {
240 let rule_node = graph.rules().get(rule_path).expect(
241 "bug: rule from topological sort not in graph - validation should have caught this",
242 );
243
244 let mut executable_branches = Vec::new();
245 for (condition, result) in &rule_node.branches {
246 executable_branches.push(Branch {
247 condition: condition.clone(),
248 result: result.clone(),
249 source: rule_node.source.clone(),
250 });
251 }
252
253 let unit_ctx = UnitResolutionContext::WithIndex(&main_resolved_types.unit_index);
254 let decision_table = build_decision_table(&rule_node.branches);
255 let mut normalized_branches = Vec::new();
256 let mut direct_data = HashSet::new();
257 for (condition, result) in decision_table {
258 let normalized_condition =
259 normalize_expression(&condition, Some(&unit_ctx)).map_err(|error| {
260 vec![Error::validation(
261 format!("failed to normalize decision table condition: {error}"),
262 Some(rule_node.source.clone()),
263 None::<String>,
264 )]
265 })?;
266 if is_literal_bool_expression(&normalized_condition, false) {
267 continue;
268 }
269 let normalized_result =
270 normalize_expression(&result, Some(&unit_ctx)).map_err(|error| {
271 vec![Error::validation(
272 format!("failed to normalize decision table result: {error}"),
273 Some(rule_node.source.clone()),
274 None::<String>,
275 )]
276 })?;
277 normalized_condition.collect_data_paths(&mut direct_data);
278 normalized_result.collect_data_paths(&mut direct_data);
279 normalized_branches.push(NormalizedBranch {
280 condition: normalized_condition,
281 result: normalized_result,
282 });
283 }
284
285 let mut needs_data: BTreeSet<DataPath> = direct_data.into_iter().collect();
286
287 for dep in &rule_node.depends_on_rules {
288 if let Some(&dep_idx) = path_to_index.get(dep) {
289 needs_data.extend(executable_rules[dep_idx].needs_data.iter().cloned());
290 }
291 }
292
293 path_to_index.insert(rule_path.clone(), executable_rules.len());
294 executable_rules.push(ExecutableRule {
295 path: rule_path.clone(),
296 name: rule_path.rule.clone(),
297 branches: executable_branches,
298 normalized_branches,
299 source: rule_node.source.clone(),
300 needs_data,
301 rule_type: Arc::clone(&rule_node.rule_type),
302 });
303 }
304
305 Ok(ExecutionPlan {
306 spec_name: main_spec.name.clone(),
307 commentary: main_spec.commentary.clone(),
308 data,
309 rules: executable_rules,
310 reference_evaluation_order: graph.reference_evaluation_order().to_vec(),
311 meta: main_spec
312 .meta_fields
313 .iter()
314 .map(|f| (f.key.clone(), f.value.clone()))
315 .collect(),
316 resolved_types: main_resolved_types,
317 signature_index,
318 effective: effective.clone(),
319 sources,
320 })
321}
322
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347pub struct DataEntry {
348 #[serde(rename = "type")]
349 pub lemma_type: LemmaType,
350 #[serde(skip_serializing_if = "Option::is_none", default)]
351 pub bound_value: Option<LiteralValue>,
352 #[serde(skip_serializing_if = "Option::is_none", default)]
353 pub default: Option<LiteralValue>,
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
357pub struct SpecSchema {
358 pub spec: String,
360 #[serde(skip_serializing_if = "Option::is_none", default)]
362 pub commentary: Option<String>,
363 #[serde(skip_serializing_if = "Option::is_none", default)]
365 pub effective: Option<DateTimeValue>,
366 #[serde(skip_serializing_if = "Vec::is_empty", default)]
369 pub versions: Vec<DateTimeValue>,
370 pub data: indexmap::IndexMap<String, DataEntry>,
372 pub rules: indexmap::IndexMap<String, LemmaType>,
374 pub meta: HashMap<String, MetaValue>,
376}
377
378impl std::fmt::Display for SpecSchema {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 write!(f, "Spec: {}", self.spec)?;
381
382 if let Some(commentary) = &self.commentary {
383 write!(f, "\n {}", commentary)?;
384 }
385
386 if !self.meta.is_empty() {
387 write!(f, "\n\nMeta:")?;
388 let mut entries: Vec<(&String, &MetaValue)> = self.meta.iter().collect();
390 entries.sort_by_key(|(k, _)| *k);
391 for (key, value) in entries {
392 write!(f, "\n {}: {}", key, value)?;
393 }
394 }
395
396 if !self.data.is_empty() {
397 write!(f, "\n\nData:")?;
398 for (name, entry) in &self.data {
399 write!(f, "\n {} ({})", name, entry.lemma_type.specifications)?;
400 for line in type_detail_lines(&entry.lemma_type.specifications) {
401 write!(f, "\n {}", line)?;
402 }
403 let help = entry.lemma_type.specifications.help();
404 if !help.is_empty() {
405 write!(f, "\n help: {}", help)?;
406 }
407 if let Some(val) = &entry.bound_value {
408 write!(f, "\n value: {}", val)?;
409 }
410 if let Some(val) = &entry.default {
411 write!(f, "\n default: {}", val)?;
412 }
413 }
414 }
415
416 if !self.rules.is_empty() {
417 write!(f, "\n\nRules:")?;
418 for (name, rule_type) in &self.rules {
419 write!(f, "\n {} ({})", name, rule_type.specifications)?;
420 }
421 }
422
423 if self.data.is_empty() && self.rules.is_empty() {
424 write!(f, "\n (no data or rules)")?;
425 }
426
427 Ok(())
428 }
429}
430
431impl SpecSchema {
432 pub(crate) fn is_type_compatible(&self, other: &SpecSchema) -> bool {
437 for (name, entry) in &self.data {
438 if let Some(other_entry) = other.data.get(name) {
439 if entry.lemma_type != other_entry.lemma_type {
440 return false;
441 }
442 }
443 }
444 for (name, lt) in &self.rules {
445 if let Some(other_lt) = other.rules.get(name) {
446 if lt != other_lt {
447 return false;
448 }
449 }
450 }
451 true
452 }
453}
454
455pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
461 use crate::computation::rational::rational_to_display_str;
462 let mut lines = Vec::new();
463 match spec {
464 TypeSpecification::Quantity {
465 minimum,
466 maximum,
467 decimals,
468 units,
469 ..
470 } => {
471 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
472 if !unit_names.is_empty() {
473 lines.push(format!("units: {}", unit_names.join(", ")));
474 }
475 if let Some(d) = decimals {
476 lines.push(format!("decimals: {}", d));
477 }
478 if let Some((magnitude, unit_name)) = minimum {
479 lines.push(format!(
480 "minimum: {} {}",
481 rational_to_display_str(magnitude),
482 unit_name
483 ));
484 }
485 if let Some((magnitude, unit_name)) = maximum {
486 lines.push(format!(
487 "maximum: {} {}",
488 rational_to_display_str(magnitude),
489 unit_name
490 ));
491 }
492 }
493 TypeSpecification::Number {
494 minimum,
495 maximum,
496 decimals,
497 ..
498 } => {
499 if let Some(d) = decimals {
500 lines.push(format!("decimals: {}", d));
501 }
502 if let Some(v) = minimum {
503 lines.push(format!("minimum: {}", rational_to_display_str(v)));
504 }
505 if let Some(v) = maximum {
506 lines.push(format!("maximum: {}", rational_to_display_str(v)));
507 }
508 }
509 TypeSpecification::Ratio {
510 minimum,
511 maximum,
512 decimals,
513 units,
514 ..
515 } => {
516 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
517 if !unit_names.is_empty() {
518 lines.push(format!("units: {}", unit_names.join(", ")));
519 }
520 if let Some(d) = decimals {
521 lines.push(format!("decimals: {}", d));
522 }
523 if let Some(v) = minimum {
524 lines.push(format!("minimum: {}", rational_to_display_str(v)));
525 }
526 if let Some(v) = maximum {
527 lines.push(format!("maximum: {}", rational_to_display_str(v)));
528 }
529 }
530 TypeSpecification::Text {
531 options, length, ..
532 } => {
533 if let Some(l) = length {
534 lines.push(format!("length: {}", l));
535 }
536 if !options.is_empty() {
537 let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
538 lines.push(format!("options: {}", quoted.join(", ")));
539 }
540 }
541 TypeSpecification::Date {
542 minimum, maximum, ..
543 } => {
544 if let Some(v) = minimum {
545 lines.push(format!("minimum: {}", v));
546 }
547 if let Some(v) = maximum {
548 lines.push(format!("maximum: {}", v));
549 }
550 }
551 TypeSpecification::Time {
552 minimum, maximum, ..
553 } => {
554 if let Some(v) = minimum {
555 lines.push(format!("minimum: {}", v));
556 }
557 if let Some(v) = maximum {
558 lines.push(format!("maximum: {}", v));
559 }
560 }
561 TypeSpecification::QuantityRange { units, .. } => {
562 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
563 if !unit_names.is_empty() {
564 lines.push(format!("units: {}", unit_names.join(", ")));
565 }
566 }
567 TypeSpecification::RatioRange { units, .. } => {
568 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
569 if !unit_names.is_empty() {
570 lines.push(format!("units: {}", unit_names.join(", ")));
571 }
572 }
573 TypeSpecification::Boolean { .. }
574 | TypeSpecification::NumberRange { .. }
575 | TypeSpecification::DateRange { .. }
576 | TypeSpecification::TimeRange { .. }
577 | TypeSpecification::Veto { .. }
578 | TypeSpecification::Undetermined => {}
579 }
580 lines
581}
582
583impl ExecutionPlan {
584 pub(crate) fn expression_unit_index(&self) -> &HashMap<String, Arc<LemmaType>> {
588 &self.resolved_types.unit_index
589 }
590
591 pub fn schema(&self) -> SpecSchema {
599 let all_local_rules: Vec<String> = self
600 .rules
601 .iter()
602 .filter(|r| r.path.segments.is_empty())
603 .map(|r| r.name.clone())
604 .collect();
605 self.schema_for_rules(&all_local_rules)
606 .expect("BUG: all_local_rules sourced from self.rules")
607 }
608
609 pub fn interface_schema(&self) -> SpecSchema {
611 let mut data_entries: Vec<(usize, String, DataEntry)> = self
612 .data
613 .iter()
614 .filter(|(_, data)| data.schema_type().is_some())
615 .map(|(path, data)| {
616 let lemma_type = data
617 .schema_type()
618 .expect("BUG: filter above ensured schema_type is Some")
619 .clone();
620 let bound_value = data.bound_value().cloned();
621 let default = data.default_suggestion();
622 (
623 data.source().span.start,
624 path.input_key(),
625 DataEntry {
626 lemma_type,
627 bound_value,
628 default,
629 },
630 )
631 })
632 .collect();
633 data_entries.sort_by_key(|(pos, _, _)| *pos);
634
635 let rule_entries: Vec<(String, LemmaType)> = self
636 .rules
637 .iter()
638 .filter(|r| r.path.segments.is_empty())
639 .map(|r| (r.name.clone(), (*r.rule_type).clone()))
640 .collect();
641
642 SpecSchema {
643 spec: self.spec_name.clone(),
644 commentary: self.commentary.clone(),
645 effective: self.effective.as_ref().cloned(),
646 versions: Vec::new(),
647 data: data_entries
648 .into_iter()
649 .map(|(_, name, data)| (name, data))
650 .collect(),
651 rules: rule_entries.into_iter().collect(),
652 meta: self.meta.clone(),
653 }
654 }
655
656 pub fn schema_for_rules(&self, rule_names: &[String]) -> Result<SpecSchema, Error> {
665 let mut needed_data = HashSet::new();
666 let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
667
668 for rule_name in rule_names {
669 let rule = self.get_rule(rule_name).ok_or_else(|| {
670 Error::request(
671 format!(
672 "Rule '{}' not found in spec '{}'",
673 rule_name, self.spec_name
674 ),
675 None::<String>,
676 )
677 })?;
678 needed_data.extend(rule.needs_data.iter().cloned());
679 rule_entries.push((rule.name.clone(), (*rule.rule_type).clone()));
680 }
681
682 let mut data_entries: Vec<(usize, String, DataEntry)> = self
683 .data
684 .iter()
685 .filter(|(path, _)| needed_data.contains(path))
686 .filter_map(|(path, data)| {
687 let lemma_type = data.schema_type()?.clone();
688 let bound_value = data.bound_value().cloned();
689 let default = data.default_suggestion();
690 Some((
691 data.source().span.start,
692 path.input_key(),
693 DataEntry {
694 lemma_type,
695 bound_value,
696 default,
697 },
698 ))
699 })
700 .collect();
701 data_entries.sort_by_key(|(pos, _, _)| *pos);
702 let data_entries: Vec<(String, DataEntry)> = data_entries
703 .into_iter()
704 .map(|(_, name, data)| (name, data))
705 .collect();
706
707 Ok(SpecSchema {
708 spec: self.spec_name.clone(),
709 commentary: self.commentary.clone(),
710 effective: self.effective.as_ref().cloned(),
711 versions: Vec::new(),
712 data: data_entries.into_iter().collect(),
713 rules: rule_entries.into_iter().collect(),
714 meta: self.meta.clone(),
715 })
716 }
717
718 pub fn get_data_path_by_str(&self, name: &str) -> Option<&DataPath> {
720 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
721 self.data
722 .keys()
723 .find(|path| path.input_key() == canonical_name)
724 }
725
726 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
728 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
729 self.rules
730 .iter()
731 .find(|r| r.name == canonical_name && r.path.segments.is_empty())
732 }
733
734 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
736 self.rules.iter().find(|r| &r.path == rule_path)
737 }
738
739 pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
741 self.data.get(path).and_then(|d| d.value())
742 }
743
744 pub fn set_data_values(
748 mut self,
749 values: std::collections::HashMap<String, DataValueInput>,
750 limits: &ResourceLimits,
751 ) -> Result<Self, Error> {
752 for (name, raw_value) in values {
753 let data_path = self.get_data_path_by_str(&name).ok_or_else(|| {
754 let available: Vec<String> = self.data.keys().map(|p| p.input_key()).collect();
755 Error::request(
756 format!(
757 "Data '{}' not found. Available data: {}",
758 name,
759 available.join(", ")
760 ),
761 None::<String>,
762 )
763 })?;
764 let data_path = data_path.clone();
765
766 let data_definition = self
767 .data
768 .get(&data_path)
769 .expect("BUG: data_path was just resolved from self.data, must exist");
770
771 let data_source = data_definition.source().clone();
772 let type_arc = match data_definition {
773 DataDefinition::TypeDeclaration { resolved_type, .. }
774 | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
775 DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
776 DataDefinition::Import { .. } => {
777 return Err(Error::request(
778 format!(
779 "Data '{}' is a spec reference; cannot provide a value.",
780 name
781 ),
782 None::<String>,
783 ));
784 }
785 DataDefinition::Violated { .. } => {
786 unreachable!(
787 "BUG: Violated data '{}' cannot appear before set_data_values on a fresh plan clone",
788 name
789 );
790 }
791 };
792
793 let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
794 Ok(value) => value,
795 Err(error) => {
796 self.data.insert(
797 data_path,
798 DataDefinition::Violated {
799 reason: error.message().to_string(),
800 source: data_source,
801 },
802 );
803 continue;
804 }
805 };
806
807 let size = literal_value.byte_size();
808 if size > limits.max_data_value_bytes {
809 return Err(Error::resource_limit_exceeded(
810 "max_data_value_bytes",
811 limits.max_data_value_bytes.to_string(),
812 size.to_string(),
813 format!(
814 "Reduce the size of data values to {} bytes or less",
815 limits.max_data_value_bytes
816 ),
817 Some(data_source.clone()),
818 None,
819 None,
820 )
821 .with_related_data(&name));
822 }
823
824 if let Err(msg) = validate_value_against_type(type_arc.as_ref(), &literal_value) {
825 self.data.insert(
826 data_path,
827 DataDefinition::Violated {
828 reason: msg,
829 source: data_source,
830 },
831 );
832 continue;
833 }
834
835 self.data.insert(
836 data_path,
837 DataDefinition::Value {
838 value: literal_value,
839 source: data_source,
840 },
841 );
842 }
843
844 Ok(self)
845 }
846
847 #[must_use]
851 pub fn with_defaults(mut self) -> Self {
852 let promotions: Vec<(DataPath, DataDefinition)> = self
853 .data
854 .iter()
855 .filter_map(|(path, def)| {
856 if let DataDefinition::TypeDeclaration {
857 declared_default: Some(dv),
858 resolved_type,
859 source,
860 } = def
861 {
862 Some((
863 path.clone(),
864 DataDefinition::Value {
865 value: LiteralValue {
866 value: dv.clone(),
867 lemma_type: Arc::clone(resolved_type),
868 },
869 source: source.clone(),
870 },
871 ))
872 } else {
873 None
874 }
875 })
876 .collect();
877
878 for (path, def) in promotions {
879 self.data.insert(path, def);
880 }
881 self
882 }
883}
884
885pub(crate) fn validate_value_against_type(
886 expected_type: &LemmaType,
887 value: &LiteralValue,
888) -> Result<(), String> {
889 use crate::computation::rational::{commit_rational_to_decimal, RationalInteger};
890 use crate::planning::semantics::TypeSpecification;
891
892 fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
893 match commit_rational_to_decimal(magnitude) {
894 Ok(decimal) => decimal.scale() > u32::from(max_decimals),
895 Err(_) => true,
896 }
897 }
898
899 fn format_rational(r: &RationalInteger, decimals: Option<u8>) -> String {
900 use crate::computation::rational::rational_to_display_str;
901 match commit_rational_to_decimal(r) {
902 Ok(decimal) => match decimals {
903 Some(dp) => {
904 let rounded = decimal.round_dp(u32::from(dp));
905 format!("{:.prec$}", rounded, prec = dp as usize)
906 }
907 None => decimal.normalize().to_string(),
908 },
909 Err(_) => rational_to_display_str(r),
910 }
911 }
912
913 match (&expected_type.specifications, &value.value) {
914 (
915 TypeSpecification::Number {
916 minimum,
917 maximum,
918 decimals,
919 ..
920 },
921 ValueKind::Number(n),
922 ) => {
923 if let Some(d) = decimals {
924 if exceeds_decimal_places(n, *d) {
925 return Err(format!(
926 "{} exceeds decimals constraint {d}",
927 format_rational(n, *decimals)
928 ));
929 }
930 }
931 if let Some(min) = minimum {
932 if n < min {
933 return Err(format!(
934 "{} is below minimum {}",
935 format_rational(n, *decimals),
936 format_rational(min, *decimals)
937 ));
938 }
939 }
940 if let Some(max) = maximum {
941 if n > max {
942 return Err(format!(
943 "{} is above maximum {}",
944 format_rational(n, *decimals),
945 format_rational(max, *decimals)
946 ));
947 }
948 }
949 Ok(())
950 }
951 (
952 TypeSpecification::Quantity {
953 minimum,
954 maximum,
955 decimals,
956 units,
957 ..
958 },
959 ValueKind::Quantity(magnitude, signature),
960 ) => {
961 use crate::computation::rational::checked_div;
962 use crate::planning::semantics::quantity_declared_bound_canonical;
963 let unit = signature
964 .first()
965 .map(|(n, _)| n.as_str())
966 .expect("BUG: Quantity value has empty signature in execution plan validation");
967 let quantity_unit = units.get(unit)?;
968 let factor = &quantity_unit.factor;
969 let in_unit = checked_div(magnitude, factor).map_err(|failure| {
970 format!("cannot de-canonicalize quantity for validation: {failure}")
971 })?;
972 if let Some(d) = decimals {
973 if exceeds_decimal_places(&in_unit, *d) {
974 return Err(format!(
975 "{} {unit} exceeds decimals constraint {d}",
976 format_rational(&in_unit, *decimals)
977 ));
978 }
979 }
980 if let Some(bound) = minimum {
981 let canonical_min = quantity_declared_bound_canonical(
982 bound,
983 units,
984 expected_type.name().as_str(),
985 "minimum",
986 )?;
987 if magnitude < &canonical_min {
988 let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
989 format!("cannot de-canonicalize minimum for validation: {failure}")
990 })?;
991 let value_display =
992 format!("{} {}", format_rational(&in_unit, *decimals), unit);
993 let bound_display = format!(
994 "{} {}",
995 format_rational(&min_in_unit, *decimals),
996 quantity_unit.name
997 );
998 return Err(format!("{value_display} is below minimum {bound_display}"));
999 }
1000 }
1001 if let Some(bound) = maximum {
1002 let canonical_max = quantity_declared_bound_canonical(
1003 bound,
1004 units,
1005 expected_type.name().as_str(),
1006 "maximum",
1007 )?;
1008 if magnitude > &canonical_max {
1009 let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
1010 format!("cannot de-canonicalize maximum for validation: {failure}")
1011 })?;
1012 let value_display =
1013 format!("{} {}", format_rational(&in_unit, *decimals), unit);
1014 let bound_display = format!(
1015 "{} {}",
1016 format_rational(&max_in_unit, *decimals),
1017 quantity_unit.name
1018 );
1019 return Err(format!("{value_display} is above maximum {bound_display}"));
1020 }
1021 }
1022 Ok(())
1023 }
1024 (
1025 TypeSpecification::Text {
1026 length, options, ..
1027 },
1028 ValueKind::Text(s),
1029 ) => {
1030 let len = s.chars().count();
1031 if let Some(exact) = length {
1032 if len != *exact {
1033 return Err(format!(
1034 "'{}' has length {} but required length is {}",
1035 s, len, exact
1036 ));
1037 }
1038 }
1039 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
1040 return Err(format!(
1041 "'{}' is not in allowed options: {}",
1042 s,
1043 options.join(", ")
1044 ));
1045 }
1046 Ok(())
1047 }
1048 (
1049 TypeSpecification::Ratio {
1050 minimum,
1051 maximum,
1052 decimals,
1053 units,
1054 ..
1055 },
1056 ValueKind::Ratio(r, unit_name),
1057 ) => {
1058 use crate::computation::rational::checked_mul;
1059
1060 if let Some(d) = decimals {
1061 if exceeds_decimal_places(r, *d) {
1062 return Err(format!(
1063 "{} exceeds decimals constraint {d}",
1064 format_rational(r, *decimals)
1065 ));
1066 }
1067 }
1068 if let Some(type_minimum) = minimum {
1069 if r < type_minimum {
1070 let message = match unit_name.as_deref() {
1071 Some(unit) => {
1072 let ratio_unit = units.get(unit)?;
1073 let value_per_unit = checked_mul(r, &ratio_unit.value)
1074 .map_err(|failure| failure.to_string())?;
1075 let bound_per_unit = ratio_unit.minimum.expect(
1076 "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
1077 );
1078 format!(
1079 "{} {unit} is below minimum {} {unit}",
1080 format_rational(&value_per_unit, *decimals),
1081 format_rational(&bound_per_unit, *decimals),
1082 )
1083 }
1084 None => format!(
1085 "{} is below minimum {}",
1086 format_rational(r, *decimals),
1087 format_rational(type_minimum, *decimals),
1088 ),
1089 };
1090 return Err(message);
1091 }
1092 }
1093 if let Some(type_maximum) = maximum {
1094 if r > type_maximum {
1095 let message = match unit_name.as_deref() {
1096 Some(unit) => {
1097 let ratio_unit = units.get(unit)?;
1098 let value_per_unit = checked_mul(r, &ratio_unit.value)
1099 .map_err(|failure| failure.to_string())?;
1100 let bound_per_unit = ratio_unit.maximum.expect(
1101 "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
1102 );
1103 format!(
1104 "{} {unit} is above maximum {} {unit}",
1105 format_rational(&value_per_unit, *decimals),
1106 format_rational(&bound_per_unit, *decimals),
1107 )
1108 }
1109 None => format!(
1110 "{} is above maximum {}",
1111 format_rational(r, *decimals),
1112 format_rational(type_maximum, *decimals),
1113 ),
1114 };
1115 return Err(message);
1116 }
1117 }
1118 Ok(())
1119 }
1120 (
1121 TypeSpecification::Date {
1122 minimum, maximum, ..
1123 },
1124 ValueKind::Date(dt),
1125 ) => {
1126 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
1127 use std::cmp::Ordering;
1128 if let Some(min) = minimum {
1129 let min_sem = date_time_to_semantic(min);
1130 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
1131 return Err(format!("{} is below minimum {}", dt, min));
1132 }
1133 }
1134 if let Some(max) = maximum {
1135 let max_sem = date_time_to_semantic(max);
1136 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1137 return Err(format!("{} is above maximum {}", dt, max));
1138 }
1139 }
1140 Ok(())
1141 }
1142 (
1143 TypeSpecification::Time {
1144 minimum, maximum, ..
1145 },
1146 ValueKind::Time(t),
1147 ) => {
1148 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1149 use std::cmp::Ordering;
1150 if let Some(min) = minimum {
1151 let min_sem = time_to_semantic(min);
1152 if compare_semantic_times(t, &min_sem) == Ordering::Less {
1153 return Err(format!("{} is below minimum {}", t, min));
1154 }
1155 }
1156 if let Some(max) = maximum {
1157 let max_sem = time_to_semantic(max);
1158 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1159 return Err(format!("{} is above maximum {}", t, max));
1160 }
1161 }
1162 Ok(())
1163 }
1164 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1165 | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1166 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1167 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
1168 | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1169 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1170 | (TypeSpecification::Veto { .. }, _)
1171 | (TypeSpecification::Undetermined, _) => Ok(()),
1172 (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
1173 "BUG: validate_value_against_type called with mismatched type/value: \
1174 spec={:?}, value={:?} — typing must be enforced before validation",
1175 spec, value_kind
1176 ),
1177 (_, _) => Ok(()),
1178 }
1179}
1180
1181pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1182 let mut errors = Vec::new();
1183
1184 for (data_path, data_definition) in &plan.data {
1185 let (expected_type, lit) = match data_definition {
1186 DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1187 DataDefinition::TypeDeclaration { .. }
1188 | DataDefinition::Import { .. }
1189 | DataDefinition::Reference { .. }
1190 | DataDefinition::Violated { .. } => continue,
1191 };
1192
1193 if let Err(msg) = validate_value_against_type(expected_type, lit) {
1194 let source = data_definition.source().clone();
1195 errors.push(Error::validation(
1196 format!(
1197 "Invalid value for data {} (expected {}): {}",
1198 data_path,
1199 expected_type.name().as_str(),
1200 msg
1201 ),
1202 Some(source),
1203 None::<String>,
1204 ));
1205 }
1206 }
1207
1208 errors
1209}
1210
1211fn collect_unit_conversion_targets(expression: &Expression, units: &mut BTreeSet<String>) {
1212 use crate::planning::semantics::{ExpressionKind, SemanticConversionTarget};
1213 match &expression.kind {
1214 ExpressionKind::UnitConversion(inner, SemanticConversionTarget::Unit { unit_name }) => {
1215 units.insert(unit_name.clone());
1216 collect_unit_conversion_targets(inner, units);
1217 }
1218 ExpressionKind::UnitConversion(inner, SemanticConversionTarget::Type(_))
1219 | ExpressionKind::LogicalNegation(inner, _)
1220 | ExpressionKind::MathematicalComputation(_, inner)
1221 | ExpressionKind::PastFutureRange(_, inner) => {
1222 collect_unit_conversion_targets(inner, units);
1223 }
1224 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
1225 collect_unit_conversion_targets(left, units);
1226 collect_unit_conversion_targets(right, units);
1227 }
1228 ExpressionKind::Arithmetic(left, _, right)
1229 | ExpressionKind::Comparison(left, _, right)
1230 | ExpressionKind::RangeLiteral(left, right)
1231 | ExpressionKind::RangeContainment(left, right) => {
1232 collect_unit_conversion_targets(left, units);
1233 collect_unit_conversion_targets(right, units);
1234 }
1235 ExpressionKind::DateRelative(_, date_expr) => {
1236 collect_unit_conversion_targets(date_expr, units);
1237 }
1238 ExpressionKind::DateCalendar(_, _, date_expr) => {
1239 collect_unit_conversion_targets(date_expr, units);
1240 }
1241 ExpressionKind::ResultIsVeto(operand) => {
1242 collect_unit_conversion_targets(operand, units);
1243 }
1244 ExpressionKind::Literal(_)
1245 | ExpressionKind::DataPath(_)
1246 | ExpressionKind::RulePath(_)
1247 | ExpressionKind::Veto(_)
1248 | ExpressionKind::Now => {}
1249 }
1250}
1251
1252pub(crate) fn validate_unit_index_references(plan: &ExecutionPlan) -> Result<(), Error> {
1253 let mut required_units = BTreeSet::new();
1254 for rule in &plan.rules {
1255 for branch in &rule.normalized_branches {
1256 collect_unit_conversion_targets(&branch.result, &mut required_units);
1257 collect_unit_conversion_targets(&branch.condition, &mut required_units);
1258 }
1259 }
1260 for unit_name in required_units {
1261 if plan.resolved_types.unit_index.contains_key(&unit_name) {
1262 continue;
1263 }
1264 return Err(Error::validation(
1265 format!("Unknown unit '{unit_name}' in execution plan unit index."),
1266 None::<Source>,
1267 Some(plan.spec_name.clone()),
1268 ));
1269 }
1270 Ok(())
1271}
1272
1273#[derive(Debug, Clone, Serialize, Deserialize)]
1284pub struct ExecutionPlanSerialized {
1285 pub spec_name: String,
1286 #[serde(skip_serializing_if = "Option::is_none", default)]
1287 pub commentary: Option<String>,
1288 #[serde(
1289 serialize_with = "serialize_resolved_data_value_map",
1290 deserialize_with = "deserialize_resolved_data_value_map"
1291 )]
1292 pub data: IndexMap<DataPath, DataDefinition>,
1293 #[serde(default)]
1294 pub rules: Vec<ExecutableRule>,
1295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1296 pub reference_evaluation_order: Vec<DataPath>,
1297 #[serde(default)]
1298 pub meta: HashMap<String, MetaValue>,
1299 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1302 pub unit_index: HashMap<String, Arc<LemmaType>>,
1303 pub effective: EffectiveDate,
1304 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1305 pub sources: SpecSources,
1306}
1307
1308impl From<&ExecutionPlan> for ExecutionPlanSerialized {
1309 fn from(plan: &ExecutionPlan) -> Self {
1310 Self {
1311 spec_name: plan.spec_name.clone(),
1312 commentary: plan.commentary.clone(),
1313 data: plan.data.clone(),
1314 rules: plan.rules.clone(),
1315 reference_evaluation_order: plan.reference_evaluation_order.clone(),
1316 meta: plan.meta.clone(),
1317 unit_index: plan.resolved_types.unit_index.clone(),
1318 effective: plan.effective.clone(),
1319 sources: plan.sources.clone(),
1320 }
1321 }
1322}
1323
1324impl TryFrom<ExecutionPlanSerialized> for ExecutionPlan {
1325 type Error = crate::Error;
1326
1327 fn try_from(serialized: ExecutionPlanSerialized) -> Result<Self, Self::Error> {
1328 let signature_index = crate::planning::graph::build_signature_index(
1329 &serialized.spec_name,
1330 &serialized.unit_index,
1331 )?;
1332 Ok(Self {
1333 spec_name: serialized.spec_name,
1334 commentary: serialized.commentary,
1335 data: serialized.data,
1336 rules: serialized.rules,
1337 reference_evaluation_order: serialized.reference_evaluation_order,
1338 meta: serialized.meta,
1339 resolved_types: ResolvedSpecTypes {
1340 unit_index: serialized.unit_index,
1341 ..ResolvedSpecTypes::default()
1342 },
1343 signature_index,
1344 effective: serialized.effective,
1345 sources: serialized.sources,
1346 })
1347 }
1348}
1349
1350fn serialize_resolved_data_value_map<S>(
1351 map: &IndexMap<DataPath, DataDefinition>,
1352 serializer: S,
1353) -> Result<S::Ok, S::Error>
1354where
1355 S: Serializer,
1356{
1357 let entries: Vec<(&DataPath, &DataDefinition)> = map.iter().collect();
1358 entries.serialize(serializer)
1359}
1360
1361fn deserialize_resolved_data_value_map<'de, D>(
1362 deserializer: D,
1363) -> Result<IndexMap<DataPath, DataDefinition>, D::Error>
1364where
1365 D: Deserializer<'de>,
1366{
1367 let entries: Vec<(DataPath, DataDefinition)> = Vec::deserialize(deserializer)?;
1368 Ok(entries.into_iter().collect())
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373 use super::*;
1374 use crate::computation::rational::{rational_zero, RationalInteger};
1375 use crate::parsing::ast::DateTimeValue;
1376 use crate::planning::semantics::{
1377 primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
1378 };
1379 use crate::Engine;
1380 use serde_json;
1381 use std::str::FromStr;
1382 use std::sync::Arc;
1383
1384 fn default_limits() -> ResourceLimits {
1385 ResourceLimits::default()
1386 }
1387
1388 fn roundtrip_execution_plan(plan: &ExecutionPlan) -> ExecutionPlan {
1389 let serialized = ExecutionPlanSerialized::from(plan);
1390 let json = serde_json::to_string(&serialized).expect("Should serialize");
1391 let back: ExecutionPlanSerialized =
1392 serde_json::from_str(&json).expect("Should deserialize");
1393 ExecutionPlan::try_from(back).expect("Should reconstruct")
1394 }
1395
1396 fn input_data(pairs: &[(&str, &str)]) -> HashMap<String, DataValueInput> {
1397 pairs
1398 .iter()
1399 .map(|(k, v)| (k.to_string(), DataValueInput::convenience(*v)))
1400 .collect()
1401 }
1402
1403 #[test]
1404 fn test_with_raw_values() {
1405 let mut engine = Engine::new();
1406 engine
1407 .load(
1408 r#"
1409 spec test
1410 data age: number -> default 25
1411 "#,
1412 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1413 "test.lemma",
1414 ))),
1415 )
1416 .unwrap();
1417
1418 let now = DateTimeValue::now();
1419 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1420 let data_path = DataPath::new(vec![], "age".to_string());
1421
1422 let values = input_data(&[("age", "30")]);
1423
1424 let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1425 let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1426 match &updated_value.value {
1427 crate::planning::semantics::ValueKind::Number(n) => {
1428 assert_eq!(*n, RationalInteger::new(30, 1));
1429 }
1430 other => panic!("Expected number literal, got {:?}", other),
1431 }
1432 }
1433
1434 #[test]
1435 fn test_with_raw_values_type_mismatch() {
1436 let mut engine = Engine::new();
1437 engine
1438 .load(
1439 r#"
1440 spec test
1441 data age: number
1442 "#,
1443 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1444 "test.lemma",
1445 ))),
1446 )
1447 .unwrap();
1448
1449 let now = DateTimeValue::now();
1450 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1451
1452 let values = input_data(&[("age", "thirty")]);
1453
1454 let updated = plan.set_data_values(values, &default_limits()).unwrap();
1455 let data_path = DataPath::new(vec![], "age".to_string());
1456 match updated.data.get(&data_path) {
1457 Some(crate::planning::semantics::DataDefinition::Violated { reason, .. }) => {
1458 assert!(
1459 reason.contains("number"),
1460 "type mismatch must record violation reason, got: {reason}"
1461 );
1462 }
1463 other => panic!("expected Violated data for age=thirty, got: {other:?}"),
1464 }
1465 }
1466
1467 #[test]
1468 fn test_with_raw_values_unknown_data() {
1469 let mut engine = Engine::new();
1470 engine
1471 .load(
1472 r#"
1473 spec test
1474 data known: number
1475 "#,
1476 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1477 "test.lemma",
1478 ))),
1479 )
1480 .unwrap();
1481
1482 let now = DateTimeValue::now();
1483 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1484
1485 let values = input_data(&[("unknown", "30")]);
1486
1487 assert!(plan.set_data_values(values, &default_limits()).is_err());
1488 }
1489
1490 #[test]
1491 fn test_with_raw_values_nested() {
1492 let mut engine = Engine::new();
1493 engine
1494 .load(
1495 r#"
1496 spec private
1497 data base_price: number
1498
1499 spec test
1500 uses rules: private
1501 "#,
1502 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1503 "test.lemma",
1504 ))),
1505 )
1506 .unwrap();
1507
1508 let now = DateTimeValue::now();
1509 let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1510
1511 let values = input_data(&[("rules.base_price", "100")]);
1512
1513 let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1514 let data_path = DataPath {
1515 segments: vec![PathSegment {
1516 data: "rules".to_string(),
1517 spec: "private".to_string(),
1518 }],
1519 data: "base_price".to_string(),
1520 };
1521 let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1522 match &updated_value.value {
1523 crate::planning::semantics::ValueKind::Number(n) => {
1524 assert_eq!(*n, RationalInteger::new(100, 1));
1525 }
1526 other => panic!("Expected number literal, got {:?}", other),
1527 }
1528 }
1529
1530 fn test_source() -> Source {
1531 use crate::parsing::ast::Span;
1532 Source::new(
1533 crate::parsing::source::SourceType::Volatile,
1534 Span {
1535 start: 0,
1536 end: 0,
1537 line: 1,
1538 col: 0,
1539 },
1540 )
1541 }
1542
1543 fn create_literal_expr(value: LiteralValue) -> Expression {
1544 Expression::new(
1545 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
1546 test_source(),
1547 )
1548 }
1549
1550 fn create_data_path_expr(path: DataPath) -> Expression {
1551 Expression::new(
1552 crate::planning::semantics::ExpressionKind::DataPath(path),
1553 test_source(),
1554 )
1555 }
1556
1557 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
1558 LiteralValue::number_from_decimal(n)
1559 }
1560
1561 fn create_boolean_literal(b: bool) -> LiteralValue {
1562 LiteralValue::from_bool(b)
1563 }
1564
1565 fn create_text_literal(s: String) -> LiteralValue {
1566 LiteralValue::text(s)
1567 }
1568
1569 #[test]
1570 fn with_values_should_enforce_number_maximum_constraint() {
1571 let data_path = DataPath::new(vec![], "x".to_string());
1574
1575 let max10 = crate::planning::semantics::LemmaType::primitive(
1576 crate::planning::semantics::TypeSpecification::Number {
1577 minimum: None,
1578 maximum: Some(RationalInteger::new(10, 1)),
1579 decimals: None,
1580 help: String::new(),
1581 },
1582 );
1583 let source = Source::new(
1584 crate::parsing::source::SourceType::Volatile,
1585 crate::parsing::ast::Span {
1586 start: 0,
1587 end: 0,
1588 line: 1,
1589 col: 0,
1590 },
1591 );
1592 let mut data = IndexMap::new();
1593 data.insert(
1594 data_path.clone(),
1595 crate::planning::semantics::DataDefinition::Value {
1596 value: crate::planning::semantics::LiteralValue::number_with_type(
1597 0.into(),
1598 Arc::new(max10.clone()),
1599 ),
1600 source: source.clone(),
1601 },
1602 );
1603
1604 let plan = ExecutionPlan {
1605 spec_name: "test".to_string(),
1606 commentary: None,
1607 data,
1608 rules: Vec::new(),
1609 reference_evaluation_order: Vec::new(),
1610 meta: HashMap::new(),
1611 resolved_types: ResolvedSpecTypes::default(),
1612 signature_index: HashMap::new(),
1613 effective: EffectiveDate::Origin,
1614 sources: Vec::new(),
1615 };
1616
1617 let values = input_data(&[("x", "11")]);
1618
1619 let updated = plan.set_data_values(values, &default_limits()).unwrap();
1620 match updated.data.get(&data_path) {
1621 Some(crate::planning::semantics::DataDefinition::Violated { reason, .. }) => {
1622 assert!(
1623 reason.contains("maximum") || reason.contains("10"),
1624 "x=11 must violate maximum 10, got: {reason}"
1625 );
1626 }
1627 other => panic!("expected Violated data for x=11, got: {other:?}"),
1628 }
1629 }
1630
1631 #[test]
1632 fn with_values_should_enforce_text_enum_options() {
1633 let data_path = DataPath::new(vec![], "tier".to_string());
1635
1636 let tier = crate::planning::semantics::LemmaType::primitive(
1637 crate::planning::semantics::TypeSpecification::Text {
1638 length: None,
1639 options: vec!["silver".to_string(), "gold".to_string()],
1640 help: String::new(),
1641 },
1642 );
1643 let source = Source::new(
1644 crate::parsing::source::SourceType::Volatile,
1645 crate::parsing::ast::Span {
1646 start: 0,
1647 end: 0,
1648 line: 1,
1649 col: 0,
1650 },
1651 );
1652 let mut data = IndexMap::new();
1653 data.insert(
1654 data_path.clone(),
1655 crate::planning::semantics::DataDefinition::Value {
1656 value: crate::planning::semantics::LiteralValue::text_with_type(
1657 "silver".to_string(),
1658 Arc::new(tier.clone()),
1659 ),
1660 source,
1661 },
1662 );
1663
1664 let plan = ExecutionPlan {
1665 spec_name: "test".to_string(),
1666 commentary: None,
1667 data,
1668 rules: Vec::new(),
1669 reference_evaluation_order: Vec::new(),
1670 meta: HashMap::new(),
1671 resolved_types: ResolvedSpecTypes::default(),
1672 signature_index: HashMap::new(),
1673 effective: EffectiveDate::Origin,
1674 sources: Vec::new(),
1675 };
1676
1677 let values = input_data(&[("tier", "platinum")]);
1678
1679 let updated = plan.set_data_values(values, &default_limits()).unwrap();
1680 match updated.data.get(&data_path) {
1681 Some(crate::planning::semantics::DataDefinition::Violated { reason, .. }) => {
1682 assert!(
1683 reason.contains("allowed options") || reason.contains("platinum"),
1684 "invalid enum must record violation, got: {reason}"
1685 );
1686 }
1687 other => panic!("expected Violated data for tier=platinum, got: {other:?}"),
1688 }
1689 }
1690
1691 #[test]
1692 fn with_values_should_enforce_quantity_decimals() {
1693 let data_path = DataPath::new(vec![], "price".to_string());
1696
1697 let money = crate::planning::semantics::LemmaType::primitive(
1698 crate::planning::semantics::TypeSpecification::Quantity {
1699 minimum: None,
1700 maximum: None,
1701 decimals: Some(2),
1702 units: crate::planning::semantics::QuantityUnits::from(vec![
1703 crate::planning::semantics::QuantityUnit::from_decimal_factor(
1704 "eur".to_string(),
1705 rust_decimal::Decimal::from_str("1.0").unwrap(),
1706 Vec::new(),
1707 )
1708 .expect("eur unit factor must be exact decimal"),
1709 ]),
1710 traits: Vec::new(),
1711 decomposition: None,
1712 help: String::new(),
1713 },
1714 );
1715 let source = Source::new(
1716 crate::parsing::source::SourceType::Volatile,
1717 crate::parsing::ast::Span {
1718 start: 0,
1719 end: 0,
1720 line: 1,
1721 col: 0,
1722 },
1723 );
1724 let mut data = IndexMap::new();
1725 data.insert(
1726 data_path.clone(),
1727 crate::planning::semantics::DataDefinition::Value {
1728 value: crate::planning::semantics::LiteralValue::quantity_with_type(
1729 rational_zero(),
1730 "eur".to_string(),
1731 Arc::new(money.clone()),
1732 ),
1733 source,
1734 },
1735 );
1736
1737 let plan = ExecutionPlan {
1738 spec_name: "test".to_string(),
1739 commentary: None,
1740 data,
1741 rules: Vec::new(),
1742 reference_evaluation_order: Vec::new(),
1743 meta: HashMap::new(),
1744 resolved_types: ResolvedSpecTypes::default(),
1745 signature_index: HashMap::new(),
1746 effective: EffectiveDate::Origin,
1747 sources: Vec::new(),
1748 };
1749
1750 let values = input_data(&[("price", "1.234 eur")]);
1751
1752 let updated = plan.set_data_values(values, &default_limits()).unwrap();
1753 match updated.data.get(&data_path) {
1754 Some(crate::planning::semantics::DataDefinition::Violated { reason, .. }) => {
1755 assert!(
1756 reason.contains("decimals") || reason.contains("decimal"),
1757 "1.234 eur must violate decimals=2, got: {reason}"
1758 );
1759 }
1760 other => panic!("expected Violated data for price=1.234 eur, got: {other:?}"),
1761 }
1762 }
1763
1764 #[test]
1765 fn test_serialize_deserialize_execution_plan() {
1766 let data_path = DataPath {
1767 segments: vec![],
1768 data: "age".to_string(),
1769 };
1770 let mut data = IndexMap::new();
1771 data.insert(
1772 data_path.clone(),
1773 crate::planning::semantics::DataDefinition::Value {
1774 value: create_number_literal(0.into()),
1775 source: test_source(),
1776 },
1777 );
1778 let plan = ExecutionPlan {
1779 spec_name: "test".to_string(),
1780 commentary: None,
1781 data,
1782 rules: Vec::new(),
1783 reference_evaluation_order: Vec::new(),
1784 meta: HashMap::new(),
1785 resolved_types: ResolvedSpecTypes::default(),
1786 signature_index: HashMap::new(),
1787 effective: EffectiveDate::Origin,
1788 sources: Vec::new(),
1789 };
1790
1791 let deserialized = roundtrip_execution_plan(&plan);
1792
1793 assert_eq!(deserialized.spec_name, plan.spec_name);
1794 assert_eq!(deserialized.data.len(), plan.data.len());
1795 assert_eq!(deserialized.rules.len(), plan.rules.len());
1796 }
1797
1798 #[test]
1799 fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
1800 let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
1801 let imported_type = crate::planning::semantics::LemmaType::new(
1802 "salary".to_string(),
1803 TypeSpecification::quantity(),
1804 crate::planning::semantics::TypeExtends::Custom {
1805 parent: "money".to_string(),
1806 family: "money".to_string(),
1807 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
1808 spec: Arc::clone(&dep_spec),
1809 },
1810 },
1811 );
1812
1813 let salary_path = DataPath::new(vec![], "salary".to_string());
1814 let mut data = IndexMap::new();
1815 data.insert(
1816 salary_path,
1817 crate::planning::semantics::DataDefinition::TypeDeclaration {
1818 resolved_type: Arc::new(imported_type),
1819 declared_default: None,
1820 source: test_source(),
1821 },
1822 );
1823
1824 let plan = ExecutionPlan {
1825 spec_name: "test".to_string(),
1826 commentary: None,
1827 data,
1828 rules: Vec::new(),
1829 reference_evaluation_order: Vec::new(),
1830 meta: HashMap::new(),
1831 resolved_types: ResolvedSpecTypes::default(),
1832 signature_index: HashMap::new(),
1833 effective: EffectiveDate::Origin,
1834 sources: Vec::new(),
1835 };
1836
1837 let deserialized = roundtrip_execution_plan(&plan);
1838
1839 let recovered = deserialized
1840 .data
1841 .get(&DataPath::new(vec![], "salary".to_string()))
1842 .and_then(|d| d.schema_type())
1843 .expect("salary type should be present in plan.data");
1844 match &recovered.extends {
1845 crate::planning::semantics::TypeExtends::Custom {
1846 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
1847 ..
1848 } => {
1849 assert_eq!(spec.name, "examples");
1850 }
1851 other => panic!(
1852 "Expected imported defining_spec after round-trip, got {:?}",
1853 other
1854 ),
1855 }
1856 }
1857
1858 #[test]
1859 fn test_serialize_deserialize_plan_with_rules() {
1860 use crate::planning::semantics::ExpressionKind;
1861
1862 let age_path = DataPath::new(vec![], "age".to_string());
1863 let mut data = IndexMap::new();
1864 data.insert(
1865 age_path.clone(),
1866 crate::planning::semantics::DataDefinition::Value {
1867 value: create_number_literal(0.into()),
1868 source: test_source(),
1869 },
1870 );
1871 let mut plan = ExecutionPlan {
1872 spec_name: "test".to_string(),
1873 commentary: None,
1874 data,
1875 rules: Vec::new(),
1876 reference_evaluation_order: Vec::new(),
1877 meta: HashMap::new(),
1878 resolved_types: ResolvedSpecTypes::default(),
1879 signature_index: HashMap::new(),
1880 effective: EffectiveDate::Origin,
1881 sources: Vec::new(),
1882 };
1883
1884 let rule = ExecutableRule {
1885 path: RulePath::new(vec![], "can_drive".to_string()),
1886 name: "can_drive".to_string(),
1887 branches: vec![{
1888 let result = create_literal_expr(create_boolean_literal(true));
1889 let condition = Expression::new(
1890 ExpressionKind::Comparison(
1891 Arc::new(create_data_path_expr(age_path.clone())),
1892 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1893 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1894 ),
1895 test_source(),
1896 );
1897 Branch {
1898 condition: Some(condition.clone()),
1899 result: result.clone(),
1900 source: test_source(),
1901 }
1902 }],
1903 normalized_branches: vec![{
1904 let result = create_literal_expr(create_boolean_literal(true));
1905 let condition = Expression::new(
1906 ExpressionKind::Comparison(
1907 Arc::new(create_data_path_expr(age_path.clone())),
1908 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1909 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1910 ),
1911 test_source(),
1912 );
1913 NormalizedBranch { condition, result }
1914 }],
1915 needs_data: BTreeSet::from([age_path]),
1916 source: test_source(),
1917 rule_type: Arc::new(primitive_boolean().clone()),
1918 };
1919
1920 plan.rules.push(rule);
1921
1922 let deserialized = roundtrip_execution_plan(&plan);
1923
1924 assert_eq!(deserialized.spec_name, plan.spec_name);
1925 assert_eq!(deserialized.data.len(), plan.data.len());
1926 assert_eq!(deserialized.rules.len(), plan.rules.len());
1927 assert_eq!(deserialized.rules[0].name, "can_drive");
1928 assert_eq!(deserialized.rules[0].branches.len(), 1);
1929 assert_eq!(deserialized.rules[0].needs_data.len(), 1);
1930 }
1931
1932 #[test]
1933 fn test_serialize_deserialize_plan_with_nested_data_paths() {
1934 use crate::planning::semantics::PathSegment;
1935 let data_path = DataPath {
1936 segments: vec![PathSegment {
1937 data: "employee".to_string(),
1938 spec: "private".to_string(),
1939 }],
1940 data: "salary".to_string(),
1941 };
1942
1943 let mut data = IndexMap::new();
1944 data.insert(
1945 data_path.clone(),
1946 crate::planning::semantics::DataDefinition::Value {
1947 value: create_number_literal(0.into()),
1948 source: test_source(),
1949 },
1950 );
1951 let plan = ExecutionPlan {
1952 spec_name: "test".to_string(),
1953 commentary: None,
1954 data,
1955 rules: Vec::new(),
1956 reference_evaluation_order: Vec::new(),
1957 meta: HashMap::new(),
1958 resolved_types: ResolvedSpecTypes::default(),
1959 signature_index: HashMap::new(),
1960 effective: EffectiveDate::Origin,
1961 sources: Vec::new(),
1962 };
1963
1964 let deserialized = roundtrip_execution_plan(&plan);
1965
1966 assert_eq!(deserialized.data.len(), 1);
1967 let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
1968 assert_eq!(deserialized_path.segments.len(), 1);
1969 assert_eq!(deserialized_path.segments[0].data, "employee");
1970 assert_eq!(deserialized_path.data, "salary");
1971 }
1972
1973 #[test]
1974 fn test_serialize_deserialize_plan_with_multiple_data_types() {
1975 let name_path = DataPath::new(vec![], "name".to_string());
1976 let age_path = DataPath::new(vec![], "age".to_string());
1977 let active_path = DataPath::new(vec![], "active".to_string());
1978
1979 let mut data = IndexMap::new();
1980 data.insert(
1981 name_path.clone(),
1982 crate::planning::semantics::DataDefinition::Value {
1983 value: create_text_literal("Alice".to_string()),
1984 source: test_source(),
1985 },
1986 );
1987 data.insert(
1988 age_path.clone(),
1989 crate::planning::semantics::DataDefinition::Value {
1990 value: create_number_literal(30.into()),
1991 source: test_source(),
1992 },
1993 );
1994 data.insert(
1995 active_path.clone(),
1996 crate::planning::semantics::DataDefinition::Value {
1997 value: create_boolean_literal(true),
1998 source: test_source(),
1999 },
2000 );
2001
2002 let plan = ExecutionPlan {
2003 spec_name: "test".to_string(),
2004 commentary: None,
2005 data,
2006 rules: Vec::new(),
2007 reference_evaluation_order: Vec::new(),
2008 meta: HashMap::new(),
2009 resolved_types: ResolvedSpecTypes::default(),
2010 signature_index: HashMap::new(),
2011 effective: EffectiveDate::Origin,
2012 sources: Vec::new(),
2013 };
2014
2015 let deserialized = roundtrip_execution_plan(&plan);
2016
2017 assert_eq!(deserialized.data.len(), 3);
2018
2019 assert_eq!(
2020 deserialized.get_data_value(&name_path).unwrap().value,
2021 crate::planning::semantics::ValueKind::Text("Alice".to_string())
2022 );
2023 assert_eq!(
2024 deserialized.get_data_value(&age_path).unwrap().value,
2025 crate::planning::semantics::ValueKind::Number(30.into())
2026 );
2027 assert_eq!(
2028 deserialized.get_data_value(&active_path).unwrap().value,
2029 crate::planning::semantics::ValueKind::Boolean(true)
2030 );
2031 }
2032
2033 #[test]
2034 fn test_serialize_deserialize_plan_with_multiple_branches() {
2035 use crate::planning::semantics::ExpressionKind;
2036
2037 let points_path = DataPath::new(vec![], "points".to_string());
2038 let mut data = IndexMap::new();
2039 data.insert(
2040 points_path.clone(),
2041 crate::planning::semantics::DataDefinition::Value {
2042 value: create_number_literal(0.into()),
2043 source: test_source(),
2044 },
2045 );
2046 let mut plan = ExecutionPlan {
2047 spec_name: "test".to_string(),
2048 commentary: None,
2049 data,
2050 rules: Vec::new(),
2051 reference_evaluation_order: Vec::new(),
2052 meta: HashMap::new(),
2053 resolved_types: ResolvedSpecTypes::default(),
2054 signature_index: HashMap::new(),
2055 effective: EffectiveDate::Origin,
2056 sources: Vec::new(),
2057 };
2058
2059 let rule = ExecutableRule {
2060 path: RulePath::new(vec![], "tier".to_string()),
2061 name: "tier".to_string(),
2062 branches: vec![
2063 {
2064 let result = create_literal_expr(create_text_literal("bronze".to_string()));
2065 Branch {
2066 condition: None,
2067 result: result.clone(),
2068 source: test_source(),
2069 }
2070 },
2071 {
2072 let result = create_literal_expr(create_text_literal("silver".to_string()));
2073 Branch {
2074 condition: Some(Expression::new(
2075 ExpressionKind::Comparison(
2076 Arc::new(create_data_path_expr(points_path.clone())),
2077 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2078 Arc::new(create_literal_expr(create_number_literal(100.into()))),
2079 ),
2080 test_source(),
2081 )),
2082 result: result.clone(),
2083 source: test_source(),
2084 }
2085 },
2086 {
2087 let result = create_literal_expr(create_text_literal("gold".to_string()));
2088 Branch {
2089 condition: Some(Expression::new(
2090 ExpressionKind::Comparison(
2091 Arc::new(create_data_path_expr(points_path.clone())),
2092 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2093 Arc::new(create_literal_expr(create_number_literal(500.into()))),
2094 ),
2095 test_source(),
2096 )),
2097 result: result.clone(),
2098 source: test_source(),
2099 }
2100 },
2101 ],
2102 normalized_branches: vec![
2103 NormalizedBranch {
2104 condition: create_literal_expr(create_boolean_literal(true)),
2105 result: create_literal_expr(create_text_literal("bronze".to_string())),
2106 },
2107 NormalizedBranch {
2108 condition: create_literal_expr(create_boolean_literal(true)),
2109 result: create_literal_expr(create_text_literal("silver".to_string())),
2110 },
2111 NormalizedBranch {
2112 condition: create_literal_expr(create_boolean_literal(true)),
2113 result: create_literal_expr(create_text_literal("gold".to_string())),
2114 },
2115 ],
2116 needs_data: BTreeSet::from([points_path]),
2117 source: test_source(),
2118 rule_type: Arc::new(primitive_text().clone()),
2119 };
2120
2121 plan.rules.push(rule);
2122
2123 let deserialized = roundtrip_execution_plan(&plan);
2124
2125 assert_eq!(deserialized.rules.len(), 1);
2126 assert_eq!(deserialized.rules[0].branches.len(), 3);
2127 assert!(deserialized.rules[0].branches[0].condition.is_none());
2128 assert!(deserialized.rules[0].branches[1].condition.is_some());
2129 assert!(deserialized.rules[0].branches[2].condition.is_some());
2130 }
2131
2132 #[test]
2133 fn test_serialize_deserialize_empty_plan() {
2134 let plan = ExecutionPlan {
2135 spec_name: "empty".to_string(),
2136 commentary: None,
2137 data: IndexMap::new(),
2138 rules: Vec::new(),
2139 reference_evaluation_order: Vec::new(),
2140 meta: HashMap::new(),
2141 resolved_types: ResolvedSpecTypes::default(),
2142 signature_index: HashMap::new(),
2143 effective: EffectiveDate::Origin,
2144 sources: Vec::new(),
2145 };
2146
2147 let deserialized = roundtrip_execution_plan(&plan);
2148
2149 assert_eq!(deserialized.spec_name, "empty");
2150 assert_eq!(deserialized.data.len(), 0);
2151 assert_eq!(deserialized.rules.len(), 0);
2152 }
2153
2154 #[test]
2155 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
2156 use crate::planning::semantics::ExpressionKind;
2157
2158 let x_path = DataPath::new(vec![], "x".to_string());
2159 let mut data = IndexMap::new();
2160 data.insert(
2161 x_path.clone(),
2162 crate::planning::semantics::DataDefinition::Value {
2163 value: create_number_literal(0.into()),
2164 source: test_source(),
2165 },
2166 );
2167 let mut plan = ExecutionPlan {
2168 spec_name: "test".to_string(),
2169 commentary: None,
2170 data,
2171 rules: Vec::new(),
2172 reference_evaluation_order: Vec::new(),
2173 meta: HashMap::new(),
2174 resolved_types: ResolvedSpecTypes::default(),
2175 signature_index: HashMap::new(),
2176 effective: EffectiveDate::Origin,
2177 sources: Vec::new(),
2178 };
2179
2180 let rule = ExecutableRule {
2181 path: RulePath::new(vec![], "doubled".to_string()),
2182 name: "doubled".to_string(),
2183 branches: vec![{
2184 let result = Expression::new(
2185 ExpressionKind::Arithmetic(
2186 Arc::new(create_data_path_expr(x_path.clone())),
2187 crate::parsing::ast::ArithmeticComputation::Multiply,
2188 Arc::new(create_literal_expr(create_number_literal(2.into()))),
2189 ),
2190 test_source(),
2191 );
2192 Branch {
2193 condition: None,
2194 result: result.clone(),
2195 source: test_source(),
2196 }
2197 }],
2198 normalized_branches: vec![{
2199 let result = Expression::new(
2200 ExpressionKind::Arithmetic(
2201 Arc::new(create_data_path_expr(x_path.clone())),
2202 crate::parsing::ast::ArithmeticComputation::Multiply,
2203 Arc::new(create_literal_expr(create_number_literal(2.into()))),
2204 ),
2205 test_source(),
2206 );
2207 NormalizedBranch {
2208 condition: create_literal_expr(create_boolean_literal(true)),
2209 result,
2210 }
2211 }],
2212 needs_data: BTreeSet::from([x_path]),
2213 source: test_source(),
2214 rule_type: Arc::new(crate::planning::semantics::primitive_number().clone()),
2215 };
2216
2217 plan.rules.push(rule);
2218
2219 let deserialized = roundtrip_execution_plan(&plan);
2220
2221 assert_eq!(deserialized.rules.len(), 1);
2222 match &deserialized.rules[0].branches[0].result.kind {
2223 ExpressionKind::Arithmetic(left, op, right) => {
2224 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
2225 match &left.kind {
2226 ExpressionKind::DataPath(_) => {}
2227 _ => panic!("Expected DataPath in left operand"),
2228 }
2229 match &right.kind {
2230 ExpressionKind::Literal(_) => {}
2231 _ => panic!("Expected Literal in right operand"),
2232 }
2233 }
2234 _ => panic!("Expected Arithmetic expression"),
2235 }
2236 }
2237
2238 #[test]
2239 fn test_serialize_deserialize_round_trip_equality() {
2240 use crate::planning::semantics::ExpressionKind;
2241
2242 let age_path = DataPath::new(vec![], "age".to_string());
2243 let mut data = IndexMap::new();
2244 data.insert(
2245 age_path.clone(),
2246 crate::planning::semantics::DataDefinition::Value {
2247 value: create_number_literal(0.into()),
2248 source: test_source(),
2249 },
2250 );
2251 let mut plan = ExecutionPlan {
2252 spec_name: "test".to_string(),
2253 commentary: None,
2254 data,
2255 rules: Vec::new(),
2256 reference_evaluation_order: Vec::new(),
2257 meta: HashMap::new(),
2258 resolved_types: ResolvedSpecTypes::default(),
2259 signature_index: HashMap::new(),
2260 effective: EffectiveDate::Origin,
2261 sources: Vec::new(),
2262 };
2263
2264 let rule = ExecutableRule {
2265 path: RulePath::new(vec![], "is_adult".to_string()),
2266 name: "is_adult".to_string(),
2267 branches: vec![{
2268 let result = create_literal_expr(create_boolean_literal(true));
2269 let condition = Expression::new(
2270 ExpressionKind::Comparison(
2271 Arc::new(create_data_path_expr(age_path.clone())),
2272 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2273 Arc::new(create_literal_expr(create_number_literal(18.into()))),
2274 ),
2275 test_source(),
2276 );
2277 Branch {
2278 condition: Some(condition.clone()),
2279 result: result.clone(),
2280 source: test_source(),
2281 }
2282 }],
2283 normalized_branches: vec![{
2284 let result = create_literal_expr(create_boolean_literal(true));
2285 let condition = Expression::new(
2286 ExpressionKind::Comparison(
2287 Arc::new(create_data_path_expr(age_path.clone())),
2288 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2289 Arc::new(create_literal_expr(create_number_literal(18.into()))),
2290 ),
2291 test_source(),
2292 );
2293 NormalizedBranch { condition, result }
2294 }],
2295 needs_data: BTreeSet::from([age_path]),
2296 source: test_source(),
2297 rule_type: Arc::new(primitive_boolean().clone()),
2298 };
2299
2300 plan.rules.push(rule);
2301
2302 let deserialized = roundtrip_execution_plan(&plan);
2303 let deserialized2 = roundtrip_execution_plan(&deserialized);
2304
2305 assert_eq!(deserialized2.spec_name, plan.spec_name);
2306 assert_eq!(deserialized2.data.len(), plan.data.len());
2307 assert_eq!(deserialized2.rules.len(), plan.rules.len());
2308 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
2309 assert_eq!(
2310 deserialized2.rules[0].branches.len(),
2311 plan.rules[0].branches.len()
2312 );
2313 }
2314
2315 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
2316 ExecutionPlan {
2317 spec_name: "s".into(),
2318 commentary: None,
2319 data: IndexMap::new(),
2320 rules: Vec::new(),
2321 reference_evaluation_order: Vec::new(),
2322 meta: HashMap::new(),
2323 resolved_types: ResolvedSpecTypes::default(),
2324 signature_index: HashMap::new(),
2325 effective,
2326 sources: Vec::new(),
2327 }
2328 }
2329
2330 #[test]
2331 fn plan_at_exact_boundary_selects_later_slice() {
2332 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2333
2334 let june = DateTimeValue {
2335 year: 2025,
2336 month: 6,
2337 day: 1,
2338 hour: 0,
2339 minute: 0,
2340 second: 0,
2341 microsecond: 0,
2342 timezone: None,
2343 };
2344 let dec = DateTimeValue {
2345 year: 2025,
2346 month: 12,
2347 day: 1,
2348 hour: 0,
2349 minute: 0,
2350 second: 0,
2351 microsecond: 0,
2352 timezone: None,
2353 };
2354
2355 let set = ExecutionPlanSet {
2356 spec_name: "s".into(),
2357 plans: vec![
2358 empty_plan(EffectiveDate::Origin),
2359 empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2360 empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2361 ],
2362 };
2363
2364 assert!(std::ptr::eq(
2365 set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2366 .expect("boundary instant"),
2367 &set.plans[1]
2368 ));
2369 assert!(std::ptr::eq(
2370 set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2371 .expect("dec boundary"),
2372 &set.plans[2]
2373 ));
2374 }
2375
2376 #[test]
2377 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2378 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2379
2380 let june = DateTimeValue {
2381 year: 2025,
2382 month: 6,
2383 day: 1,
2384 hour: 0,
2385 minute: 0,
2386 second: 0,
2387 microsecond: 0,
2388 timezone: None,
2389 };
2390 let may_end = DateTimeValue {
2391 year: 2025,
2392 month: 5,
2393 day: 31,
2394 hour: 23,
2395 minute: 59,
2396 second: 59,
2397 microsecond: 0,
2398 timezone: None,
2399 };
2400
2401 let set = ExecutionPlanSet {
2402 spec_name: "s".into(),
2403 plans: vec![
2404 empty_plan(EffectiveDate::Origin),
2405 empty_plan(EffectiveDate::DateTimeValue(june)),
2406 ],
2407 };
2408
2409 assert!(std::ptr::eq(
2410 set.plan_at(&EffectiveDate::DateTimeValue(may_end))
2411 .expect("may 31"),
2412 &set.plans[0]
2413 ));
2414 }
2415
2416 #[test]
2417 fn plan_at_single_plan_matches_any_instant_after_start() {
2418 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2419
2420 let t = DateTimeValue {
2421 year: 2025,
2422 month: 3,
2423 day: 1,
2424 hour: 0,
2425 minute: 0,
2426 second: 0,
2427 microsecond: 0,
2428 timezone: None,
2429 };
2430 let set = ExecutionPlanSet {
2431 spec_name: "s".into(),
2432 plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
2433 year: 2025,
2434 month: 1,
2435 day: 1,
2436 hour: 0,
2437 minute: 0,
2438 second: 0,
2439 microsecond: 0,
2440 timezone: None,
2441 }))],
2442 };
2443 assert!(std::ptr::eq(
2444 set.plan_at(&EffectiveDate::DateTimeValue(t))
2445 .expect("inside single slice"),
2446 &set.plans[0]
2447 ));
2448 }
2449
2450 #[test]
2453 fn schema_json_shape_contract() {
2454 let mut engine = Engine::new();
2455 engine
2456 .load(
2457 r#"
2458 spec pricing
2459 data bridge_height: quantity
2460 -> unit meter 1
2461 -> default 100 meter
2462 data quantity: number -> minimum 0
2463 rule cost: bridge_height * quantity
2464 "#,
2465 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2466 "test.lemma",
2467 ))),
2468 )
2469 .unwrap();
2470 let now = DateTimeValue::now();
2471 let schema = engine
2472 .get_plan(None, "pricing", Some(&now))
2473 .unwrap()
2474 .schema();
2475
2476 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2477
2478 let bh = &value["data"]["bridge_height"];
2479 assert!(
2480 bh.is_object(),
2481 "data entry must be a named object, not tuple"
2482 );
2483 assert!(
2484 bh.get("type").is_some(),
2485 "data entry must expose `type` field"
2486 );
2487 assert!(
2488 bh.get("default").is_some(),
2489 "bridge_height exposes `-> default` as schema default suggestion"
2490 );
2491 assert!(
2492 bh.get("bound_value").is_none(),
2493 "bridge_height is not a spec-bound literal"
2494 );
2495
2496 let ty = &bh["type"];
2497 assert_eq!(
2498 ty["kind"], "quantity",
2499 "kind tag sits on the type object itself"
2500 );
2501 assert!(
2502 ty["units"].is_array(),
2503 "quantity-only fields flatten up to top level"
2504 );
2505 assert!(
2506 ty.get("options").is_none(),
2507 "text-only fields must not leak"
2508 );
2509
2510 let qty = &value["data"]["quantity"];
2511 assert_eq!(qty["type"]["kind"], "number");
2512 assert!(
2513 qty.get("default").is_none(),
2514 "quantity has no default suggestion"
2515 );
2516 assert!(
2517 qty.get("bound_value").is_none(),
2518 "quantity has no bound literal"
2519 );
2520
2521 let cost = &value["rules"]["cost"];
2522 assert_eq!(
2523 cost["kind"], "quantity",
2524 "rule types use the same flat shape"
2525 );
2526 assert!(
2527 cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
2528 "quantity rule result types expose declared units"
2529 );
2530 assert!(
2531 cost["units"][0].get("factor").is_some(),
2532 "quantity rule units use factor field"
2533 );
2534 }
2535
2536 #[test]
2537 fn schema_rule_result_units_contract() {
2538 let mut engine = Engine::new();
2539 engine
2540 .load(
2541 r#"
2542 spec units_contract
2543 data money: quantity
2544 -> unit eur 1
2545 -> unit usd 0.91
2546 data rate: ratio
2547 -> unit basis_points 10000
2548 -> unit percent 100
2549 -> default 500 basis_points
2550 rule total: money
2551 rule rate_out: rate
2552 "#,
2553 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2554 "units_contract.lemma",
2555 ))),
2556 )
2557 .unwrap();
2558 let now = DateTimeValue::now();
2559 let schema = engine
2560 .get_plan(None, "units_contract", Some(&now))
2561 .unwrap()
2562 .schema();
2563 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2564
2565 let money_units = &value["data"]["money"]["type"]["units"];
2566 assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
2567 assert!(money_units[0].get("name").is_some());
2568 assert!(money_units[0].get("factor").is_some());
2569 assert!(money_units[0]["factor"].get("numer").is_some());
2570 assert!(money_units[0]["factor"].get("denom").is_some());
2571
2572 let rate_units = &value["data"]["rate"]["type"]["units"];
2573 assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
2574 assert!(rate_units[0].get("name").is_some());
2575 assert!(rate_units[0].get("value").is_some());
2576 assert!(rate_units[0]["value"].get("numer").is_some());
2577 assert!(rate_units[0]["value"].get("denom").is_some());
2578
2579 let total_rule_units = &value["rules"]["total"]["units"];
2580 let money_unit_names: Vec<_> = money_units
2581 .as_array()
2582 .unwrap()
2583 .iter()
2584 .map(|u| u["name"].as_str().unwrap())
2585 .collect();
2586 let total_rule_unit_names: Vec<_> = total_rule_units
2587 .as_array()
2588 .unwrap()
2589 .iter()
2590 .map(|u| u["name"].as_str().unwrap())
2591 .collect();
2592 assert_eq!(total_rule_unit_names, money_unit_names);
2593
2594 let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
2595 let rate_unit_names: Vec<_> = rate_units
2596 .as_array()
2597 .unwrap()
2598 .iter()
2599 .map(|u| u["name"].as_str().unwrap())
2600 .collect();
2601 let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
2602 .as_array()
2603 .unwrap()
2604 .iter()
2605 .map(|u| u["name"].as_str().unwrap())
2606 .collect();
2607 assert_eq!(rate_out_rule_unit_names, rate_unit_names);
2608 }
2609
2610 #[test]
2611 fn schema_json_round_trip_preserves_shape() {
2612 let mut engine = Engine::new();
2613 engine
2614 .load(
2615 r#"
2616 spec s
2617 data age: number -> minimum 0 -> default 18
2618 data grade: text -> options "A" "B" "C"
2619 rule adult: age >= 18
2620 "#,
2621 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
2622 )
2623 .unwrap();
2624 let now = DateTimeValue::now();
2625 let schema = engine.get_plan(None, "s", Some(&now)).unwrap().schema();
2626
2627 let json = serde_json::to_string(&schema).unwrap();
2628 let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
2629 assert_eq!(schema, round_tripped);
2630 }
2631}
2632
2633