1use crate::parsing::ast::{EffectiveDate, LemmaSpec, MetaValue};
12use crate::planning::graph::Graph;
13use crate::planning::graph::ResolvedSpecTypes;
14use crate::planning::semantics;
15use crate::planning::semantics::{
16 DataDefinition, DataPath, Expression, LemmaType, LiteralValue, RulePath, TypeSpecification,
17 ValueKind,
18};
19use crate::Error;
20use crate::ResourceLimits;
21use crate::Source;
22use indexmap::IndexMap;
23use serde::{Deserialize, Serialize};
24use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
25use std::sync::Arc;
26
27pub type SpecSources = IndexMap<(String, EffectiveDate), String>;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ExecutionPlan {
36 pub spec_name: String,
38
39 #[serde(serialize_with = "crate::serialization::serialize_resolved_data_value_map")]
41 #[serde(deserialize_with = "crate::serialization::deserialize_resolved_data_value_map")]
42 pub data: IndexMap<DataPath, DataDefinition>,
43
44 pub rules: Vec<ExecutableRule>,
46
47 #[serde(default, alias = "alias_evaluation_order")]
52 pub reference_evaluation_order: Vec<DataPath>,
53
54 pub meta: HashMap<String, MetaValue>,
56
57 pub named_types: BTreeMap<String, LemmaType>,
59
60 pub effective: EffectiveDate,
61
62 #[serde(default)]
65 #[serde(
66 serialize_with = "serialize_sources",
67 deserialize_with = "deserialize_sources"
68 )]
69 pub sources: SpecSources,
70}
71
72#[derive(Debug, Clone)]
75pub struct ExecutionPlanSet {
76 pub spec_name: String,
77 pub plans: Vec<ExecutionPlan>,
78}
79
80impl ExecutionPlanSet {
81 #[must_use]
83 pub fn plan_at(&self, effective: &EffectiveDate) -> Option<&ExecutionPlan> {
84 for (i, plan) in self.plans.iter().enumerate() {
85 let from_ok = *effective >= plan.effective;
86 let to_ok = self
87 .plans
88 .get(i + 1)
89 .map(|next| *effective < next.effective)
90 .unwrap_or(true);
91 if from_ok && to_ok {
92 return Some(plan);
93 }
94 }
95 None
96 }
97}
98
99fn serialize_sources<S>(sources: &SpecSources, serializer: S) -> Result<S::Ok, S::Error>
100where
101 S: serde::Serializer,
102{
103 use serde::ser::SerializeSeq;
104 let mut seq = serializer.serialize_seq(Some(sources.len()))?;
105 for ((name, effective_from), source) in sources {
106 seq.serialize_element(&SpecSourceEntry {
107 name,
108 effective_from,
109 source,
110 })?;
111 }
112 seq.end()
113}
114
115fn deserialize_sources<'de, D>(deserializer: D) -> Result<SpecSources, D::Error>
116where
117 D: serde::Deserializer<'de>,
118{
119 let entries: Vec<SpecSourceEntryOwned> = Vec::deserialize(deserializer)?;
120 let mut map = IndexMap::with_capacity(entries.len());
121 for e in entries {
122 map.insert((e.name, e.effective_from), e.source);
123 }
124 Ok(map)
125}
126
127#[derive(Serialize)]
128struct SpecSourceEntry<'a> {
129 name: &'a str,
130 effective_from: &'a EffectiveDate,
131 source: &'a str,
132}
133
134#[derive(Deserialize)]
135struct SpecSourceEntryOwned {
136 name: String,
137 effective_from: EffectiveDate,
138 source: String,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ExecutableRule {
146 pub path: RulePath,
148
149 pub name: String,
151
152 pub branches: Vec<Branch>,
157
158 pub needs_data: BTreeSet<DataPath>,
160
161 pub source: Source,
163
164 pub rule_type: LemmaType,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct Branch {
172 pub condition: Option<Expression>,
174
175 pub result: Expression,
177
178 pub source: Source,
180}
181
182pub(crate) fn build_execution_plan(
185 graph: &Graph,
186 resolved_types: &HashMap<Arc<LemmaSpec>, ResolvedSpecTypes>,
187 effective: &EffectiveDate,
188) -> ExecutionPlan {
189 let data = graph.build_data();
190 let execution_order = graph.execution_order();
191
192 let mut executable_rules: Vec<ExecutableRule> = Vec::new();
193 let mut path_to_index: HashMap<RulePath, usize> = HashMap::new();
194
195 for rule_path in execution_order {
196 let rule_node = graph.rules().get(rule_path).expect(
197 "bug: rule from topological sort not in graph - validation should have caught this",
198 );
199
200 let mut direct_data = HashSet::new();
201 for (condition, result) in &rule_node.branches {
202 if let Some(cond) = condition {
203 cond.collect_data_paths(&mut direct_data);
204 }
205 result.collect_data_paths(&mut direct_data);
206 }
207 let mut needs_data: BTreeSet<DataPath> = direct_data.into_iter().collect();
208
209 for dep in &rule_node.depends_on_rules {
210 if let Some(&dep_idx) = path_to_index.get(dep) {
211 needs_data.extend(executable_rules[dep_idx].needs_data.iter().cloned());
212 }
213 }
214
215 let mut executable_branches = Vec::new();
216 for (condition, result) in &rule_node.branches {
217 executable_branches.push(Branch {
218 condition: condition.clone(),
219 result: result.clone(),
220 source: rule_node.source.clone(),
221 });
222 }
223
224 path_to_index.insert(rule_path.clone(), executable_rules.len());
225 executable_rules.push(ExecutableRule {
226 path: rule_path.clone(),
227 name: rule_path.rule.clone(),
228 branches: executable_branches,
229 source: rule_node.source.clone(),
230 needs_data,
231 rule_type: rule_node.rule_type.clone(),
232 });
233 }
234
235 let main_spec = graph.main_spec();
236 let named_types = build_type_tables(main_spec, resolved_types);
237
238 let mut sources: SpecSources = IndexMap::new();
239 for spec in resolved_types.keys() {
240 let key = (spec.name.clone(), spec.effective_from.clone());
241 sources
242 .entry(key)
243 .or_insert_with(|| crate::formatting::format_spec(spec, crate::formatting::MAX_COLS));
244 }
245
246 ExecutionPlan {
247 spec_name: main_spec.name.clone(),
248 data,
249 rules: executable_rules,
250 reference_evaluation_order: graph.reference_evaluation_order().to_vec(),
251 meta: main_spec
252 .meta_fields
253 .iter()
254 .map(|f| (f.key.clone(), f.value.clone()))
255 .collect(),
256 named_types,
257 effective: effective.clone(),
258 sources,
259 }
260}
261
262fn build_type_tables(
264 main_spec: &Arc<LemmaSpec>,
265 resolved_types: &HashMap<Arc<LemmaSpec>, ResolvedSpecTypes>,
266) -> BTreeMap<String, LemmaType> {
267 let mut named_types = BTreeMap::new();
268
269 let main_resolved = resolved_types
270 .iter()
271 .find(|(spec, _)| Arc::ptr_eq(spec, main_spec))
272 .map(|(_, types)| types);
273
274 if let Some(resolved) = main_resolved {
275 for (type_name, lemma_type) in &resolved.named_types {
276 named_types.insert(type_name.clone(), lemma_type.clone());
277 }
278 }
279
280 named_types
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct DataEntry {
308 #[serde(rename = "type")]
309 pub lemma_type: LemmaType,
310 #[serde(skip_serializing_if = "Option::is_none", default)]
311 pub default: Option<LiteralValue>,
312}
313
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
315pub struct SpecSchema {
316 pub spec: String,
318 pub data: indexmap::IndexMap<String, DataEntry>,
320 pub rules: indexmap::IndexMap<String, LemmaType>,
322 pub meta: HashMap<String, MetaValue>,
324}
325
326impl std::fmt::Display for SpecSchema {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 write!(f, "Spec: {}", self.spec)?;
329
330 if !self.meta.is_empty() {
331 write!(f, "\n\nMeta:")?;
332 let mut keys: Vec<&String> = self.meta.keys().collect();
334 keys.sort();
335 for key in keys {
336 write!(f, "\n {}: {}", key, self.meta.get(key).unwrap())?;
337 }
338 }
339
340 if !self.data.is_empty() {
341 write!(f, "\n\nData:")?;
342 for (name, entry) in &self.data {
343 write!(f, "\n {} ({}", name, entry.lemma_type.name())?;
344 if let Some(constraints) = format_type_constraints(&entry.lemma_type.specifications)
345 {
346 write!(f, ", {}", constraints)?;
347 }
348 if let Some(val) = &entry.default {
349 write!(f, ", default: {}", val)?;
350 }
351 write!(f, ")")?;
352 }
353 }
354
355 if !self.rules.is_empty() {
356 write!(f, "\n\nRules:")?;
357 for (name, rule_type) in &self.rules {
358 write!(f, "\n {} ({})", name, rule_type.name())?;
359 }
360 }
361
362 if self.data.is_empty() && self.rules.is_empty() {
363 write!(f, "\n (no data or rules)")?;
364 }
365
366 Ok(())
367 }
368}
369
370impl SpecSchema {
371 pub(crate) fn is_type_compatible(&self, other: &SpecSchema) -> bool {
376 for (name, entry) in &self.data {
377 if let Some(other_entry) = other.data.get(name) {
378 if entry.lemma_type != other_entry.lemma_type {
379 return false;
380 }
381 }
382 }
383 for (name, lt) in &self.rules {
384 if let Some(other_lt) = other.rules.get(name) {
385 if lt != other_lt {
386 return false;
387 }
388 }
389 }
390 true
391 }
392}
393
394fn format_type_constraints(spec: &TypeSpecification) -> Option<String> {
397 let mut parts = Vec::new();
398
399 match spec {
400 TypeSpecification::Number {
401 minimum, maximum, ..
402 } => {
403 if let Some(v) = minimum {
404 parts.push(format!("minimum: {}", v));
405 }
406 if let Some(v) = maximum {
407 parts.push(format!("maximum: {}", v));
408 }
409 }
410 TypeSpecification::Scale {
411 minimum,
412 maximum,
413 decimals,
414 units,
415 ..
416 } => {
417 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
418 if !unit_names.is_empty() {
419 parts.push(format!("units: {}", unit_names.join(", ")));
420 }
421 if let Some(v) = minimum {
422 parts.push(format!("minimum: {}", v));
423 }
424 if let Some(v) = maximum {
425 parts.push(format!("maximum: {}", v));
426 }
427 if let Some(d) = decimals {
428 parts.push(format!("decimals: {}", d));
429 }
430 }
431 TypeSpecification::Ratio {
432 minimum, maximum, ..
433 } => {
434 if let Some(v) = minimum {
435 parts.push(format!("minimum: {}", v));
436 }
437 if let Some(v) = maximum {
438 parts.push(format!("maximum: {}", v));
439 }
440 }
441 TypeSpecification::Text { options, .. } => {
442 if !options.is_empty() {
443 let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
444 parts.push(format!("options: {}", quoted.join(", ")));
445 }
446 }
447 TypeSpecification::Date {
448 minimum, maximum, ..
449 } => {
450 if let Some(v) = minimum {
451 parts.push(format!("minimum: {}", v));
452 }
453 if let Some(v) = maximum {
454 parts.push(format!("maximum: {}", v));
455 }
456 }
457 TypeSpecification::Time {
458 minimum, maximum, ..
459 } => {
460 if let Some(v) = minimum {
461 parts.push(format!("minimum: {}", v));
462 }
463 if let Some(v) = maximum {
464 parts.push(format!("maximum: {}", v));
465 }
466 }
467 TypeSpecification::Boolean { .. }
468 | TypeSpecification::Duration { .. }
469 | TypeSpecification::Veto { .. }
470 | TypeSpecification::Undetermined => {}
471 }
472
473 if parts.is_empty() {
474 None
475 } else {
476 Some(parts.join(", "))
477 }
478}
479
480impl ExecutionPlan {
481 pub fn schema(&self) -> SpecSchema {
489 let all_local_rules: Vec<String> = self
490 .rules
491 .iter()
492 .filter(|r| r.path.segments.is_empty())
493 .map(|r| r.name.clone())
494 .collect();
495 self.schema_for_rules(&all_local_rules)
496 .expect("BUG: all_local_rules sourced from self.rules")
497 }
498
499 pub(crate) fn interface_schema(&self) -> SpecSchema {
501 let mut data_entries: Vec<(usize, String, DataEntry)> = self
502 .data
503 .iter()
504 .filter(|(_, data)| data.schema_type().is_some())
505 .map(|(path, data)| {
506 let lemma_type = data
507 .schema_type()
508 .expect("BUG: filter above ensured schema_type is Some")
509 .clone();
510 let default = data.schema_default();
511 (
512 data.source().span.start,
513 path.input_key(),
514 DataEntry {
515 lemma_type,
516 default,
517 },
518 )
519 })
520 .collect();
521 data_entries.sort_by_key(|(pos, _, _)| *pos);
522
523 let rule_entries: Vec<(String, LemmaType)> = self
524 .rules
525 .iter()
526 .filter(|r| r.path.segments.is_empty())
527 .map(|r| (r.name.clone(), r.rule_type.clone()))
528 .collect();
529
530 SpecSchema {
531 spec: self.spec_name.clone(),
532 data: data_entries
533 .into_iter()
534 .map(|(_, name, data)| (name, data))
535 .collect(),
536 rules: rule_entries.into_iter().collect(),
537 meta: self.meta.clone(),
538 }
539 }
540
541 pub fn schema_for_rules(&self, rule_names: &[String]) -> Result<SpecSchema, Error> {
550 let mut needed_data = HashSet::new();
551 let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
552
553 for rule_name in rule_names {
554 let rule = self.get_rule(rule_name).ok_or_else(|| {
555 Error::request(
556 format!(
557 "Rule '{}' not found in spec '{}'",
558 rule_name, self.spec_name
559 ),
560 None::<String>,
561 )
562 })?;
563 needed_data.extend(rule.needs_data.iter().cloned());
564 rule_entries.push((rule.name.clone(), rule.rule_type.clone()));
565 }
566
567 let mut data_entries: Vec<(usize, String, DataEntry)> = self
568 .data
569 .iter()
570 .filter(|(path, _)| needed_data.contains(path))
571 .filter(|(_, data)| data.schema_type().is_some())
572 .map(|(path, data)| {
573 let lemma_type = data.schema_type().unwrap().clone();
574 let default = data.schema_default();
575 (
576 data.source().span.start,
577 path.input_key(),
578 DataEntry {
579 lemma_type,
580 default,
581 },
582 )
583 })
584 .collect();
585 data_entries.sort_by_key(|(pos, _, _)| *pos);
586 let data_entries: Vec<(String, DataEntry)> = data_entries
587 .into_iter()
588 .map(|(_, name, data)| (name, data))
589 .collect();
590
591 Ok(SpecSchema {
592 spec: self.spec_name.clone(),
593 data: data_entries.into_iter().collect(),
594 rules: rule_entries.into_iter().collect(),
595 meta: self.meta.clone(),
596 })
597 }
598
599 pub fn get_data_path_by_str(&self, name: &str) -> Option<&DataPath> {
601 self.data.keys().find(|path| path.input_key() == name)
602 }
603
604 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
606 self.rules
607 .iter()
608 .find(|r| r.name == name && r.path.segments.is_empty())
609 }
610
611 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
613 self.rules.iter().find(|r| &r.path == rule_path)
614 }
615
616 pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
618 self.data.get(path).and_then(|d| d.value())
619 }
620
621 pub fn with_data_values(
625 mut self,
626 values: HashMap<String, String>,
627 limits: &ResourceLimits,
628 ) -> Result<Self, Error> {
629 for (name, raw_value) in values {
630 let data_path = self.get_data_path_by_str(&name).ok_or_else(|| {
631 let available: Vec<String> = self.data.keys().map(|p| p.input_key()).collect();
632 Error::request(
633 format!(
634 "Data '{}' not found. Available data: {}",
635 name,
636 available.join(", ")
637 ),
638 None::<String>,
639 )
640 })?;
641 let data_path = data_path.clone();
642
643 let data_definition = self
644 .data
645 .get(&data_path)
646 .expect("BUG: data_path was just resolved from self.data, must exist");
647
648 let data_source = data_definition.source().clone();
649 let expected_type = data_definition.schema_type().cloned().ok_or_else(|| {
650 Error::request(
651 format!(
652 "Data '{}' is a spec reference; cannot provide a value.",
653 name
654 ),
655 None::<String>,
656 )
657 })?;
658
659 let parsed_value = crate::planning::semantics::parse_value_from_string(
660 &raw_value,
661 &expected_type.specifications,
662 &data_source,
663 )
664 .map_err(|e| e.with_related_data(&name))?;
665 let semantic_value = semantics::value_to_semantic(&parsed_value).map_err(|msg| {
666 Error::validation(msg, Some(data_source.clone()), None::<String>)
667 .with_related_data(&name)
668 })?;
669 let literal_value = LiteralValue {
670 value: semantic_value,
671 lemma_type: expected_type.clone(),
672 };
673
674 let size = literal_value.byte_size();
675 if size > limits.max_data_value_bytes {
676 return Err(Error::resource_limit_exceeded(
677 "max_data_value_bytes",
678 limits.max_data_value_bytes.to_string(),
679 size.to_string(),
680 format!(
681 "Reduce the size of data values to {} bytes or less",
682 limits.max_data_value_bytes
683 ),
684 Some(data_source.clone()),
685 None,
686 None,
687 )
688 .with_related_data(&name));
689 }
690
691 validate_value_against_type(&expected_type, &literal_value).map_err(|msg| {
692 Error::validation(msg, Some(data_source.clone()), None::<String>)
693 .with_related_data(&name)
694 })?;
695
696 self.data.insert(
697 data_path,
698 DataDefinition::Value {
699 value: literal_value,
700 source: data_source,
701 },
702 );
703 }
704
705 Ok(self)
706 }
707}
708
709pub(crate) fn validate_value_against_type(
710 expected_type: &LemmaType,
711 value: &LiteralValue,
712) -> Result<(), String> {
713 use crate::planning::semantics::TypeSpecification;
714
715 let effective_decimals = |n: rust_decimal::Decimal| n.scale();
716
717 match (&expected_type.specifications, &value.value) {
718 (
719 TypeSpecification::Number {
720 minimum,
721 maximum,
722 decimals,
723 ..
724 },
725 ValueKind::Number(n),
726 ) => {
727 if let Some(min) = minimum {
728 if n < min {
729 return Err(format!("{} is below minimum {}", n, min));
730 }
731 }
732 if let Some(max) = maximum {
733 if n > max {
734 return Err(format!("{} is above maximum {}", n, max));
735 }
736 }
737 if let Some(d) = decimals {
738 if effective_decimals(*n) > u32::from(*d) {
739 return Err(format!("{} has more than {} decimals", n, d));
740 }
741 }
742 Ok(())
743 }
744 (
745 TypeSpecification::Scale {
746 minimum,
747 maximum,
748 decimals,
749 ..
750 },
751 ValueKind::Scale(n, _unit),
752 ) => {
753 if let Some(min) = minimum {
754 if n < min {
755 return Err(format!("{} is below minimum {}", n, min));
756 }
757 }
758 if let Some(max) = maximum {
759 if n > max {
760 return Err(format!("{} is above maximum {}", n, max));
761 }
762 }
763 if let Some(d) = decimals {
764 if effective_decimals(*n) > u32::from(*d) {
765 return Err(format!("{} has more than {} decimals", n, d));
766 }
767 }
768 Ok(())
769 }
770 (
771 TypeSpecification::Text {
772 length, options, ..
773 },
774 ValueKind::Text(s),
775 ) => {
776 let len = s.chars().count();
777 if let Some(exact) = length {
778 if len != *exact {
779 return Err(format!(
780 "'{}' has length {} but required length is {}",
781 s, len, exact
782 ));
783 }
784 }
785 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
786 return Err(format!(
787 "'{}' is not in allowed options: {}",
788 s,
789 options.join(", ")
790 ));
791 }
792 Ok(())
793 }
794 (
795 TypeSpecification::Ratio {
796 minimum,
797 maximum,
798 decimals,
799 ..
800 },
801 ValueKind::Ratio(r, _unit),
802 ) => {
803 if let Some(min) = minimum {
804 if r < min {
805 return Err(format!("{} is below minimum {}", r, min));
806 }
807 }
808 if let Some(max) = maximum {
809 if r > max {
810 return Err(format!("{} is above maximum {}", r, max));
811 }
812 }
813 if let Some(d) = decimals {
814 if effective_decimals(*r) > u32::from(*d) {
815 return Err(format!("{} has more than {} decimals", r, d));
816 }
817 }
818 Ok(())
819 }
820 (
821 TypeSpecification::Date {
822 minimum, maximum, ..
823 },
824 ValueKind::Date(dt),
825 ) => {
826 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
827 use std::cmp::Ordering;
828 if let Some(min) = minimum {
829 let min_sem = date_time_to_semantic(min);
830 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
831 return Err(format!("{} is below minimum {}", dt, min));
832 }
833 }
834 if let Some(max) = maximum {
835 let max_sem = date_time_to_semantic(max);
836 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
837 return Err(format!("{} is above maximum {}", dt, max));
838 }
839 }
840 Ok(())
841 }
842 (
843 TypeSpecification::Duration {
844 minimum, maximum, ..
845 },
846 ValueKind::Duration(value, unit),
847 ) => {
848 use crate::computation::units::duration_to_seconds;
849 let value_secs = duration_to_seconds(*value, unit);
850 if let Some((min_v, min_u)) = minimum {
851 let min_secs = duration_to_seconds(*min_v, min_u);
852 if value_secs < min_secs {
853 return Err(format!(
854 "{} {} is below minimum {} {}",
855 value, unit, min_v, min_u
856 ));
857 }
858 }
859 if let Some((max_v, max_u)) = maximum {
860 let max_secs = duration_to_seconds(*max_v, max_u);
861 if value_secs > max_secs {
862 return Err(format!(
863 "{} {} is above maximum {} {}",
864 value, unit, max_v, max_u
865 ));
866 }
867 }
868 Ok(())
869 }
870 (
871 TypeSpecification::Time {
872 minimum, maximum, ..
873 },
874 ValueKind::Time(t),
875 ) => {
876 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
877 use std::cmp::Ordering;
878 if let Some(min) = minimum {
879 let min_sem = time_to_semantic(min);
880 if compare_semantic_times(t, &min_sem) == Ordering::Less {
881 return Err(format!("{} is below minimum {}", t, min));
882 }
883 }
884 if let Some(max) = maximum {
885 let max_sem = time_to_semantic(max);
886 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
887 return Err(format!("{} is above maximum {}", t, max));
888 }
889 }
890 Ok(())
891 }
892 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
893 | (TypeSpecification::Veto { .. }, _)
894 | (TypeSpecification::Undetermined, _) => Ok(()),
895 (spec, value_kind) => unreachable!(
896 "BUG: validate_value_against_type called with mismatched type/value: \
897 spec={:?}, value={:?} — typing must be enforced before validation",
898 spec, value_kind
899 ),
900 }
901}
902
903pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
904 let mut errors = Vec::new();
905
906 for (data_path, data_definition) in &plan.data {
907 let (expected_type, lit) = match data_definition {
908 DataDefinition::Value { value, .. } => (&value.lemma_type, value),
909 DataDefinition::TypeDeclaration { .. }
910 | DataDefinition::SpecRef { .. }
911 | DataDefinition::Reference { .. } => continue,
912 };
913
914 if let Err(msg) = validate_value_against_type(expected_type, lit) {
915 let source = data_definition.source().clone();
916 errors.push(Error::validation(
917 format!(
918 "Invalid value for data {} (expected {}): {}",
919 data_path,
920 expected_type.name(),
921 msg
922 ),
923 Some(source),
924 None::<String>,
925 ));
926 }
927 }
928
929 errors
930}
931
932#[cfg(test)]
933mod tests {
934 use super::*;
935 use crate::parsing::ast::DateTimeValue;
936 use crate::planning::semantics::{
937 primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
938 };
939 use crate::Engine;
940 use serde_json;
941 use std::str::FromStr;
942 use std::sync::Arc;
943
944 fn default_limits() -> ResourceLimits {
945 ResourceLimits::default()
946 }
947
948 #[test]
949 fn test_with_raw_values() {
950 let mut engine = Engine::new();
951 engine
952 .load(
953 r#"
954 spec test
955 data age: number -> default 25
956 "#,
957 crate::SourceType::Labeled("test.lemma"),
958 )
959 .unwrap();
960
961 let now = DateTimeValue::now();
962 let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
963 let data_path = DataPath::new(vec![], "age".to_string());
964
965 let mut values = HashMap::new();
966 values.insert("age".to_string(), "30".to_string());
967
968 let updated_plan = plan.with_data_values(values, &default_limits()).unwrap();
969 let updated_value = updated_plan.get_data_value(&data_path).unwrap();
970 match &updated_value.value {
971 crate::planning::semantics::ValueKind::Number(n) => {
972 assert_eq!(n, &rust_decimal::Decimal::from(30))
973 }
974 other => panic!("Expected number literal, got {:?}", other),
975 }
976 }
977
978 #[test]
979 fn test_with_raw_values_type_mismatch() {
980 let mut engine = Engine::new();
981 engine
982 .load(
983 r#"
984 spec test
985 data age: number
986 "#,
987 crate::SourceType::Labeled("test.lemma"),
988 )
989 .unwrap();
990
991 let now = DateTimeValue::now();
992 let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
993
994 let mut values = HashMap::new();
995 values.insert("age".to_string(), "thirty".to_string());
996
997 assert!(plan.with_data_values(values, &default_limits()).is_err());
998 }
999
1000 #[test]
1001 fn test_with_raw_values_unknown_data() {
1002 let mut engine = Engine::new();
1003 engine
1004 .load(
1005 r#"
1006 spec test
1007 data known: number
1008 "#,
1009 crate::SourceType::Labeled("test.lemma"),
1010 )
1011 .unwrap();
1012
1013 let now = DateTimeValue::now();
1014 let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
1015
1016 let mut values = HashMap::new();
1017 values.insert("unknown".to_string(), "30".to_string());
1018
1019 assert!(plan.with_data_values(values, &default_limits()).is_err());
1020 }
1021
1022 #[test]
1023 fn test_with_raw_values_nested() {
1024 let mut engine = Engine::new();
1025 engine
1026 .load(
1027 r#"
1028 spec private
1029 data base_price: number
1030
1031 spec test
1032 with rules: private
1033 "#,
1034 crate::SourceType::Labeled("test.lemma"),
1035 )
1036 .unwrap();
1037
1038 let now = DateTimeValue::now();
1039 let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
1040
1041 let mut values = HashMap::new();
1042 values.insert("rules.base_price".to_string(), "100".to_string());
1043
1044 let updated_plan = plan.with_data_values(values, &default_limits()).unwrap();
1045 let data_path = DataPath {
1046 segments: vec![PathSegment {
1047 data: "rules".to_string(),
1048 spec: "private".to_string(),
1049 }],
1050 data: "base_price".to_string(),
1051 };
1052 let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1053 match &updated_value.value {
1054 crate::planning::semantics::ValueKind::Number(n) => {
1055 assert_eq!(n, &rust_decimal::Decimal::from(100))
1056 }
1057 other => panic!("Expected number literal, got {:?}", other),
1058 }
1059 }
1060
1061 fn test_source() -> crate::Source {
1062 use crate::parsing::ast::Span;
1063 crate::Source::new(
1064 "<test>",
1065 Span {
1066 start: 0,
1067 end: 0,
1068 line: 1,
1069 col: 0,
1070 },
1071 )
1072 }
1073
1074 fn create_literal_expr(value: LiteralValue) -> Expression {
1075 Expression::new(
1076 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
1077 test_source(),
1078 )
1079 }
1080
1081 fn create_data_path_expr(path: DataPath) -> Expression {
1082 Expression::new(
1083 crate::planning::semantics::ExpressionKind::DataPath(path),
1084 test_source(),
1085 )
1086 }
1087
1088 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
1089 LiteralValue::number(n)
1090 }
1091
1092 fn create_boolean_literal(b: bool) -> LiteralValue {
1093 LiteralValue::from_bool(b)
1094 }
1095
1096 fn create_text_literal(s: String) -> LiteralValue {
1097 LiteralValue::text(s)
1098 }
1099
1100 #[test]
1101 fn with_values_should_enforce_number_maximum_constraint() {
1102 let data_path = DataPath::new(vec![], "x".to_string());
1105
1106 let max10 = crate::planning::semantics::LemmaType::primitive(
1107 crate::planning::semantics::TypeSpecification::Number {
1108 minimum: None,
1109 maximum: Some(rust_decimal::Decimal::from_str("10").unwrap()),
1110 decimals: None,
1111 precision: None,
1112 help: String::new(),
1113 },
1114 );
1115 let source = Source::new(
1116 "<test>",
1117 crate::parsing::ast::Span {
1118 start: 0,
1119 end: 0,
1120 line: 1,
1121 col: 0,
1122 },
1123 );
1124 let mut data = IndexMap::new();
1125 data.insert(
1126 data_path.clone(),
1127 crate::planning::semantics::DataDefinition::Value {
1128 value: crate::planning::semantics::LiteralValue::number_with_type(
1129 0.into(),
1130 max10.clone(),
1131 ),
1132 source: source.clone(),
1133 },
1134 );
1135
1136 let plan = ExecutionPlan {
1137 spec_name: "test".to_string(),
1138 data,
1139 rules: Vec::new(),
1140 reference_evaluation_order: Vec::new(),
1141 meta: HashMap::new(),
1142 named_types: BTreeMap::new(),
1143 effective: EffectiveDate::Origin,
1144 sources: IndexMap::new(),
1145 };
1146
1147 let mut values = HashMap::new();
1148 values.insert("x".to_string(), "11".to_string());
1149
1150 assert!(
1151 plan.with_data_values(values, &default_limits()).is_err(),
1152 "Providing x=11 should fail due to maximum 10"
1153 );
1154 }
1155
1156 #[test]
1157 fn with_values_should_enforce_text_enum_options() {
1158 let data_path = DataPath::new(vec![], "tier".to_string());
1160
1161 let tier = crate::planning::semantics::LemmaType::primitive(
1162 crate::planning::semantics::TypeSpecification::Text {
1163 length: None,
1164 options: vec!["silver".to_string(), "gold".to_string()],
1165 help: String::new(),
1166 },
1167 );
1168 let source = Source::new(
1169 "<test>",
1170 crate::parsing::ast::Span {
1171 start: 0,
1172 end: 0,
1173 line: 1,
1174 col: 0,
1175 },
1176 );
1177 let mut data = IndexMap::new();
1178 data.insert(
1179 data_path.clone(),
1180 crate::planning::semantics::DataDefinition::Value {
1181 value: crate::planning::semantics::LiteralValue::text_with_type(
1182 "silver".to_string(),
1183 tier.clone(),
1184 ),
1185 source,
1186 },
1187 );
1188
1189 let plan = ExecutionPlan {
1190 spec_name: "test".to_string(),
1191 data,
1192 rules: Vec::new(),
1193 reference_evaluation_order: Vec::new(),
1194 meta: HashMap::new(),
1195 named_types: BTreeMap::new(),
1196 effective: EffectiveDate::Origin,
1197 sources: IndexMap::new(),
1198 };
1199
1200 let mut values = HashMap::new();
1201 values.insert("tier".to_string(), "platinum".to_string());
1202
1203 assert!(
1204 plan.with_data_values(values, &default_limits()).is_err(),
1205 "Invalid enum value should be rejected (tier='platinum')"
1206 );
1207 }
1208
1209 #[test]
1210 fn with_values_should_enforce_scale_decimals() {
1211 let data_path = DataPath::new(vec![], "price".to_string());
1214
1215 let money = crate::planning::semantics::LemmaType::primitive(
1216 crate::planning::semantics::TypeSpecification::Scale {
1217 minimum: None,
1218 maximum: None,
1219 decimals: Some(2),
1220 precision: None,
1221 units: crate::planning::semantics::ScaleUnits::from(vec![
1222 crate::planning::semantics::ScaleUnit {
1223 name: "eur".to_string(),
1224 value: rust_decimal::Decimal::from_str("1.0").unwrap(),
1225 },
1226 ]),
1227 help: String::new(),
1228 },
1229 );
1230 let source = Source::new(
1231 "<test>",
1232 crate::parsing::ast::Span {
1233 start: 0,
1234 end: 0,
1235 line: 1,
1236 col: 0,
1237 },
1238 );
1239 let mut data = IndexMap::new();
1240 data.insert(
1241 data_path.clone(),
1242 crate::planning::semantics::DataDefinition::Value {
1243 value: crate::planning::semantics::LiteralValue::scale_with_type(
1244 rust_decimal::Decimal::from_str("0").unwrap(),
1245 "eur".to_string(),
1246 money.clone(),
1247 ),
1248 source,
1249 },
1250 );
1251
1252 let plan = ExecutionPlan {
1253 spec_name: "test".to_string(),
1254 data,
1255 rules: Vec::new(),
1256 reference_evaluation_order: Vec::new(),
1257 meta: HashMap::new(),
1258 named_types: BTreeMap::new(),
1259 effective: EffectiveDate::Origin,
1260 sources: IndexMap::new(),
1261 };
1262
1263 let mut values = HashMap::new();
1264 values.insert("price".to_string(), "1.234 eur".to_string());
1265
1266 assert!(
1267 plan.with_data_values(values, &default_limits()).is_err(),
1268 "Scale decimals=2 should reject 1.234 eur"
1269 );
1270 }
1271
1272 #[test]
1273 fn test_serialize_deserialize_execution_plan() {
1274 let data_path = DataPath {
1275 segments: vec![],
1276 data: "age".to_string(),
1277 };
1278 let mut data = IndexMap::new();
1279 data.insert(
1280 data_path.clone(),
1281 crate::planning::semantics::DataDefinition::Value {
1282 value: create_number_literal(0.into()),
1283 source: test_source(),
1284 },
1285 );
1286 let plan = ExecutionPlan {
1287 spec_name: "test".to_string(),
1288 data,
1289 rules: Vec::new(),
1290 reference_evaluation_order: Vec::new(),
1291 meta: HashMap::new(),
1292 named_types: BTreeMap::new(),
1293 effective: EffectiveDate::Origin,
1294 sources: IndexMap::new(),
1295 };
1296
1297 let json = serde_json::to_string(&plan).expect("Should serialize");
1298 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1299
1300 assert_eq!(deserialized.spec_name, plan.spec_name);
1301 assert_eq!(deserialized.data.len(), plan.data.len());
1302 assert_eq!(deserialized.rules.len(), plan.rules.len());
1303 }
1304
1305 #[test]
1306 fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
1307 let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
1308 let imported_type = crate::planning::semantics::LemmaType::new(
1309 "salary".to_string(),
1310 TypeSpecification::scale(),
1311 crate::planning::semantics::TypeExtends::Custom {
1312 parent: "money".to_string(),
1313 family: "money".to_string(),
1314 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
1315 spec: Arc::clone(&dep_spec),
1316 },
1317 },
1318 );
1319
1320 let mut named_types = BTreeMap::new();
1321 named_types.insert("salary".to_string(), imported_type);
1322
1323 let plan = ExecutionPlan {
1324 spec_name: "test".to_string(),
1325 data: IndexMap::new(),
1326 rules: Vec::new(),
1327 reference_evaluation_order: Vec::new(),
1328 meta: HashMap::new(),
1329 named_types,
1330 effective: EffectiveDate::Origin,
1331 sources: IndexMap::new(),
1332 };
1333
1334 let json = serde_json::to_string(&plan).expect("Should serialize");
1335 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1336
1337 let recovered = deserialized
1338 .named_types
1339 .get("salary")
1340 .expect("salary type should be present");
1341 match &recovered.extends {
1342 crate::planning::semantics::TypeExtends::Custom {
1343 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
1344 ..
1345 } => {
1346 assert_eq!(spec.name, "examples");
1347 }
1348 other => panic!(
1349 "Expected imported defining_spec after round-trip, got {:?}",
1350 other
1351 ),
1352 }
1353 }
1354
1355 #[test]
1356 fn test_serialize_deserialize_plan_with_rules() {
1357 use crate::planning::semantics::ExpressionKind;
1358
1359 let age_path = DataPath::new(vec![], "age".to_string());
1360 let mut data = IndexMap::new();
1361 data.insert(
1362 age_path.clone(),
1363 crate::planning::semantics::DataDefinition::Value {
1364 value: create_number_literal(0.into()),
1365 source: test_source(),
1366 },
1367 );
1368 let mut plan = ExecutionPlan {
1369 spec_name: "test".to_string(),
1370 data,
1371 rules: Vec::new(),
1372 reference_evaluation_order: Vec::new(),
1373 meta: HashMap::new(),
1374 named_types: BTreeMap::new(),
1375 effective: EffectiveDate::Origin,
1376 sources: IndexMap::new(),
1377 };
1378
1379 let rule = ExecutableRule {
1380 path: RulePath::new(vec![], "can_drive".to_string()),
1381 name: "can_drive".to_string(),
1382 branches: vec![Branch {
1383 condition: Some(Expression::new(
1384 ExpressionKind::Comparison(
1385 Arc::new(create_data_path_expr(age_path.clone())),
1386 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1387 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1388 ),
1389 test_source(),
1390 )),
1391 result: create_literal_expr(create_boolean_literal(true)),
1392 source: test_source(),
1393 }],
1394 needs_data: BTreeSet::from([age_path]),
1395 source: test_source(),
1396 rule_type: primitive_boolean().clone(),
1397 };
1398
1399 plan.rules.push(rule);
1400
1401 let json = serde_json::to_string(&plan).expect("Should serialize");
1402 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1403
1404 assert_eq!(deserialized.spec_name, plan.spec_name);
1405 assert_eq!(deserialized.data.len(), plan.data.len());
1406 assert_eq!(deserialized.rules.len(), plan.rules.len());
1407 assert_eq!(deserialized.rules[0].name, "can_drive");
1408 assert_eq!(deserialized.rules[0].branches.len(), 1);
1409 assert_eq!(deserialized.rules[0].needs_data.len(), 1);
1410 }
1411
1412 #[test]
1413 fn test_serialize_deserialize_plan_with_nested_data_paths() {
1414 use crate::planning::semantics::PathSegment;
1415 let data_path = DataPath {
1416 segments: vec![PathSegment {
1417 data: "employee".to_string(),
1418 spec: "private".to_string(),
1419 }],
1420 data: "salary".to_string(),
1421 };
1422
1423 let mut data = IndexMap::new();
1424 data.insert(
1425 data_path.clone(),
1426 crate::planning::semantics::DataDefinition::Value {
1427 value: create_number_literal(0.into()),
1428 source: test_source(),
1429 },
1430 );
1431 let plan = ExecutionPlan {
1432 spec_name: "test".to_string(),
1433 data,
1434 rules: Vec::new(),
1435 reference_evaluation_order: Vec::new(),
1436 meta: HashMap::new(),
1437 named_types: BTreeMap::new(),
1438 effective: EffectiveDate::Origin,
1439 sources: IndexMap::new(),
1440 };
1441
1442 let json = serde_json::to_string(&plan).expect("Should serialize");
1443 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1444
1445 assert_eq!(deserialized.data.len(), 1);
1446 let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
1447 assert_eq!(deserialized_path.segments.len(), 1);
1448 assert_eq!(deserialized_path.segments[0].data, "employee");
1449 assert_eq!(deserialized_path.data, "salary");
1450 }
1451
1452 #[test]
1453 fn test_serialize_deserialize_plan_with_multiple_data_types() {
1454 let name_path = DataPath::new(vec![], "name".to_string());
1455 let age_path = DataPath::new(vec![], "age".to_string());
1456 let active_path = DataPath::new(vec![], "active".to_string());
1457
1458 let mut data = IndexMap::new();
1459 data.insert(
1460 name_path.clone(),
1461 crate::planning::semantics::DataDefinition::Value {
1462 value: create_text_literal("Alice".to_string()),
1463 source: test_source(),
1464 },
1465 );
1466 data.insert(
1467 age_path.clone(),
1468 crate::planning::semantics::DataDefinition::Value {
1469 value: create_number_literal(30.into()),
1470 source: test_source(),
1471 },
1472 );
1473 data.insert(
1474 active_path.clone(),
1475 crate::planning::semantics::DataDefinition::Value {
1476 value: create_boolean_literal(true),
1477 source: test_source(),
1478 },
1479 );
1480
1481 let plan = ExecutionPlan {
1482 spec_name: "test".to_string(),
1483 data,
1484 rules: Vec::new(),
1485 reference_evaluation_order: Vec::new(),
1486 meta: HashMap::new(),
1487 named_types: BTreeMap::new(),
1488 effective: EffectiveDate::Origin,
1489 sources: IndexMap::new(),
1490 };
1491
1492 let json = serde_json::to_string(&plan).expect("Should serialize");
1493 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1494
1495 assert_eq!(deserialized.data.len(), 3);
1496
1497 assert_eq!(
1498 deserialized.get_data_value(&name_path).unwrap().value,
1499 crate::planning::semantics::ValueKind::Text("Alice".to_string())
1500 );
1501 assert_eq!(
1502 deserialized.get_data_value(&age_path).unwrap().value,
1503 crate::planning::semantics::ValueKind::Number(30.into())
1504 );
1505 assert_eq!(
1506 deserialized.get_data_value(&active_path).unwrap().value,
1507 crate::planning::semantics::ValueKind::Boolean(true)
1508 );
1509 }
1510
1511 #[test]
1512 fn test_serialize_deserialize_plan_with_multiple_branches() {
1513 use crate::planning::semantics::ExpressionKind;
1514
1515 let points_path = DataPath::new(vec![], "points".to_string());
1516 let mut data = IndexMap::new();
1517 data.insert(
1518 points_path.clone(),
1519 crate::planning::semantics::DataDefinition::Value {
1520 value: create_number_literal(0.into()),
1521 source: test_source(),
1522 },
1523 );
1524 let mut plan = ExecutionPlan {
1525 spec_name: "test".to_string(),
1526 data,
1527 rules: Vec::new(),
1528 reference_evaluation_order: Vec::new(),
1529 meta: HashMap::new(),
1530 named_types: BTreeMap::new(),
1531 effective: EffectiveDate::Origin,
1532 sources: IndexMap::new(),
1533 };
1534
1535 let rule = ExecutableRule {
1536 path: RulePath::new(vec![], "tier".to_string()),
1537 name: "tier".to_string(),
1538 branches: vec![
1539 Branch {
1540 condition: None,
1541 result: create_literal_expr(create_text_literal("bronze".to_string())),
1542 source: test_source(),
1543 },
1544 Branch {
1545 condition: Some(Expression::new(
1546 ExpressionKind::Comparison(
1547 Arc::new(create_data_path_expr(points_path.clone())),
1548 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1549 Arc::new(create_literal_expr(create_number_literal(100.into()))),
1550 ),
1551 test_source(),
1552 )),
1553 result: create_literal_expr(create_text_literal("silver".to_string())),
1554 source: test_source(),
1555 },
1556 Branch {
1557 condition: Some(Expression::new(
1558 ExpressionKind::Comparison(
1559 Arc::new(create_data_path_expr(points_path.clone())),
1560 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1561 Arc::new(create_literal_expr(create_number_literal(500.into()))),
1562 ),
1563 test_source(),
1564 )),
1565 result: create_literal_expr(create_text_literal("gold".to_string())),
1566 source: test_source(),
1567 },
1568 ],
1569 needs_data: BTreeSet::from([points_path]),
1570 source: test_source(),
1571 rule_type: primitive_text().clone(),
1572 };
1573
1574 plan.rules.push(rule);
1575
1576 let json = serde_json::to_string(&plan).expect("Should serialize");
1577 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1578
1579 assert_eq!(deserialized.rules.len(), 1);
1580 assert_eq!(deserialized.rules[0].branches.len(), 3);
1581 assert!(deserialized.rules[0].branches[0].condition.is_none());
1582 assert!(deserialized.rules[0].branches[1].condition.is_some());
1583 assert!(deserialized.rules[0].branches[2].condition.is_some());
1584 }
1585
1586 #[test]
1587 fn test_serialize_deserialize_empty_plan() {
1588 let plan = ExecutionPlan {
1589 spec_name: "empty".to_string(),
1590 data: IndexMap::new(),
1591 rules: Vec::new(),
1592 reference_evaluation_order: Vec::new(),
1593 meta: HashMap::new(),
1594 named_types: BTreeMap::new(),
1595 effective: EffectiveDate::Origin,
1596 sources: IndexMap::new(),
1597 };
1598
1599 let json = serde_json::to_string(&plan).expect("Should serialize");
1600 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1601
1602 assert_eq!(deserialized.spec_name, "empty");
1603 assert_eq!(deserialized.data.len(), 0);
1604 assert_eq!(deserialized.rules.len(), 0);
1605 }
1606
1607 #[test]
1608 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
1609 use crate::planning::semantics::ExpressionKind;
1610
1611 let x_path = DataPath::new(vec![], "x".to_string());
1612 let mut data = IndexMap::new();
1613 data.insert(
1614 x_path.clone(),
1615 crate::planning::semantics::DataDefinition::Value {
1616 value: create_number_literal(0.into()),
1617 source: test_source(),
1618 },
1619 );
1620 let mut plan = ExecutionPlan {
1621 spec_name: "test".to_string(),
1622 data,
1623 rules: Vec::new(),
1624 reference_evaluation_order: Vec::new(),
1625 meta: HashMap::new(),
1626 named_types: BTreeMap::new(),
1627 effective: EffectiveDate::Origin,
1628 sources: IndexMap::new(),
1629 };
1630
1631 let rule = ExecutableRule {
1632 path: RulePath::new(vec![], "doubled".to_string()),
1633 name: "doubled".to_string(),
1634 branches: vec![Branch {
1635 condition: None,
1636 result: Expression::new(
1637 ExpressionKind::Arithmetic(
1638 Arc::new(create_data_path_expr(x_path.clone())),
1639 crate::parsing::ast::ArithmeticComputation::Multiply,
1640 Arc::new(create_literal_expr(create_number_literal(2.into()))),
1641 ),
1642 test_source(),
1643 ),
1644 source: test_source(),
1645 }],
1646 needs_data: BTreeSet::from([x_path]),
1647 source: test_source(),
1648 rule_type: crate::planning::semantics::primitive_number().clone(),
1649 };
1650
1651 plan.rules.push(rule);
1652
1653 let json = serde_json::to_string(&plan).expect("Should serialize");
1654 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1655
1656 assert_eq!(deserialized.rules.len(), 1);
1657 match &deserialized.rules[0].branches[0].result.kind {
1658 ExpressionKind::Arithmetic(left, op, right) => {
1659 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
1660 match &left.kind {
1661 ExpressionKind::DataPath(_) => {}
1662 _ => panic!("Expected DataPath in left operand"),
1663 }
1664 match &right.kind {
1665 ExpressionKind::Literal(_) => {}
1666 _ => panic!("Expected Literal in right operand"),
1667 }
1668 }
1669 _ => panic!("Expected Arithmetic expression"),
1670 }
1671 }
1672
1673 #[test]
1674 fn test_serialize_deserialize_round_trip_equality() {
1675 use crate::planning::semantics::ExpressionKind;
1676
1677 let age_path = DataPath::new(vec![], "age".to_string());
1678 let mut data = IndexMap::new();
1679 data.insert(
1680 age_path.clone(),
1681 crate::planning::semantics::DataDefinition::Value {
1682 value: create_number_literal(0.into()),
1683 source: test_source(),
1684 },
1685 );
1686 let mut 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 named_types: BTreeMap::new(),
1693 effective: EffectiveDate::Origin,
1694 sources: IndexMap::new(),
1695 };
1696
1697 let rule = ExecutableRule {
1698 path: RulePath::new(vec![], "is_adult".to_string()),
1699 name: "is_adult".to_string(),
1700 branches: vec![Branch {
1701 condition: Some(Expression::new(
1702 ExpressionKind::Comparison(
1703 Arc::new(create_data_path_expr(age_path.clone())),
1704 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1705 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1706 ),
1707 test_source(),
1708 )),
1709 result: create_literal_expr(create_boolean_literal(true)),
1710 source: test_source(),
1711 }],
1712 needs_data: BTreeSet::from([age_path]),
1713 source: test_source(),
1714 rule_type: primitive_boolean().clone(),
1715 };
1716
1717 plan.rules.push(rule);
1718
1719 let json = serde_json::to_string(&plan).expect("Should serialize");
1720 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1721
1722 let json2 = serde_json::to_string(&deserialized).expect("Should serialize again");
1723 let deserialized2: ExecutionPlan =
1724 serde_json::from_str(&json2).expect("Should deserialize again");
1725
1726 assert_eq!(deserialized2.spec_name, plan.spec_name);
1727 assert_eq!(deserialized2.data.len(), plan.data.len());
1728 assert_eq!(deserialized2.rules.len(), plan.rules.len());
1729 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
1730 assert_eq!(
1731 deserialized2.rules[0].branches.len(),
1732 plan.rules[0].branches.len()
1733 );
1734 }
1735
1736 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
1737 ExecutionPlan {
1738 spec_name: "s".into(),
1739 data: IndexMap::new(),
1740 rules: Vec::new(),
1741 reference_evaluation_order: Vec::new(),
1742 meta: HashMap::new(),
1743 named_types: BTreeMap::new(),
1744 effective,
1745 sources: IndexMap::new(),
1746 }
1747 }
1748
1749 #[test]
1750 fn plan_at_exact_boundary_selects_later_slice() {
1751 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
1752
1753 let june = DateTimeValue {
1754 year: 2025,
1755 month: 6,
1756 day: 1,
1757 hour: 0,
1758 minute: 0,
1759 second: 0,
1760 microsecond: 0,
1761 timezone: None,
1762 };
1763 let dec = DateTimeValue {
1764 year: 2025,
1765 month: 12,
1766 day: 1,
1767 hour: 0,
1768 minute: 0,
1769 second: 0,
1770 microsecond: 0,
1771 timezone: None,
1772 };
1773
1774 let set = ExecutionPlanSet {
1775 spec_name: "s".into(),
1776 plans: vec![
1777 empty_plan(EffectiveDate::Origin),
1778 empty_plan(EffectiveDate::DateTimeValue(june.clone())),
1779 empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
1780 ],
1781 };
1782
1783 assert!(std::ptr::eq(
1784 set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
1785 .expect("boundary instant"),
1786 &set.plans[1]
1787 ));
1788 assert!(std::ptr::eq(
1789 set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
1790 .expect("dec boundary"),
1791 &set.plans[2]
1792 ));
1793 }
1794
1795 #[test]
1796 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
1797 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
1798
1799 let june = DateTimeValue {
1800 year: 2025,
1801 month: 6,
1802 day: 1,
1803 hour: 0,
1804 minute: 0,
1805 second: 0,
1806 microsecond: 0,
1807 timezone: None,
1808 };
1809 let may_end = DateTimeValue {
1810 year: 2025,
1811 month: 5,
1812 day: 31,
1813 hour: 23,
1814 minute: 59,
1815 second: 59,
1816 microsecond: 0,
1817 timezone: None,
1818 };
1819
1820 let set = ExecutionPlanSet {
1821 spec_name: "s".into(),
1822 plans: vec![
1823 empty_plan(EffectiveDate::Origin),
1824 empty_plan(EffectiveDate::DateTimeValue(june)),
1825 ],
1826 };
1827
1828 assert!(std::ptr::eq(
1829 set.plan_at(&EffectiveDate::DateTimeValue(may_end))
1830 .expect("may 31"),
1831 &set.plans[0]
1832 ));
1833 }
1834
1835 #[test]
1836 fn plan_at_single_plan_matches_any_instant_after_start() {
1837 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
1838
1839 let t = DateTimeValue {
1840 year: 2025,
1841 month: 3,
1842 day: 1,
1843 hour: 0,
1844 minute: 0,
1845 second: 0,
1846 microsecond: 0,
1847 timezone: None,
1848 };
1849 let set = ExecutionPlanSet {
1850 spec_name: "s".into(),
1851 plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
1852 year: 2025,
1853 month: 1,
1854 day: 1,
1855 hour: 0,
1856 minute: 0,
1857 second: 0,
1858 microsecond: 0,
1859 timezone: None,
1860 }))],
1861 };
1862 assert!(std::ptr::eq(
1863 set.plan_at(&EffectiveDate::DateTimeValue(t))
1864 .expect("inside single slice"),
1865 &set.plans[0]
1866 ));
1867 }
1868
1869 #[test]
1872 fn schema_json_shape_contract() {
1873 let mut engine = Engine::new();
1874 engine
1875 .load(
1876 r#"
1877 spec pricing
1878 data bridge_height: scale
1879 -> unit meter 1
1880 -> default 100 meter
1881 data quantity: number -> minimum 0
1882 rule cost: bridge_height * quantity
1883 "#,
1884 crate::SourceType::Labeled("test.lemma"),
1885 )
1886 .unwrap();
1887 let now = DateTimeValue::now();
1888 let schema = engine.get_plan("pricing", Some(&now)).unwrap().schema();
1889
1890 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
1891
1892 let bh = &value["data"]["bridge_height"];
1893 assert!(
1894 bh.is_object(),
1895 "data entry must be a named object, not tuple"
1896 );
1897 assert!(
1898 bh.get("type").is_some(),
1899 "data entry must expose `type` field"
1900 );
1901 assert!(
1902 bh.get("default").is_some(),
1903 "bridge_height has a promoted default"
1904 );
1905
1906 let ty = &bh["type"];
1907 assert_eq!(
1908 ty["kind"], "scale",
1909 "kind tag sits on the type object itself"
1910 );
1911 assert!(
1912 ty["units"].is_array(),
1913 "scale-only fields flatten up to top level"
1914 );
1915 assert!(
1916 ty.get("options").is_none(),
1917 "text-only fields must not leak"
1918 );
1919
1920 let qty = &value["data"]["quantity"];
1921 assert_eq!(qty["type"]["kind"], "number");
1922 assert!(
1923 qty.get("default").is_none(),
1924 "no declared default means no field"
1925 );
1926
1927 let cost = &value["rules"]["cost"];
1928 assert_eq!(cost["kind"], "scale", "rule types use the same flat shape");
1929 }
1930
1931 #[test]
1932 fn schema_json_round_trip_preserves_shape() {
1933 let mut engine = Engine::new();
1934 engine
1935 .load(
1936 r#"
1937 spec s
1938 data age: number -> minimum 0 -> default 18
1939 data grade: text -> options "A" "B" "C"
1940 rule adult: age >= 18
1941 "#,
1942 crate::SourceType::Labeled("s.lemma"),
1943 )
1944 .unwrap();
1945 let now = DateTimeValue::now();
1946 let schema = engine.get_plan("s", Some(&now)).unwrap().schema();
1947
1948 let json = serde_json::to_string(&schema).unwrap();
1949 let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
1950 assert_eq!(schema, round_tripped);
1951 }
1952}
1953
1954