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