1use crate::computation::UnitResolutionContext;
12use crate::parsing::ast::{CalendarPeriodUnit, DateCalendarKind, DateRelativeKind};
13use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, LemmaSpec, MetaValue};
14use crate::parsing::source::Source;
15use crate::planning::data_input::{parse_data_value, DataValueInput};
16use crate::planning::graph::Graph;
17use crate::planning::graph::ResolvedSpecTypes;
18use crate::planning::normalize::{build_normalized_rule_instructions, CompiledRule};
19use crate::planning::semantics::{
20 value_kind_matches_spec, ArithmeticComputation, ComparisonComputation, DataDefinition,
21 DataPath, Expression, LemmaType, LiteralValue, MathematicalComputation, ReferenceTarget,
22 RulePath, SemanticConversionTarget, TypeSpecification, ValueKind,
23};
24use crate::Error;
25use crate::ResourceLimits;
26use indexmap::IndexMap;
27use serde::{Deserialize, Deserializer, Serialize, Serializer};
28use std::collections::{BTreeSet, HashMap, HashSet};
29use std::sync::Arc;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SpecSource {
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub repository: Option<String>,
41 pub name: String,
42 pub effective_from: EffectiveDate,
43 pub source: String,
44}
45
46pub type SpecSources = Vec<SpecSource>;
47
48#[derive(Debug, Clone)]
53pub struct ExecutionPlan {
54 pub spec_name: String,
56
57 pub commentary: Option<String>,
59
60 pub data: IndexMap<DataPath, DataDefinition>,
62
63 pub rules: Vec<ExecutableRule>,
65
66 pub max_register_count: u16,
68
69 pub reference_evaluation_order: Vec<DataPath>,
74
75 pub meta: HashMap<String, MetaValue>,
77
78 pub resolved_types: ResolvedSpecTypes,
82
83 pub signature_index: crate::computation::arithmetic::SignatureIndex,
89
90 pub effective: EffectiveDate,
91
92 pub sources: SpecSources,
95}
96
97#[derive(Debug, Clone)]
100pub struct ExecutionPlanSet {
101 pub spec_name: String,
102 pub plans: Vec<ExecutionPlan>,
103}
104
105impl ExecutionPlanSet {
106 #[must_use]
108 pub fn plan_at(&self, effective: &EffectiveDate) -> Option<&ExecutionPlan> {
109 for (i, plan) in self.plans.iter().enumerate() {
110 let from_ok = *effective >= plan.effective;
111 let to_ok = self
112 .plans
113 .get(i + 1)
114 .map(|next| *effective < next.effective)
115 .unwrap_or(true);
116 if from_ok && to_ok {
117 return Some(plan);
118 }
119 }
120 None
121 }
122}
123
124pub const INSTRUCTIONS_VERSION: u32 = 2;
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
128#[serde(rename_all = "snake_case")]
129pub enum JumpVetoSemantics {
130 #[default]
132 UnlessExpression,
133 UnlessRuleReference,
135}
136
137pub fn validate_instruction_jumps(code: &[Instruction]) {
141 if let Err(message) = check_instruction_jumps(code) {
142 panic!("BUG: {message}");
143 }
144}
145
146fn check_instruction_jumps(code: &[Instruction]) -> Result<(), String> {
149 let code_len = code.len();
150 for (index, instruction) in code.iter().enumerate() {
151 match instruction {
152 Instruction::JumpIfFalse {
153 target_instruction, ..
154 } => {
155 if *target_instruction == 0 {
156 return Err(format!("unpatched JumpIfFalse at instruction {index}"));
157 }
158 if (*target_instruction as usize) >= code_len {
159 return Err(format!(
160 "JumpIfFalse at instruction {index} targets {target_instruction} past the last instruction (length {code_len})"
161 ));
162 }
163 }
164 Instruction::Jump { target_instruction } => {
165 if *target_instruction == 0 {
166 return Err(format!("unpatched Jump at instruction {index}"));
167 }
168 if (*target_instruction as usize) >= code_len {
169 return Err(format!(
170 "Jump at instruction {index} targets {target_instruction} past the last instruction (length {code_len})"
171 ));
172 }
173 }
174 _ => {}
175 }
176 }
177 Ok(())
178}
179
180pub fn validate_instructions(instructions: &Instructions) -> Result<(), String> {
190 if instructions.version != INSTRUCTIONS_VERSION {
191 return Err(format!(
192 "instructions version {} does not match supported version {}",
193 instructions.version, INSTRUCTIONS_VERSION
194 ));
195 }
196
197 check_instruction_jumps(&instructions.code)?;
198
199 let register_count = instructions.register_count;
200 let constant_count = instructions.constants.len();
201 let data_count = instructions.data_manifest.len();
202 let veto_message_count = instructions.veto_messages.len();
203
204 let check_register = |index: usize, name: &str, register: u16| -> Result<(), String> {
205 if register >= register_count {
206 return Err(format!(
207 "instruction {index} {name} register r{register} is out of bounds (register count {register_count})"
208 ));
209 }
210 Ok(())
211 };
212
213 for (index, instruction) in instructions.code.iter().enumerate() {
214 match instruction {
215 Instruction::LoadConstant {
216 destination_register,
217 constant_index,
218 } => {
219 check_register(index, "destination", *destination_register)?;
220 if (*constant_index as usize) >= constant_count {
221 return Err(format!(
222 "instruction {index} constant index {constant_index} is out of bounds (constant count {constant_count})"
223 ));
224 }
225 }
226 Instruction::LoadData {
227 destination_register,
228 data_index,
229 } => {
230 check_register(index, "destination", *destination_register)?;
231 if (*data_index as usize) >= data_count {
232 return Err(format!(
233 "instruction {index} data index {data_index} is out of bounds (data manifest size {data_count})"
234 ));
235 }
236 }
237 Instruction::LoadNow {
238 destination_register,
239 } => {
240 check_register(index, "destination", *destination_register)?;
241 }
242 Instruction::Arithmetic {
243 destination_register,
244 operation: _,
245 left_register,
246 right_register,
247 }
248 | Instruction::Comparison {
249 destination_register,
250 operation: _,
251 left_register,
252 right_register,
253 }
254 | Instruction::RangeLiteral {
255 destination_register,
256 left_register,
257 right_register,
258 } => {
259 check_register(index, "destination", *destination_register)?;
260 check_register(index, "left", *left_register)?;
261 check_register(index, "right", *right_register)?;
262 }
263 Instruction::UnitConversion {
264 destination_register,
265 source_register,
266 target: _,
267 }
268 | Instruction::Mathematical {
269 destination_register,
270 operation: _,
271 source_register,
272 }
273 | Instruction::DateRelative {
274 destination_register,
275 kind: _,
276 source_register,
277 }
278 | Instruction::DateCalendar {
279 destination_register,
280 kind: _,
281 unit: _,
282 source_register,
283 }
284 | Instruction::PastFutureRange {
285 destination_register,
286 kind: _,
287 source_register,
288 }
289 | Instruction::ResultIsVeto {
290 destination_register,
291 source_register,
292 }
293 | Instruction::MoveRegister {
294 destination_register,
295 source_register,
296 } => {
297 check_register(index, "destination", *destination_register)?;
298 check_register(index, "source", *source_register)?;
299 }
300 Instruction::RangeContainment {
301 destination_register,
302 value_register,
303 range_register,
304 } => {
305 check_register(index, "destination", *destination_register)?;
306 check_register(index, "value", *value_register)?;
307 check_register(index, "range", *range_register)?;
308 }
309 Instruction::UserVeto {
310 destination_register,
311 message_index,
312 } => {
313 check_register(index, "destination", *destination_register)?;
314 if (*message_index as usize) >= veto_message_count {
315 return Err(format!(
316 "instruction {index} veto message index {message_index} is out of bounds (veto message count {veto_message_count})"
317 ));
318 }
319 }
320 Instruction::JumpIfFalse {
321 condition_register,
322 target_instruction: _,
323 veto_semantics: _,
324 } => {
325 check_register(index, "condition", *condition_register)?;
326 }
327 Instruction::Jump {
328 target_instruction: _,
329 } => {}
330 Instruction::Return { source_register } => {
331 check_register(index, "source", *source_register)?;
332 }
333 }
334 }
335
336 match instructions.code.last() {
337 Some(Instruction::Return { .. }) => {}
338 Some(other) => {
339 return Err(format!(
340 "instruction stream must end with Return, found {other:?}"
341 ))
342 }
343 None => return Err("instruction stream is empty".to_string()),
344 }
345
346 let code_len = instructions.code.len();
347 for tag in &instructions.arm_tags {
348 if (tag.pc as usize) >= code_len {
349 return Err(format!(
350 "arm tag pc {} is out of bounds (code length {code_len})",
351 tag.pc
352 ));
353 }
354 let tagged = &instructions.code[tag.pc as usize];
355 let valid = match tag.role {
356 ArmRole::Condition => matches!(tagged, Instruction::JumpIfFalse { .. }),
357 ArmRole::Result => matches!(tagged, Instruction::Return { .. }),
358 };
359 if !valid {
360 return Err(format!(
361 "arm tag at pc {} does not match its instruction {tagged:?}",
362 tag.pc
363 ));
364 }
365 }
366 for tag in &instructions.conversion_tags {
367 if (tag.pc as usize) >= code_len {
368 return Err(format!(
369 "conversion tag pc {} is out of bounds (code length {code_len})",
370 tag.pc
371 ));
372 }
373 if !matches!(
374 instructions.code[tag.pc as usize],
375 Instruction::UnitConversion { .. }
376 ) {
377 return Err(format!(
378 "conversion tag at pc {} does not reference a UnitConversion instruction",
379 tag.pc
380 ));
381 }
382 }
383
384 Ok(())
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct Instructions {
390 pub version: u32,
391 pub register_count: u16,
392 #[serde(with = "register_types_serde")]
393 pub register_types: Vec<Arc<LemmaType>>,
394 pub constants: Vec<LiteralValue>,
395 pub data_manifest: Vec<DataPath>,
396 pub veto_messages: Vec<String>,
397 pub code: Vec<Instruction>,
398 #[serde(default, skip_serializing_if = "Vec::is_empty")]
403 pub arm_tags: Vec<ArmTag>,
404 #[serde(default, skip_serializing_if = "Vec::is_empty")]
409 pub conversion_tags: Vec<ConversionTag>,
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
414#[serde(rename_all = "snake_case")]
415pub enum ArmRole {
416 Condition,
418 Result,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
427pub struct ArmTag {
428 pub pc: u32,
429 pub arm: u16,
430 pub role: ArmRole,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct ConversionTag {
437 pub pc: u32,
438 pub source: Source,
439}
440
441mod register_types_serde {
442 use super::LemmaType;
443 use serde::{Deserialize, Deserializer, Serialize, Serializer};
444 use std::sync::Arc;
445
446 pub fn serialize<S>(values: &[Arc<LemmaType>], serializer: S) -> Result<S::Ok, S::Error>
447 where
448 S: Serializer,
449 {
450 let refs: Vec<&LemmaType> = values.iter().map(|v| v.as_ref()).collect();
451 refs.serialize(serializer)
452 }
453
454 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Arc<LemmaType>>, D::Error>
455 where
456 D: Deserializer<'de>,
457 {
458 let values: Vec<LemmaType> = Vec::deserialize(deserializer)?;
459 Ok(values.into_iter().map(Arc::new).collect())
460 }
461}
462
463#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
465#[serde(rename_all = "snake_case")]
466pub enum Instruction {
467 LoadConstant {
468 destination_register: u16,
469 constant_index: u16,
470 },
471 LoadData {
472 destination_register: u16,
473 data_index: u16,
474 },
475 LoadNow {
476 destination_register: u16,
477 },
478 Arithmetic {
479 destination_register: u16,
480 operation: ArithmeticComputation,
481 left_register: u16,
482 right_register: u16,
483 },
484 Comparison {
485 destination_register: u16,
486 operation: ComparisonComputation,
487 left_register: u16,
488 right_register: u16,
489 },
490 UnitConversion {
491 destination_register: u16,
492 source_register: u16,
493 target: SemanticConversionTarget,
494 },
495 Mathematical {
496 destination_register: u16,
497 operation: MathematicalComputation,
498 source_register: u16,
499 },
500 DateRelative {
501 destination_register: u16,
502 kind: DateRelativeKind,
503 source_register: u16,
504 },
505 DateCalendar {
506 destination_register: u16,
507 kind: DateCalendarKind,
508 unit: CalendarPeriodUnit,
509 source_register: u16,
510 },
511 RangeLiteral {
512 destination_register: u16,
513 left_register: u16,
514 right_register: u16,
515 },
516 PastFutureRange {
517 destination_register: u16,
518 kind: DateRelativeKind,
519 source_register: u16,
520 },
521 RangeContainment {
522 destination_register: u16,
523 value_register: u16,
524 range_register: u16,
525 },
526 ResultIsVeto {
527 destination_register: u16,
528 source_register: u16,
529 },
530 MoveRegister {
531 destination_register: u16,
532 source_register: u16,
533 },
534 UserVeto {
535 destination_register: u16,
536 message_index: u16,
537 },
538 JumpIfFalse {
539 condition_register: u16,
540 target_instruction: u32,
541 #[serde(default)]
542 veto_semantics: JumpVetoSemantics,
543 },
544 Jump {
545 target_instruction: u32,
546 },
547 Return {
548 source_register: u16,
549 },
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct ExecutableRule {
557 pub path: RulePath,
559
560 pub name: String,
562
563 pub branches: Vec<Branch>,
565
566 pub instructions: Instructions,
568
569 pub source_instructions: Instructions,
574
575 pub source: Source,
577
578 #[serde(with = "arc_lemma_type")]
581 pub rule_type: Arc<LemmaType>,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct Branch {
587 pub condition: Option<Expression>,
589
590 pub result: Expression,
592
593 pub source: Source,
595}
596
597mod arc_lemma_type {
598 use super::LemmaType;
599 use serde::{Deserialize, Deserializer, Serialize, Serializer};
600 use std::sync::Arc;
601
602 pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
603 where
604 S: Serializer,
605 {
606 value.as_ref().serialize(serializer)
607 }
608
609 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
610 where
611 D: Deserializer<'de>,
612 {
613 LemmaType::deserialize(deserializer).map(Arc::new)
614 }
615}
616
617pub(crate) fn build_execution_plan(
620 graph: &Graph,
621 resolved_types: &mut Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>, ResolvedSpecTypes)>,
622 effective: &EffectiveDate,
623 limits: &crate::limits::ResourceLimits,
624) -> Result<ExecutionPlan, Vec<Error>> {
625 let execution_order = graph.execution_order();
626
627 let main_spec = graph.main_spec();
628 let main_idx = resolved_types
629 .iter()
630 .position(|(_, spec, _)| Arc::ptr_eq(spec, main_spec));
631
632 let mut sources: SpecSources = Vec::new();
633 for (repo, spec, _) in resolved_types.iter() {
634 if !sources.iter().any(|e| {
635 e.repository == repo.name
636 && e.name == spec.name
637 && e.effective_from == spec.effective_from
638 }) {
639 sources.push(SpecSource {
640 repository: repo.name.clone(),
641 name: spec.name.clone(),
642 effective_from: spec.effective_from.clone(),
643 source: crate::formatting::format_specs(&[spec.as_ref().clone()]),
644 });
645 }
646 }
647
648 let main_resolved_types = main_idx
649 .map(|idx| resolved_types.remove(idx).2)
650 .unwrap_or_default();
651 let data = graph.build_data(&main_resolved_types.resolved);
652
653 let undetermined_errors: Vec<Error> = data
660 .iter()
661 .filter_map(|(path, definition)| {
662 let (resolved_type, source) = match definition {
663 DataDefinition::TypeDeclaration {
664 resolved_type,
665 source,
666 ..
667 } => (resolved_type, source),
668 DataDefinition::Reference {
669 target: ReferenceTarget::Data(_),
670 resolved_type,
671 source,
672 ..
673 } => (resolved_type, source),
674 DataDefinition::Reference {
675 target: ReferenceTarget::Rule(_),
676 ..
677 }
678 | DataDefinition::Value { .. }
679 | DataDefinition::Import { .. } => return None,
680 };
681 if resolved_type.is_undetermined() {
682 Some(Error::validation(
683 format!("could not determine the type of '{path}'"),
684 Some(source.clone()),
685 None::<String>,
686 ))
687 } else {
688 None
689 }
690 })
691 .collect();
692 if !undetermined_errors.is_empty() {
693 return Err(undetermined_errors);
694 }
695
696 let signature_index = crate::planning::graph::build_signature_index(
697 &main_spec.name,
698 &main_resolved_types.unit_index,
699 )
700 .expect("BUG: signature_index build already validated during resolve_and_validate");
701
702 let mut executable_rules: Vec<ExecutableRule> = Vec::new();
703 let mut max_register_count: u16 = 0;
704 let plan_rule_paths: HashSet<RulePath> = graph.rules().keys().cloned().collect();
705 let mut completed_rules: HashMap<RulePath, Arc<Expression>> = HashMap::new();
706
707 for rule_path in execution_order {
708 let rule_node = graph.rules().get(rule_path).expect(
709 "bug: rule from topological sort not in graph - validation should have caught this",
710 );
711
712 let mut executable_branches = Vec::new();
713 for (condition, result) in &rule_node.branches {
714 executable_branches.push(Branch {
715 condition: condition.clone(),
716 result: result.clone(),
717 source: rule_node.source.clone(),
718 });
719 }
720
721 let unit_ctx = UnitResolutionContext::WithIndex(&main_resolved_types.unit_index);
722 let compiled = build_normalized_rule_instructions(
723 &rule_node.branches,
724 &completed_rules,
725 &plan_rule_paths,
726 &data,
727 &unit_ctx,
728 Some(rule_node.source.clone()),
729 &rule_node.rule_type,
730 limits.max_normalized_expression_nodes,
731 )
732 .map_err(|error| vec![error])?;
736 let CompiledRule {
737 instructions,
738 source_instructions,
739 inlined_expression,
740 } = compiled;
741 max_register_count = max_register_count
742 .max(instructions.register_count)
743 .max(source_instructions.register_count);
744 completed_rules.insert(rule_path.clone(), inlined_expression);
745
746 executable_rules.push(ExecutableRule {
747 path: rule_path.clone(),
748 name: rule_path.rule.clone(),
749 branches: executable_branches,
750 instructions,
751 source_instructions,
752 source: rule_node.source.clone(),
753 rule_type: Arc::clone(&rule_node.rule_type),
754 });
755 }
756
757 Ok(ExecutionPlan {
758 spec_name: main_spec.name.clone(),
759 commentary: main_spec.commentary.clone(),
760 data,
761 rules: executable_rules,
762 max_register_count,
763 reference_evaluation_order: graph.reference_evaluation_order().to_vec(),
764 meta: main_spec
765 .meta_fields
766 .iter()
767 .map(|f| (f.key.clone(), f.value.clone()))
768 .collect(),
769 resolved_types: main_resolved_types,
770 signature_index,
771 effective: effective.clone(),
772 sources,
773 })
774}
775
776#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
800pub struct DataEntry {
801 #[serde(rename = "type")]
802 pub lemma_type: LemmaType,
803 #[serde(skip_serializing_if = "Option::is_none", default)]
804 pub bound_value: Option<LiteralValue>,
805 #[serde(skip_serializing_if = "Option::is_none", default)]
806 pub default: Option<LiteralValue>,
807}
808
809#[derive(Debug, Clone, Default)]
815pub struct DataOverlay {
816 pub values: HashMap<DataPath, LiteralValue>,
818 pub violated: HashMap<DataPath, String>,
821}
822
823impl DataOverlay {
824 pub fn resolve(
830 plan: &ExecutionPlan,
831 raw_values: HashMap<String, DataValueInput>,
832 limits: &ResourceLimits,
833 ) -> Result<Self, Error> {
834 let mut overlay = Self::default();
835
836 for (name, raw_value) in raw_values {
837 let data_path = plan.get_data_path_by_str(&name).ok_or_else(|| {
838 let available: Vec<String> = plan.data.keys().map(|p| p.input_key()).collect();
839 Error::request(
840 format!(
841 "Data '{}' not found. Available data: {}",
842 name,
843 available.join(", ")
844 ),
845 None::<String>,
846 )
847 })?;
848 let data_path = data_path.clone();
849
850 let data_definition = plan
851 .data
852 .get(&data_path)
853 .expect("BUG: data_path was just resolved from plan.data, must exist");
854
855 let data_source = data_definition.source().clone();
856 let type_arc = match data_definition {
857 DataDefinition::TypeDeclaration { resolved_type, .. }
858 | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
859 DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
860 DataDefinition::Import { .. } => {
861 return Err(Error::request(
862 format!(
863 "Data '{}' is a spec reference; cannot provide a value.",
864 name
865 ),
866 None::<String>,
867 ));
868 }
869 };
870
871 let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
872 Ok(value) => value,
873 Err(error) => {
874 overlay
875 .violated
876 .insert(data_path, error.message().to_string());
877 continue;
878 }
879 };
880
881 let size = literal_value.byte_size();
882 if size > limits.max_data_value_bytes {
883 return Err(Error::resource_limit_exceeded(
884 "max_data_value_bytes",
885 limits.max_data_value_bytes.to_string(),
886 size.to_string(),
887 format!(
888 "Reduce the size of data values to {} bytes or less",
889 limits.max_data_value_bytes
890 ),
891 Some(data_source.clone()),
892 None,
893 None,
894 )
895 .with_related_data(&name));
896 }
897
898 if let Err(message) = validate_value_against_type(type_arc.as_ref(), &literal_value) {
899 overlay.violated.insert(data_path, message);
900 continue;
901 }
902
903 overlay.values.insert(data_path, literal_value);
904 }
905
906 Ok(overlay)
907 }
908
909 pub fn is_empty(&self) -> bool {
910 self.values.is_empty() && self.violated.is_empty()
911 }
912}
913
914pub(crate) fn build_known_values(
922 plan: &ExecutionPlan,
923 overlay: &DataOverlay,
924) -> HashMap<DataPath, LiteralValue> {
925 let mut known_values: HashMap<DataPath, LiteralValue> = plan
926 .data
927 .iter()
928 .filter_map(|(path, definition)| {
929 if overlay.violated.contains_key(path) {
930 return None;
931 }
932 definition
933 .value()
934 .map(|value| (path.clone(), value.clone()))
935 })
936 .collect();
937
938 for (path, value) in &overlay.values {
939 known_values.insert(path.clone(), value.clone());
940 }
941
942 known_values
943}
944
945fn schema_bound_value(
946 path: &DataPath,
947 data: &DataDefinition,
948 overlay: &DataOverlay,
949) -> Option<LiteralValue> {
950 if let Some(value) = overlay.values.get(path) {
951 return Some(value.clone());
952 }
953 data.bound_value().cloned()
954}
955
956#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
957pub struct SpecSchema {
958 pub spec: String,
960 #[serde(skip_serializing_if = "Option::is_none", default)]
962 pub commentary: Option<String>,
963 #[serde(skip_serializing_if = "Option::is_none", default)]
965 pub effective: Option<DateTimeValue>,
966 #[serde(skip_serializing_if = "Vec::is_empty", default)]
969 pub versions: Vec<DateTimeValue>,
970 pub data: indexmap::IndexMap<String, DataEntry>,
972 pub rules: indexmap::IndexMap<String, LemmaType>,
974 pub meta: HashMap<String, MetaValue>,
976}
977
978impl std::fmt::Display for SpecSchema {
979 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
980 write!(f, "Spec: {}", self.spec)?;
981
982 if let Some(commentary) = &self.commentary {
983 write!(f, "\n {}", commentary)?;
984 }
985
986 if !self.meta.is_empty() {
987 write!(f, "\n\nMeta:")?;
988 let mut entries: Vec<(&String, &MetaValue)> = self.meta.iter().collect();
990 entries.sort_by_key(|(k, _)| *k);
991 for (key, value) in entries {
992 write!(f, "\n {}: {}", key, value)?;
993 }
994 }
995
996 if !self.data.is_empty() {
997 write!(f, "\n\nData:")?;
998 for (name, entry) in &self.data {
999 write!(f, "\n {} ({})", name, entry.lemma_type.specifications)?;
1000 for line in type_detail_lines(&entry.lemma_type.specifications) {
1001 write!(f, "\n {}", line)?;
1002 }
1003 let help = entry.lemma_type.specifications.help();
1004 if !help.is_empty() {
1005 write!(f, "\n help: {}", help)?;
1006 }
1007 if let Some(val) = &entry.bound_value {
1008 write!(f, "\n value: {}", val)?;
1009 }
1010 if let Some(val) = &entry.default {
1011 write!(f, "\n default: {}", val)?;
1012 }
1013 }
1014 }
1015
1016 if !self.rules.is_empty() {
1017 write!(f, "\n\nRules:")?;
1018 for (name, rule_type) in &self.rules {
1019 write!(f, "\n {} ({})", name, rule_type.specifications)?;
1020 }
1021 }
1022
1023 if self.data.is_empty() && self.rules.is_empty() {
1024 write!(f, "\n (no data or rules)")?;
1025 }
1026
1027 Ok(())
1028 }
1029}
1030
1031pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
1037 use crate::computation::rational::rational_to_display_str;
1038 let mut lines = Vec::new();
1039 match spec {
1040 TypeSpecification::Quantity {
1041 minimum,
1042 maximum,
1043 decimals,
1044 units,
1045 ..
1046 } => {
1047 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1048 if !unit_names.is_empty() {
1049 lines.push(format!("units: {}", unit_names.join(", ")));
1050 }
1051 if let Some(d) = decimals {
1052 lines.push(format!("decimals: {}", d));
1053 }
1054 if let Some((magnitude, unit_name)) = minimum {
1055 lines.push(format!(
1056 "minimum: {} {}",
1057 rational_to_display_str(magnitude),
1058 unit_name
1059 ));
1060 }
1061 if let Some((magnitude, unit_name)) = maximum {
1062 lines.push(format!(
1063 "maximum: {} {}",
1064 rational_to_display_str(magnitude),
1065 unit_name
1066 ));
1067 }
1068 }
1069 TypeSpecification::Number {
1070 minimum,
1071 maximum,
1072 decimals,
1073 ..
1074 } => {
1075 if let Some(d) = decimals {
1076 lines.push(format!("decimals: {}", d));
1077 }
1078 if let Some(v) = minimum {
1079 lines.push(format!("minimum: {}", rational_to_display_str(v)));
1080 }
1081 if let Some(v) = maximum {
1082 lines.push(format!("maximum: {}", rational_to_display_str(v)));
1083 }
1084 }
1085 TypeSpecification::Ratio {
1086 minimum,
1087 maximum,
1088 decimals,
1089 units,
1090 ..
1091 } => {
1092 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1093 if !unit_names.is_empty() {
1094 lines.push(format!("units: {}", unit_names.join(", ")));
1095 }
1096 if let Some(d) = decimals {
1097 lines.push(format!("decimals: {}", d));
1098 }
1099 if let Some(v) = minimum {
1100 lines.push(format!("minimum: {}", rational_to_display_str(v)));
1101 }
1102 if let Some(v) = maximum {
1103 lines.push(format!("maximum: {}", rational_to_display_str(v)));
1104 }
1105 }
1106 TypeSpecification::Text {
1107 options, length, ..
1108 } => {
1109 if let Some(l) = length {
1110 lines.push(format!("length: {}", l));
1111 }
1112 if !options.is_empty() {
1113 let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
1114 lines.push(format!("options: {}", quoted.join(", ")));
1115 }
1116 }
1117 TypeSpecification::Date {
1118 minimum, maximum, ..
1119 } => {
1120 if let Some(v) = minimum {
1121 lines.push(format!("minimum: {}", v));
1122 }
1123 if let Some(v) = maximum {
1124 lines.push(format!("maximum: {}", v));
1125 }
1126 }
1127 TypeSpecification::Time {
1128 minimum, maximum, ..
1129 } => {
1130 if let Some(v) = minimum {
1131 lines.push(format!("minimum: {}", v));
1132 }
1133 if let Some(v) = maximum {
1134 lines.push(format!("maximum: {}", v));
1135 }
1136 }
1137 TypeSpecification::QuantityRange { units, .. } => {
1138 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1139 if !unit_names.is_empty() {
1140 lines.push(format!("units: {}", unit_names.join(", ")));
1141 }
1142 }
1143 TypeSpecification::RatioRange { units, .. } => {
1144 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1145 if !unit_names.is_empty() {
1146 lines.push(format!("units: {}", unit_names.join(", ")));
1147 }
1148 }
1149 TypeSpecification::Boolean { .. }
1150 | TypeSpecification::NumberRange { .. }
1151 | TypeSpecification::DateRange { .. }
1152 | TypeSpecification::TimeRange { .. }
1153 | TypeSpecification::Veto { .. }
1154 | TypeSpecification::Undetermined => {}
1155 }
1156 lines
1157}
1158
1159impl ExecutionPlan {
1160 pub(crate) fn expression_unit_index(&self) -> &HashMap<String, Arc<LemmaType>> {
1164 &self.resolved_types.unit_index
1165 }
1166
1167 pub fn local_rule_names(&self) -> Vec<String> {
1179 self.rules
1180 .iter()
1181 .filter(|r| r.path.segments.is_empty())
1182 .map(|r| r.name.clone())
1183 .collect()
1184 }
1185
1186 pub fn schema(&self, overlay: &DataOverlay) -> SpecSchema {
1187 let all_local_rules = self.local_rule_names();
1188 self.schema_for_rules(&all_local_rules, overlay)
1189 .expect("BUG: all_local_rules sourced from self.rules")
1190 }
1191
1192 pub fn interface_schema(&self, overlay: &DataOverlay) -> SpecSchema {
1197 let mut data_entries: Vec<(usize, usize, String, DataEntry)> = self
1198 .data
1199 .iter()
1200 .filter(|(_, data)| {
1201 data.schema_type().is_some() && !matches!(data, DataDefinition::Reference { .. })
1202 })
1203 .map(|(path, data)| {
1204 let lemma_type = data
1205 .schema_type()
1206 .expect("BUG: filter above ensured schema_type is Some")
1207 .clone();
1208 let bound_value = schema_bound_value(path, data, overlay);
1209 let default = data.default_suggestion();
1210 (
1211 path.segments.len(),
1212 data.source().span.start,
1213 path.input_key(),
1214 DataEntry {
1215 lemma_type,
1216 bound_value,
1217 default,
1218 },
1219 )
1220 })
1221 .collect();
1222 data_entries.sort_by_key(|(depth, pos, _, _)| (*depth, *pos));
1223
1224 let rule_entries: Vec<(String, LemmaType)> = self
1225 .rules
1226 .iter()
1227 .filter(|r| r.path.segments.is_empty())
1228 .map(|r| (r.name.clone(), (*r.rule_type).clone()))
1229 .collect();
1230
1231 SpecSchema {
1232 spec: self.spec_name.clone(),
1233 commentary: self.commentary.clone(),
1234 effective: self.effective.as_ref().cloned(),
1235 versions: Vec::new(),
1236 data: data_entries
1237 .into_iter()
1238 .map(|(_, _, name, data)| (name, data))
1239 .collect(),
1240 rules: rule_entries.into_iter().collect(),
1241 meta: self.meta.clone(),
1242 }
1243 }
1244
1245 pub fn schema_for_rules(
1261 &self,
1262 rule_names: &[String],
1263 overlay: &DataOverlay,
1264 ) -> Result<SpecSchema, Error> {
1265 let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
1266 for rule_name in rule_names {
1267 let rule = self.get_rule(rule_name).ok_or_else(|| {
1268 Error::request(
1269 format!(
1270 "Rule '{}' not found in spec '{}'",
1271 rule_name, self.spec_name
1272 ),
1273 None::<String>,
1274 )
1275 })?;
1276 rule_entries.push((rule.name.clone(), (*rule.rule_type).clone()));
1277 }
1278
1279 let needed_data = self.collect_needed_data_paths(rule_names, overlay)?;
1280
1281 let mut data_entries: Vec<(usize, usize, String, DataEntry)> = self
1282 .data
1283 .iter()
1284 .filter(|(path, _)| needed_data.contains(path))
1285 .filter(|(_, data)| !matches!(data, DataDefinition::Reference { .. }))
1286 .filter_map(|(path, data)| {
1287 let lemma_type = data.schema_type()?.clone();
1288 let bound_value = schema_bound_value(path, data, overlay);
1289 let default = data.default_suggestion();
1290 Some((
1291 path.segments.len(),
1292 data.source().span.start,
1293 path.input_key(),
1294 DataEntry {
1295 lemma_type,
1296 bound_value,
1297 default,
1298 },
1299 ))
1300 })
1301 .collect();
1302 data_entries.sort_by_key(|(depth, pos, _, _)| (*depth, *pos));
1303 let data_entries: Vec<(String, DataEntry)> = data_entries
1304 .into_iter()
1305 .map(|(_, _, name, data)| (name, data))
1306 .collect();
1307
1308 Ok(SpecSchema {
1309 spec: self.spec_name.clone(),
1310 commentary: self.commentary.clone(),
1311 effective: self.effective.as_ref().cloned(),
1312 versions: Vec::new(),
1313 data: data_entries.into_iter().collect(),
1314 rules: rule_entries.into_iter().collect(),
1315 meta: self.meta.clone(),
1316 })
1317 }
1318
1319 pub fn get_data_path_by_str(&self, name: &str) -> Option<&DataPath> {
1321 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
1322 self.data
1323 .keys()
1324 .find(|path| path.input_key() == canonical_name)
1325 }
1326
1327 pub fn validated_response_rule_names(
1331 &self,
1332 rules: Option<&[String]>,
1333 ) -> Result<std::collections::HashSet<String>, Error> {
1334 let Some(rules) = rules else {
1335 return Ok(self.local_rule_names().into_iter().collect());
1336 };
1337 if rules.is_empty() {
1338 return Err(Error::request(
1339 "at least one rule required".to_string(),
1340 None::<String>,
1341 ));
1342 }
1343 let mut names = std::collections::HashSet::new();
1344 for rule_name in rules {
1345 let rule = self.get_rule(rule_name).ok_or_else(|| {
1346 Error::request(
1347 format!("Rule '{rule_name}' not found in spec '{}'", self.spec_name),
1348 None::<String>,
1349 )
1350 })?;
1351 names.insert(rule.name.clone());
1352 }
1353 Ok(names)
1354 }
1355
1356 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
1358 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
1359 self.rules
1360 .iter()
1361 .find(|r| r.name == canonical_name && r.path.segments.is_empty())
1362 }
1363
1364 pub fn collect_needed_data_paths(
1373 &self,
1374 rule_names: &[String],
1375 overlay: &DataOverlay,
1376 ) -> Result<HashSet<DataPath>, Error> {
1377 let known_values = build_known_values(self, overlay);
1378
1379 let mut needed_data: HashSet<DataPath> = HashSet::new();
1380 let mut visited_rules: HashSet<RulePath> = HashSet::new();
1381 let mut rule_worklist: Vec<RulePath> = Vec::new();
1382
1383 for rule_name in rule_names {
1385 let rule = self.get_rule(rule_name).ok_or_else(|| {
1386 Error::request(
1387 format!(
1388 "Rule '{}' not found in spec '{}'",
1389 rule_name, self.spec_name
1390 ),
1391 None::<String>,
1392 )
1393 })?;
1394 rule_worklist.push(rule.path.clone());
1395 }
1396
1397 while let Some(rule_path) = rule_worklist.pop() {
1400 if !visited_rules.insert(rule_path.clone()) {
1401 continue;
1402 }
1403
1404 let rule = self.get_rule_by_path(&rule_path).unwrap_or_else(|| {
1405 panic!(
1406 "BUG: rule path '{}' placed on worklist but not found in plan '{}'",
1407 rule_path.rule, self.spec_name
1408 )
1409 });
1410
1411 for (branch_index, branch) in rule.branches.iter().enumerate() {
1412 if branch_index == 0 {
1413 let any_unless_definitely_true =
1414 rule.branches[1..].iter().any(|unless_branch| {
1415 let unless_condition = unless_branch
1416 .condition
1417 .as_ref()
1418 .expect("BUG: unless branch missing condition");
1419 crate::evaluation::partial::try_evaluate_condition(
1420 unless_condition,
1421 &known_values,
1422 self,
1423 ) == Some(true)
1424 });
1425 if any_unless_definitely_true {
1426 continue;
1427 }
1428 } else if let Some(condition) = &branch.condition {
1429 if crate::evaluation::partial::try_evaluate_condition(
1430 condition,
1431 &known_values,
1432 self,
1433 ) == Some(false)
1434 {
1435 continue;
1436 }
1437 }
1438
1439 let mut branch_data: HashSet<DataPath> = HashSet::new();
1440 if let Some(condition) = &branch.condition {
1441 condition.collect_data_paths(&mut branch_data);
1442 }
1443 branch.result.collect_data_paths(&mut branch_data);
1444
1445 let mut branch_rules: HashSet<RulePath> = HashSet::new();
1446 if let Some(condition) = &branch.condition {
1447 condition.collect_rule_paths(&mut branch_rules);
1448 }
1449 branch.result.collect_rule_paths(&mut branch_rules);
1450
1451 for data_path in &branch_data {
1452 if let Some(DataDefinition::Reference {
1453 target: ReferenceTarget::Rule(target_rule),
1454 ..
1455 }) = self.data.get(data_path)
1456 {
1457 branch_rules.insert(target_rule.clone());
1458 }
1459 }
1460
1461 needed_data.extend(branch_data);
1462 rule_worklist.extend(branch_rules);
1463 }
1464 }
1465
1466 Ok(needed_data)
1467 }
1468
1469 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
1471 self.rules.iter().find(|r| &r.path == rule_path)
1472 }
1473
1474 pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
1476 self.data.get(path).and_then(|d| d.value())
1477 }
1478}
1479
1480pub(crate) fn validate_value_against_type(
1481 expected_type: &LemmaType,
1482 value: &LiteralValue,
1483) -> Result<(), String> {
1484 use crate::computation::rational::{commit_rational_to_decimal, RationalInteger};
1485 use crate::planning::semantics::TypeSpecification;
1486
1487 fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
1488 match commit_rational_to_decimal(magnitude) {
1489 Ok(decimal) => decimal.scale() > u32::from(max_decimals),
1490 Err(_) => true,
1491 }
1492 }
1493
1494 fn format_rational(r: &RationalInteger, decimals: Option<u8>) -> String {
1495 use crate::computation::rational::rational_to_display_str;
1496 match commit_rational_to_decimal(r) {
1497 Ok(decimal) => match decimals {
1498 Some(dp) => {
1499 let rounded = decimal.round_dp(u32::from(dp));
1500 format!("{:.prec$}", rounded, prec = dp as usize)
1501 }
1502 None => decimal.normalize().to_string(),
1503 },
1504 Err(_) => rational_to_display_str(r),
1505 }
1506 }
1507
1508 match (&expected_type.specifications, &value.value) {
1509 (
1510 TypeSpecification::Number {
1511 minimum,
1512 maximum,
1513 decimals,
1514 ..
1515 },
1516 ValueKind::Number(n),
1517 ) => {
1518 if let Some(d) = decimals {
1519 if exceeds_decimal_places(n, *d) {
1520 return Err(format!(
1521 "{} exceeds decimals constraint {d}",
1522 format_rational(n, *decimals)
1523 ));
1524 }
1525 }
1526 if let Some(min) = minimum {
1527 if n < min {
1528 return Err(format!(
1529 "{} is below minimum {}",
1530 format_rational(n, *decimals),
1531 format_rational(min, *decimals)
1532 ));
1533 }
1534 }
1535 if let Some(max) = maximum {
1536 if n > max {
1537 return Err(format!(
1538 "{} is above maximum {}",
1539 format_rational(n, *decimals),
1540 format_rational(max, *decimals)
1541 ));
1542 }
1543 }
1544 Ok(())
1545 }
1546 (
1547 TypeSpecification::Quantity {
1548 minimum,
1549 maximum,
1550 decimals,
1551 units,
1552 ..
1553 },
1554 ValueKind::Quantity(magnitude, signature),
1555 ) => {
1556 use crate::computation::rational::checked_div;
1557 use crate::planning::semantics::quantity_declared_bound_canonical;
1558 let unit = signature
1559 .first()
1560 .map(|(n, _)| n.as_str())
1561 .expect("BUG: Quantity value has empty signature in execution plan validation");
1562 let quantity_unit = units.get(unit)?;
1563 let factor = &quantity_unit.factor;
1564 let in_unit = checked_div(magnitude, factor).map_err(|failure| {
1565 format!("cannot de-canonicalize quantity for validation: {failure}")
1566 })?;
1567 if let Some(d) = decimals {
1568 if exceeds_decimal_places(&in_unit, *d) {
1569 return Err(format!(
1570 "{} {unit} exceeds decimals constraint {d}",
1571 format_rational(&in_unit, *decimals)
1572 ));
1573 }
1574 }
1575 if let Some(bound) = minimum {
1576 let canonical_min = quantity_declared_bound_canonical(
1577 bound,
1578 units,
1579 expected_type.name().as_str(),
1580 "minimum",
1581 )?;
1582 if magnitude < &canonical_min {
1583 let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
1584 format!("cannot de-canonicalize minimum for validation: {failure}")
1585 })?;
1586 let value_display =
1587 format!("{} {}", format_rational(&in_unit, *decimals), unit);
1588 let bound_display = format!(
1589 "{} {}",
1590 format_rational(&min_in_unit, *decimals),
1591 quantity_unit.name
1592 );
1593 return Err(format!("{value_display} is below minimum {bound_display}"));
1594 }
1595 }
1596 if let Some(bound) = maximum {
1597 let canonical_max = quantity_declared_bound_canonical(
1598 bound,
1599 units,
1600 expected_type.name().as_str(),
1601 "maximum",
1602 )?;
1603 if magnitude > &canonical_max {
1604 let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
1605 format!("cannot de-canonicalize maximum for validation: {failure}")
1606 })?;
1607 let value_display =
1608 format!("{} {}", format_rational(&in_unit, *decimals), unit);
1609 let bound_display = format!(
1610 "{} {}",
1611 format_rational(&max_in_unit, *decimals),
1612 quantity_unit.name
1613 );
1614 return Err(format!("{value_display} is above maximum {bound_display}"));
1615 }
1616 }
1617 Ok(())
1618 }
1619 (
1620 TypeSpecification::Text {
1621 length, options, ..
1622 },
1623 ValueKind::Text(s),
1624 ) => {
1625 let len = s.chars().count();
1626 if let Some(exact) = length {
1627 if len != *exact {
1628 return Err(format!(
1629 "'{}' has length {} but required length is {}",
1630 s, len, exact
1631 ));
1632 }
1633 }
1634 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
1635 return Err(format!(
1636 "'{}' is not in allowed options: {}",
1637 s,
1638 options.join(", ")
1639 ));
1640 }
1641 Ok(())
1642 }
1643 (
1644 TypeSpecification::Ratio {
1645 minimum,
1646 maximum,
1647 decimals,
1648 units,
1649 ..
1650 },
1651 ValueKind::Ratio(r, unit_name),
1652 ) => {
1653 use crate::computation::rational::checked_mul;
1654
1655 if let Some(d) = decimals {
1656 if exceeds_decimal_places(r, *d) {
1657 return Err(format!(
1658 "{} exceeds decimals constraint {d}",
1659 format_rational(r, *decimals)
1660 ));
1661 }
1662 }
1663 if let Some(type_minimum) = minimum {
1664 if r < type_minimum {
1665 let message = match unit_name.as_deref() {
1666 Some(unit) => {
1667 let ratio_unit = units.get(unit)?;
1668 let value_per_unit = checked_mul(r, &ratio_unit.value)
1669 .map_err(|failure| failure.to_string())?;
1670 let bound_per_unit = ratio_unit.minimum.clone().expect(
1671 "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
1672 );
1673 format!(
1674 "{} {unit} is below minimum {} {unit}",
1675 format_rational(&value_per_unit, *decimals),
1676 format_rational(&bound_per_unit.clone(), *decimals),
1677 )
1678 }
1679 None => format!(
1680 "{} is below minimum {}",
1681 format_rational(r, *decimals),
1682 format_rational(type_minimum, *decimals),
1683 ),
1684 };
1685 return Err(message);
1686 }
1687 }
1688 if let Some(type_maximum) = maximum {
1689 if r > type_maximum {
1690 let message = match unit_name.as_deref() {
1691 Some(unit) => {
1692 let ratio_unit = units.get(unit)?;
1693 let value_per_unit = checked_mul(r, &ratio_unit.value)
1694 .map_err(|failure| failure.to_string())?;
1695 let bound_per_unit = ratio_unit.maximum.clone().expect(
1696 "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
1697 );
1698 format!(
1699 "{} {unit} is above maximum {} {unit}",
1700 format_rational(&value_per_unit, *decimals),
1701 format_rational(&bound_per_unit.clone(), *decimals),
1702 )
1703 }
1704 None => format!(
1705 "{} is above maximum {}",
1706 format_rational(r, *decimals),
1707 format_rational(type_maximum, *decimals),
1708 ),
1709 };
1710 return Err(message);
1711 }
1712 }
1713 Ok(())
1714 }
1715 (
1716 TypeSpecification::Date {
1717 minimum, maximum, ..
1718 },
1719 ValueKind::Date(dt),
1720 ) => {
1721 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
1722 use std::cmp::Ordering;
1723 if let Some(min) = minimum {
1724 let min_sem = date_time_to_semantic(min);
1725 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
1726 return Err(format!("{} is below minimum {}", dt, min));
1727 }
1728 }
1729 if let Some(max) = maximum {
1730 let max_sem = date_time_to_semantic(max);
1731 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1732 return Err(format!("{} is above maximum {}", dt, max));
1733 }
1734 }
1735 Ok(())
1736 }
1737 (
1738 TypeSpecification::Time {
1739 minimum, maximum, ..
1740 },
1741 ValueKind::Time(t),
1742 ) => {
1743 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1744 use std::cmp::Ordering;
1745 if let Some(min) = minimum {
1746 let min_sem = time_to_semantic(min);
1747 if compare_semantic_times(t, &min_sem) == Ordering::Less {
1748 return Err(format!("{} is below minimum {}", t, min));
1749 }
1750 }
1751 if let Some(max) = maximum {
1752 let max_sem = time_to_semantic(max);
1753 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1754 return Err(format!("{} is above maximum {}", t, max));
1755 }
1756 }
1757 Ok(())
1758 }
1759 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1760 | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1761 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1762 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
1763 | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1764 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1765 | (TypeSpecification::Veto { .. }, _)
1766 | (TypeSpecification::Undetermined, _) => Ok(()),
1767 (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
1768 "BUG: validate_value_against_type called with mismatched type/value: \
1769 spec={:?}, value={:?} — typing must be enforced before validation",
1770 spec, value_kind
1771 ),
1772 (_, _) => Ok(()),
1773 }
1774}
1775
1776pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1777 let mut errors = Vec::new();
1778
1779 for (data_path, data_definition) in &plan.data {
1780 let (expected_type, lit) = match data_definition {
1781 DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1782 DataDefinition::TypeDeclaration { .. }
1783 | DataDefinition::Import { .. }
1784 | DataDefinition::Reference { .. } => continue,
1785 };
1786
1787 if let Err(msg) = validate_value_against_type(expected_type, lit) {
1788 let source = data_definition.source().clone();
1789 errors.push(Error::validation(
1790 format!(
1791 "Invalid value for data {} (expected {}): {}",
1792 data_path,
1793 expected_type.name().as_str(),
1794 msg
1795 ),
1796 Some(source),
1797 None::<String>,
1798 ));
1799 }
1800 }
1801
1802 errors
1803}
1804
1805fn collect_unit_conversion_targets_from_instructions(
1806 instructions: &Instructions,
1807 units: &mut BTreeSet<String>,
1808) {
1809 for insn in &instructions.code {
1810 if let Instruction::UnitConversion {
1811 target: SemanticConversionTarget::Unit { unit_name },
1812 ..
1813 } = insn
1814 {
1815 units.insert(unit_name.clone());
1816 }
1817 }
1818}
1819
1820pub(crate) fn validate_unit_index_references(plan: &ExecutionPlan) -> Result<(), Error> {
1821 let mut required_units = BTreeSet::new();
1822 for rule in &plan.rules {
1823 collect_unit_conversion_targets_from_instructions(&rule.instructions, &mut required_units);
1824 }
1825 for unit_name in required_units {
1826 if plan.resolved_types.unit_index.contains_key(&unit_name) {
1827 continue;
1828 }
1829 return Err(Error::validation(
1830 format!("Unknown unit '{unit_name}' in execution plan unit index."),
1831 None::<Source>,
1832 Some(plan.spec_name.clone()),
1833 ));
1834 }
1835 Ok(())
1836}
1837
1838#[derive(Debug, Clone, Serialize, Deserialize)]
1849pub struct ExecutionPlanSerialized {
1850 pub spec_name: String,
1851 #[serde(skip_serializing_if = "Option::is_none", default)]
1852 pub commentary: Option<String>,
1853 #[serde(
1854 serialize_with = "serialize_resolved_data_value_map",
1855 deserialize_with = "deserialize_resolved_data_value_map"
1856 )]
1857 pub data: IndexMap<DataPath, DataDefinition>,
1858 #[serde(default)]
1859 pub rules: Vec<ExecutableRule>,
1860 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1861 pub reference_evaluation_order: Vec<DataPath>,
1862 #[serde(default)]
1863 pub meta: HashMap<String, MetaValue>,
1864 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1867 pub unit_index: HashMap<String, Arc<LemmaType>>,
1868 pub effective: EffectiveDate,
1869 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1870 pub sources: SpecSources,
1871}
1872
1873impl From<&ExecutionPlan> for ExecutionPlanSerialized {
1874 fn from(plan: &ExecutionPlan) -> Self {
1875 Self {
1876 spec_name: plan.spec_name.clone(),
1877 commentary: plan.commentary.clone(),
1878 data: plan.data.clone(),
1879 rules: plan.rules.clone(),
1880 reference_evaluation_order: plan.reference_evaluation_order.clone(),
1881 meta: plan.meta.clone(),
1882 unit_index: plan.resolved_types.unit_index.clone(),
1883 effective: plan.effective.clone(),
1884 sources: plan.sources.clone(),
1885 }
1886 }
1887}
1888
1889impl TryFrom<ExecutionPlanSerialized> for ExecutionPlan {
1890 type Error = crate::Error;
1891
1892 fn try_from(serialized: ExecutionPlanSerialized) -> Result<Self, Self::Error> {
1893 let signature_index = crate::planning::graph::build_signature_index(
1894 &serialized.spec_name,
1895 &serialized.unit_index,
1896 )?;
1897 for rule in &serialized.rules {
1901 validate_instructions(&rule.instructions).map_err(|message| {
1902 crate::Error::request(
1903 format!(
1904 "Serialized execution plan for spec '{}' contains invalid instructions for rule '{}': {message}",
1905 serialized.spec_name, rule.name
1906 ),
1907 None::<String>,
1908 )
1909 })?;
1910 validate_instructions(&rule.source_instructions).map_err(|message| {
1911 crate::Error::request(
1912 format!(
1913 "Serialized execution plan for spec '{}' contains invalid source instructions for rule '{}': {message}",
1914 serialized.spec_name, rule.name
1915 ),
1916 None::<String>,
1917 )
1918 })?;
1919 }
1920 let max_register_count = serialized
1921 .rules
1922 .iter()
1923 .map(|rule| rule.instructions.register_count)
1924 .max()
1925 .unwrap_or(0);
1926 Ok(Self {
1927 spec_name: serialized.spec_name,
1928 commentary: serialized.commentary,
1929 data: serialized.data,
1930 rules: serialized.rules,
1931 max_register_count,
1932 reference_evaluation_order: serialized.reference_evaluation_order,
1933 meta: serialized.meta,
1934 resolved_types: ResolvedSpecTypes {
1935 unit_index: serialized.unit_index,
1936 ..ResolvedSpecTypes::default()
1937 },
1938 signature_index,
1939 effective: serialized.effective,
1940 sources: serialized.sources,
1941 })
1942 }
1943}
1944
1945fn serialize_resolved_data_value_map<S>(
1946 map: &IndexMap<DataPath, DataDefinition>,
1947 serializer: S,
1948) -> Result<S::Ok, S::Error>
1949where
1950 S: Serializer,
1951{
1952 let entries: Vec<(&DataPath, &DataDefinition)> = map.iter().collect();
1953 entries.serialize(serializer)
1954}
1955
1956fn deserialize_resolved_data_value_map<'de, D>(
1957 deserializer: D,
1958) -> Result<IndexMap<DataPath, DataDefinition>, D::Error>
1959where
1960 D: Deserializer<'de>,
1961{
1962 let entries: Vec<(DataPath, DataDefinition)> = Vec::deserialize(deserializer)?;
1963 Ok(entries.into_iter().collect())
1964}
1965
1966#[cfg(test)]
1967mod tests {
1968 use super::*;
1969 use crate::computation::rational::{rational_new, rational_zero};
1970 use crate::literals::DateGranularity;
1971 use crate::parsing::ast::DateTimeValue;
1972 use crate::planning::semantics::{
1973 primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
1974 };
1975 use crate::Engine;
1976 use serde_json;
1977 use std::str::FromStr;
1978 use std::sync::Arc;
1979
1980 fn default_limits() -> ResourceLimits {
1981 ResourceLimits::default()
1982 }
1983
1984 fn roundtrip_execution_plan(plan: &ExecutionPlan) -> ExecutionPlan {
1985 let serialized = ExecutionPlanSerialized::from(plan);
1986 let json = serde_json::to_string(&serialized).expect("Should serialize");
1987 let back: ExecutionPlanSerialized =
1988 serde_json::from_str(&json).expect("Should deserialize");
1989 ExecutionPlan::try_from(back).expect("Should reconstruct")
1990 }
1991
1992 fn input_data(pairs: &[(&str, &str)]) -> HashMap<String, DataValueInput> {
1993 pairs
1994 .iter()
1995 .map(|(k, v)| (k.to_string(), DataValueInput::convenience(*v)))
1996 .collect()
1997 }
1998
1999 #[test]
2000 fn test_with_raw_values() {
2001 let mut engine = Engine::new();
2002 engine
2003 .load(
2004 r#"
2005 spec test
2006 data age: number -> default 25
2007 "#,
2008 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2009 "test.lemma",
2010 ))),
2011 )
2012 .unwrap();
2013
2014 let now = DateTimeValue::now();
2015 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2016 let data_path = DataPath::new(vec![], "age".to_string());
2017
2018 let values = input_data(&[("age", "30")]);
2019
2020 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2021 let updated_value = overlay.values.get(&data_path).unwrap();
2022 match &updated_value.value {
2023 crate::planning::semantics::ValueKind::Number(n) => {
2024 assert_eq!(n, &rational_new(30, 1));
2025 }
2026 other => panic!("Expected number literal, got {:?}", other),
2027 }
2028 }
2029
2030 #[test]
2031 fn test_with_raw_values_type_mismatch() {
2032 let mut engine = Engine::new();
2033 engine
2034 .load(
2035 r#"
2036 spec test
2037 data age: number
2038 "#,
2039 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2040 "test.lemma",
2041 ))),
2042 )
2043 .unwrap();
2044
2045 let now = DateTimeValue::now();
2046 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2047
2048 let values = input_data(&[("age", "thirty")]);
2049
2050 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2051 let data_path = DataPath::new(vec![], "age".to_string());
2052 match overlay.violated.get(&data_path) {
2053 Some(reason) => {
2054 assert!(
2055 reason.contains("number"),
2056 "type mismatch must record violation reason, got: {reason}"
2057 );
2058 }
2059 None => panic!("expected violated data for age=thirty"),
2060 }
2061 }
2062
2063 #[test]
2064 fn test_with_raw_values_unknown_data() {
2065 let mut engine = Engine::new();
2066 engine
2067 .load(
2068 r#"
2069 spec test
2070 data known: number
2071 "#,
2072 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2073 "test.lemma",
2074 ))),
2075 )
2076 .unwrap();
2077
2078 let now = DateTimeValue::now();
2079 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2080
2081 let values = input_data(&[("unknown", "30")]);
2082
2083 assert!(DataOverlay::resolve(plan, values, &default_limits()).is_err());
2084 }
2085
2086 #[test]
2087 fn test_with_raw_values_nested() {
2088 let mut engine = Engine::new();
2089 engine
2090 .load(
2091 r#"
2092 spec private
2093 data base_price: number
2094
2095 spec test
2096 uses rules: private
2097 "#,
2098 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2099 "test.lemma",
2100 ))),
2101 )
2102 .unwrap();
2103
2104 let now = DateTimeValue::now();
2105 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2106
2107 let values = input_data(&[("rules.base_price", "100")]);
2108
2109 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2110 let data_path = DataPath {
2111 segments: vec![PathSegment {
2112 data: "rules".to_string(),
2113 spec: "private".to_string(),
2114 }],
2115 data: "base_price".to_string(),
2116 };
2117 let updated_value = overlay.values.get(&data_path).unwrap();
2118 match &updated_value.value {
2119 crate::planning::semantics::ValueKind::Number(n) => {
2120 assert_eq!(n, &rational_new(100, 1));
2121 }
2122 other => panic!("Expected number literal, got {:?}", other),
2123 }
2124 }
2125
2126 fn test_source() -> Source {
2127 use crate::parsing::ast::Span;
2128 Source::new(
2129 crate::parsing::source::SourceType::Volatile,
2130 Span {
2131 start: 0,
2132 end: 0,
2133 line: 1,
2134 col: 0,
2135 },
2136 )
2137 }
2138
2139 fn create_literal_expr(value: LiteralValue) -> Expression {
2140 Expression::new(
2141 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
2142 test_source(),
2143 )
2144 }
2145
2146 fn create_data_path_expr(path: DataPath) -> Expression {
2147 Expression::new(
2148 crate::planning::semantics::ExpressionKind::DataPath(path),
2149 test_source(),
2150 )
2151 }
2152
2153 fn constant_return_instructions(literal: LiteralValue) -> Instructions {
2154 Instructions {
2155 version: INSTRUCTIONS_VERSION,
2156 register_count: 1,
2157 register_types: vec![Arc::clone(&literal.lemma_type)],
2158 constants: vec![literal],
2159 data_manifest: Vec::new(),
2160 veto_messages: Vec::new(),
2161 arm_tags: Vec::new(),
2162 conversion_tags: Vec::new(),
2163 code: vec![
2164 Instruction::LoadConstant {
2165 destination_register: 0,
2166 constant_index: 0,
2167 },
2168 Instruction::Return { source_register: 0 },
2169 ],
2170 }
2171 }
2172
2173 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
2174 LiteralValue::number_from_decimal(n)
2175 }
2176
2177 fn create_boolean_literal(b: bool) -> LiteralValue {
2178 LiteralValue::from_bool(b)
2179 }
2180
2181 fn create_text_literal(s: String) -> LiteralValue {
2182 LiteralValue::text(s)
2183 }
2184
2185 #[test]
2186 fn with_values_should_enforce_number_maximum_constraint() {
2187 let data_path = DataPath::new(vec![], "x".to_string());
2190
2191 let max10 = crate::planning::semantics::LemmaType::primitive(
2192 crate::planning::semantics::TypeSpecification::Number {
2193 minimum: None,
2194 maximum: Some(rational_new(10, 1)),
2195 decimals: None,
2196 help: String::new(),
2197 },
2198 );
2199 let source = Source::new(
2200 crate::parsing::source::SourceType::Volatile,
2201 crate::parsing::ast::Span {
2202 start: 0,
2203 end: 0,
2204 line: 1,
2205 col: 0,
2206 },
2207 );
2208 let mut data = IndexMap::new();
2209 data.insert(
2210 data_path.clone(),
2211 crate::planning::semantics::DataDefinition::Value {
2212 value: crate::planning::semantics::LiteralValue::number_with_type(
2213 rational_new(0, 1),
2214 Arc::new(max10.clone()),
2215 ),
2216 source: source.clone(),
2217 },
2218 );
2219
2220 let plan = ExecutionPlan {
2221 spec_name: "test".to_string(),
2222 commentary: None,
2223 data,
2224 rules: Vec::new(),
2225 max_register_count: 0,
2226 reference_evaluation_order: Vec::new(),
2227 meta: HashMap::new(),
2228 resolved_types: ResolvedSpecTypes::default(),
2229 signature_index: HashMap::new(),
2230 effective: EffectiveDate::Origin,
2231 sources: Vec::new(),
2232 };
2233
2234 let values = input_data(&[("x", "11")]);
2235
2236 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2237 match overlay.violated.get(&data_path) {
2238 Some(reason) => {
2239 assert!(
2240 reason.contains("maximum") || reason.contains("10"),
2241 "x=11 must violate maximum 10, got: {reason}"
2242 );
2243 }
2244 None => panic!("expected violated data for x=11"),
2245 }
2246 }
2247
2248 #[test]
2249 fn with_values_should_enforce_text_enum_options() {
2250 let data_path = DataPath::new(vec![], "tier".to_string());
2252
2253 let tier = crate::planning::semantics::LemmaType::primitive(
2254 crate::planning::semantics::TypeSpecification::Text {
2255 length: None,
2256 options: vec!["silver".to_string(), "gold".to_string()],
2257 help: String::new(),
2258 },
2259 );
2260 let source = Source::new(
2261 crate::parsing::source::SourceType::Volatile,
2262 crate::parsing::ast::Span {
2263 start: 0,
2264 end: 0,
2265 line: 1,
2266 col: 0,
2267 },
2268 );
2269 let mut data = IndexMap::new();
2270 data.insert(
2271 data_path.clone(),
2272 crate::planning::semantics::DataDefinition::Value {
2273 value: crate::planning::semantics::LiteralValue::text_with_type(
2274 "silver".to_string(),
2275 Arc::new(tier.clone()),
2276 ),
2277 source,
2278 },
2279 );
2280
2281 let plan = ExecutionPlan {
2282 spec_name: "test".to_string(),
2283 commentary: None,
2284 data,
2285 rules: Vec::new(),
2286 max_register_count: 0,
2287 reference_evaluation_order: Vec::new(),
2288 meta: HashMap::new(),
2289 resolved_types: ResolvedSpecTypes::default(),
2290 signature_index: HashMap::new(),
2291 effective: EffectiveDate::Origin,
2292 sources: Vec::new(),
2293 };
2294
2295 let values = input_data(&[("tier", "platinum")]);
2296
2297 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2298 match overlay.violated.get(&data_path) {
2299 Some(reason) => {
2300 assert!(
2301 reason.contains("allowed options") || reason.contains("platinum"),
2302 "invalid enum must record violation, got: {reason}"
2303 );
2304 }
2305 None => panic!("expected violated data for tier=platinum"),
2306 }
2307 }
2308
2309 #[test]
2310 fn with_values_should_enforce_quantity_decimals() {
2311 let data_path = DataPath::new(vec![], "price".to_string());
2314
2315 let money = crate::planning::semantics::LemmaType::primitive(
2316 crate::planning::semantics::TypeSpecification::Quantity {
2317 minimum: None,
2318 maximum: None,
2319 decimals: Some(2),
2320 units: crate::planning::semantics::QuantityUnits::from(vec![
2321 crate::planning::semantics::QuantityUnit::from_decimal_factor(
2322 "eur".to_string(),
2323 rust_decimal::Decimal::from_str("1.0").unwrap(),
2324 Vec::new(),
2325 )
2326 .expect("eur unit factor must be exact decimal"),
2327 ]),
2328 traits: Vec::new(),
2329 decomposition: None,
2330 help: String::new(),
2331 },
2332 );
2333 let source = Source::new(
2334 crate::parsing::source::SourceType::Volatile,
2335 crate::parsing::ast::Span {
2336 start: 0,
2337 end: 0,
2338 line: 1,
2339 col: 0,
2340 },
2341 );
2342 let mut data = IndexMap::new();
2343 data.insert(
2344 data_path.clone(),
2345 crate::planning::semantics::DataDefinition::Value {
2346 value: crate::planning::semantics::LiteralValue::quantity_with_type(
2347 rational_zero(),
2348 "eur".to_string(),
2349 Arc::new(money.clone()),
2350 ),
2351 source,
2352 },
2353 );
2354
2355 let plan = ExecutionPlan {
2356 spec_name: "test".to_string(),
2357 commentary: None,
2358 data,
2359 rules: Vec::new(),
2360 max_register_count: 0,
2361 reference_evaluation_order: Vec::new(),
2362 meta: HashMap::new(),
2363 resolved_types: ResolvedSpecTypes::default(),
2364 signature_index: HashMap::new(),
2365 effective: EffectiveDate::Origin,
2366 sources: Vec::new(),
2367 };
2368
2369 let values = input_data(&[("price", "1.234 eur")]);
2370
2371 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2372 match overlay.violated.get(&data_path) {
2373 Some(reason) => {
2374 assert!(
2375 reason.contains("decimals") || reason.contains("decimal"),
2376 "1.234 eur must violate decimals=2, got: {reason}"
2377 );
2378 }
2379 None => panic!("expected violated data for price=1.234 eur"),
2380 }
2381 }
2382
2383 #[test]
2384 fn test_serialize_deserialize_execution_plan() {
2385 let data_path = DataPath {
2386 segments: vec![],
2387 data: "age".to_string(),
2388 };
2389 let mut data = IndexMap::new();
2390 data.insert(
2391 data_path.clone(),
2392 crate::planning::semantics::DataDefinition::Value {
2393 value: create_number_literal(0.into()),
2394 source: test_source(),
2395 },
2396 );
2397 let plan = ExecutionPlan {
2398 spec_name: "test".to_string(),
2399 commentary: None,
2400 data,
2401 rules: Vec::new(),
2402 max_register_count: 0,
2403 reference_evaluation_order: Vec::new(),
2404 meta: HashMap::new(),
2405 resolved_types: ResolvedSpecTypes::default(),
2406 signature_index: HashMap::new(),
2407 effective: EffectiveDate::Origin,
2408 sources: Vec::new(),
2409 };
2410
2411 let deserialized = roundtrip_execution_plan(&plan);
2412
2413 assert_eq!(deserialized.spec_name, plan.spec_name);
2414 assert_eq!(deserialized.data.len(), plan.data.len());
2415 assert_eq!(deserialized.rules.len(), plan.rules.len());
2416 }
2417
2418 #[test]
2419 fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
2420 let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
2421 let imported_type = crate::planning::semantics::LemmaType::new(
2422 "salary".to_string(),
2423 TypeSpecification::quantity(),
2424 crate::planning::semantics::TypeExtends::Custom {
2425 parent: "money".to_string(),
2426 family: "money".to_string(),
2427 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
2428 spec: Arc::clone(&dep_spec),
2429 },
2430 },
2431 );
2432
2433 let salary_path = DataPath::new(vec![], "salary".to_string());
2434 let mut data = IndexMap::new();
2435 data.insert(
2436 salary_path,
2437 crate::planning::semantics::DataDefinition::TypeDeclaration {
2438 resolved_type: Arc::new(imported_type),
2439 declared_default: None,
2440 source: test_source(),
2441 },
2442 );
2443
2444 let plan = ExecutionPlan {
2445 spec_name: "test".to_string(),
2446 commentary: None,
2447 data,
2448 rules: Vec::new(),
2449 max_register_count: 0,
2450 reference_evaluation_order: Vec::new(),
2451 meta: HashMap::new(),
2452 resolved_types: ResolvedSpecTypes::default(),
2453 signature_index: HashMap::new(),
2454 effective: EffectiveDate::Origin,
2455 sources: Vec::new(),
2456 };
2457
2458 let deserialized = roundtrip_execution_plan(&plan);
2459
2460 let recovered = deserialized
2461 .data
2462 .get(&DataPath::new(vec![], "salary".to_string()))
2463 .and_then(|d| d.schema_type())
2464 .expect("salary type should be present in plan.data");
2465 match &recovered.extends {
2466 crate::planning::semantics::TypeExtends::Custom {
2467 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
2468 ..
2469 } => {
2470 assert_eq!(spec.name, "examples");
2471 }
2472 other => panic!(
2473 "Expected imported defining_spec after round-trip, got {:?}",
2474 other
2475 ),
2476 }
2477 }
2478
2479 #[test]
2480 fn test_serialize_deserialize_plan_with_rules() {
2481 use crate::planning::semantics::ExpressionKind;
2482
2483 let age_path = DataPath::new(vec![], "age".to_string());
2484 let mut data = IndexMap::new();
2485 data.insert(
2486 age_path.clone(),
2487 crate::planning::semantics::DataDefinition::Value {
2488 value: create_number_literal(0.into()),
2489 source: test_source(),
2490 },
2491 );
2492 let mut plan = ExecutionPlan {
2493 spec_name: "test".to_string(),
2494 commentary: None,
2495 data,
2496 rules: Vec::new(),
2497 max_register_count: 0,
2498 reference_evaluation_order: Vec::new(),
2499 meta: HashMap::new(),
2500 resolved_types: ResolvedSpecTypes::default(),
2501 signature_index: HashMap::new(),
2502 effective: EffectiveDate::Origin,
2503 sources: Vec::new(),
2504 };
2505
2506 let rule = ExecutableRule {
2507 path: RulePath::new(vec![], "can_drive".to_string()),
2508 name: "can_drive".to_string(),
2509 branches: vec![{
2510 let result = create_literal_expr(create_boolean_literal(true));
2511 let condition = Expression::new(
2512 ExpressionKind::Comparison(
2513 Arc::new(create_data_path_expr(age_path.clone())),
2514 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2515 Arc::new(create_literal_expr(create_number_literal(18.into()))),
2516 ),
2517 test_source(),
2518 );
2519 Branch {
2520 condition: Some(condition.clone()),
2521 result: result.clone(),
2522 source: test_source(),
2523 }
2524 }],
2525 instructions: constant_return_instructions(create_boolean_literal(true)),
2526 source_instructions: constant_return_instructions(create_boolean_literal(true)),
2527 source: test_source(),
2528 rule_type: Arc::new(primitive_boolean().clone()),
2529 };
2530
2531 plan.rules.push(rule);
2532 plan.max_register_count = plan.rules[0].instructions.register_count;
2533
2534 let deserialized = roundtrip_execution_plan(&plan);
2535
2536 assert_eq!(deserialized.spec_name, plan.spec_name);
2537 assert_eq!(deserialized.data.len(), plan.data.len());
2538 assert_eq!(deserialized.rules.len(), plan.rules.len());
2539 assert_eq!(deserialized.rules[0].name, "can_drive");
2540 assert_eq!(deserialized.rules[0].branches.len(), 1);
2541 }
2542
2543 #[test]
2544 fn test_serialize_deserialize_plan_with_nested_data_paths() {
2545 use crate::planning::semantics::PathSegment;
2546 let data_path = DataPath {
2547 segments: vec![PathSegment {
2548 data: "employee".to_string(),
2549 spec: "private".to_string(),
2550 }],
2551 data: "salary".to_string(),
2552 };
2553
2554 let mut data = IndexMap::new();
2555 data.insert(
2556 data_path.clone(),
2557 crate::planning::semantics::DataDefinition::Value {
2558 value: create_number_literal(0.into()),
2559 source: test_source(),
2560 },
2561 );
2562 let plan = ExecutionPlan {
2563 spec_name: "test".to_string(),
2564 commentary: None,
2565 data,
2566 rules: Vec::new(),
2567 max_register_count: 0,
2568 reference_evaluation_order: Vec::new(),
2569 meta: HashMap::new(),
2570 resolved_types: ResolvedSpecTypes::default(),
2571 signature_index: HashMap::new(),
2572 effective: EffectiveDate::Origin,
2573 sources: Vec::new(),
2574 };
2575
2576 let deserialized = roundtrip_execution_plan(&plan);
2577
2578 assert_eq!(deserialized.data.len(), 1);
2579 let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
2580 assert_eq!(deserialized_path.segments.len(), 1);
2581 assert_eq!(deserialized_path.segments[0].data, "employee");
2582 assert_eq!(deserialized_path.data, "salary");
2583 }
2584
2585 #[test]
2586 fn test_serialize_deserialize_plan_with_multiple_data_types() {
2587 let name_path = DataPath::new(vec![], "name".to_string());
2588 let age_path = DataPath::new(vec![], "age".to_string());
2589 let active_path = DataPath::new(vec![], "active".to_string());
2590
2591 let mut data = IndexMap::new();
2592 data.insert(
2593 name_path.clone(),
2594 crate::planning::semantics::DataDefinition::Value {
2595 value: create_text_literal("Alice".to_string()),
2596 source: test_source(),
2597 },
2598 );
2599 data.insert(
2600 age_path.clone(),
2601 crate::planning::semantics::DataDefinition::Value {
2602 value: create_number_literal(30.into()),
2603 source: test_source(),
2604 },
2605 );
2606 data.insert(
2607 active_path.clone(),
2608 crate::planning::semantics::DataDefinition::Value {
2609 value: create_boolean_literal(true),
2610 source: test_source(),
2611 },
2612 );
2613
2614 let plan = ExecutionPlan {
2615 spec_name: "test".to_string(),
2616 commentary: None,
2617 data,
2618 rules: Vec::new(),
2619 max_register_count: 0,
2620 reference_evaluation_order: Vec::new(),
2621 meta: HashMap::new(),
2622 resolved_types: ResolvedSpecTypes::default(),
2623 signature_index: HashMap::new(),
2624 effective: EffectiveDate::Origin,
2625 sources: Vec::new(),
2626 };
2627
2628 let deserialized = roundtrip_execution_plan(&plan);
2629
2630 assert_eq!(deserialized.data.len(), 3);
2631
2632 assert_eq!(
2633 deserialized.get_data_value(&name_path).unwrap().value,
2634 crate::planning::semantics::ValueKind::Text("Alice".to_string())
2635 );
2636 assert_eq!(
2637 deserialized.get_data_value(&age_path).unwrap().value,
2638 crate::planning::semantics::ValueKind::Number(rational_new(30, 1))
2639 );
2640 assert_eq!(
2641 deserialized.get_data_value(&active_path).unwrap().value,
2642 crate::planning::semantics::ValueKind::Boolean(true)
2643 );
2644 }
2645
2646 #[test]
2647 fn test_serialize_deserialize_plan_with_multiple_branches() {
2648 use crate::planning::semantics::ExpressionKind;
2649
2650 let points_path = DataPath::new(vec![], "points".to_string());
2651 let mut data = IndexMap::new();
2652 data.insert(
2653 points_path.clone(),
2654 crate::planning::semantics::DataDefinition::Value {
2655 value: create_number_literal(0.into()),
2656 source: test_source(),
2657 },
2658 );
2659 let mut plan = ExecutionPlan {
2660 spec_name: "test".to_string(),
2661 commentary: None,
2662 data,
2663 rules: Vec::new(),
2664 max_register_count: 0,
2665 reference_evaluation_order: Vec::new(),
2666 meta: HashMap::new(),
2667 resolved_types: ResolvedSpecTypes::default(),
2668 signature_index: HashMap::new(),
2669 effective: EffectiveDate::Origin,
2670 sources: Vec::new(),
2671 };
2672
2673 let rule = ExecutableRule {
2674 path: RulePath::new(vec![], "tier".to_string()),
2675 name: "tier".to_string(),
2676 branches: vec![
2677 {
2678 let result = create_literal_expr(create_text_literal("bronze".to_string()));
2679 Branch {
2680 condition: None,
2681 result: result.clone(),
2682 source: test_source(),
2683 }
2684 },
2685 {
2686 let result = create_literal_expr(create_text_literal("silver".to_string()));
2687 Branch {
2688 condition: Some(Expression::new(
2689 ExpressionKind::Comparison(
2690 Arc::new(create_data_path_expr(points_path.clone())),
2691 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2692 Arc::new(create_literal_expr(create_number_literal(100.into()))),
2693 ),
2694 test_source(),
2695 )),
2696 result: result.clone(),
2697 source: test_source(),
2698 }
2699 },
2700 {
2701 let result = create_literal_expr(create_text_literal("gold".to_string()));
2702 Branch {
2703 condition: Some(Expression::new(
2704 ExpressionKind::Comparison(
2705 Arc::new(create_data_path_expr(points_path.clone())),
2706 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2707 Arc::new(create_literal_expr(create_number_literal(500.into()))),
2708 ),
2709 test_source(),
2710 )),
2711 result: result.clone(),
2712 source: test_source(),
2713 }
2714 },
2715 ],
2716 instructions: constant_return_instructions(create_text_literal("bronze".to_string())),
2717 source_instructions: constant_return_instructions(create_text_literal(
2718 "bronze".to_string(),
2719 )),
2720 source: test_source(),
2721 rule_type: Arc::new(primitive_text().clone()),
2722 };
2723
2724 plan.rules.push(rule);
2725 plan.max_register_count = plan.rules[0].instructions.register_count;
2726
2727 let deserialized = roundtrip_execution_plan(&plan);
2728
2729 assert_eq!(deserialized.rules.len(), 1);
2730 assert_eq!(deserialized.rules[0].branches.len(), 3);
2731 assert!(deserialized.rules[0].branches[0].condition.is_none());
2732 assert!(deserialized.rules[0].branches[1].condition.is_some());
2733 assert!(deserialized.rules[0].branches[2].condition.is_some());
2734 }
2735
2736 #[test]
2737 fn test_serialize_deserialize_empty_plan() {
2738 let plan = ExecutionPlan {
2739 spec_name: "empty".to_string(),
2740 commentary: None,
2741 data: IndexMap::new(),
2742 rules: Vec::new(),
2743 max_register_count: 0,
2744 reference_evaluation_order: Vec::new(),
2745 meta: HashMap::new(),
2746 resolved_types: ResolvedSpecTypes::default(),
2747 signature_index: HashMap::new(),
2748 effective: EffectiveDate::Origin,
2749 sources: Vec::new(),
2750 };
2751
2752 let deserialized = roundtrip_execution_plan(&plan);
2753
2754 assert_eq!(deserialized.spec_name, "empty");
2755 assert_eq!(deserialized.data.len(), 0);
2756 assert_eq!(deserialized.rules.len(), 0);
2757 }
2758
2759 #[test]
2760 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
2761 use crate::planning::semantics::ExpressionKind;
2762
2763 let x_path = DataPath::new(vec![], "x".to_string());
2764 let mut data = IndexMap::new();
2765 data.insert(
2766 x_path.clone(),
2767 crate::planning::semantics::DataDefinition::Value {
2768 value: create_number_literal(0.into()),
2769 source: test_source(),
2770 },
2771 );
2772 let mut plan = ExecutionPlan {
2773 spec_name: "test".to_string(),
2774 commentary: None,
2775 data,
2776 rules: Vec::new(),
2777 max_register_count: 0,
2778 reference_evaluation_order: Vec::new(),
2779 meta: HashMap::new(),
2780 resolved_types: ResolvedSpecTypes::default(),
2781 signature_index: HashMap::new(),
2782 effective: EffectiveDate::Origin,
2783 sources: Vec::new(),
2784 };
2785
2786 let rule = ExecutableRule {
2787 path: RulePath::new(vec![], "doubled".to_string()),
2788 name: "doubled".to_string(),
2789 branches: vec![{
2790 let result = Expression::new(
2791 ExpressionKind::Arithmetic(
2792 Arc::new(create_data_path_expr(x_path.clone())),
2793 crate::parsing::ast::ArithmeticComputation::Multiply,
2794 Arc::new(create_literal_expr(create_number_literal(2.into()))),
2795 ),
2796 test_source(),
2797 );
2798 Branch {
2799 condition: None,
2800 result: result.clone(),
2801 source: test_source(),
2802 }
2803 }],
2804 instructions: constant_return_instructions(create_number_literal(0.into())),
2805 source_instructions: constant_return_instructions(create_number_literal(0.into())),
2806 source: test_source(),
2807 rule_type: Arc::new(crate::planning::semantics::primitive_number().clone()),
2808 };
2809
2810 plan.rules.push(rule);
2811 plan.max_register_count = plan.rules[0].instructions.register_count;
2812
2813 let deserialized = roundtrip_execution_plan(&plan);
2814
2815 assert_eq!(deserialized.rules.len(), 1);
2816 match &deserialized.rules[0].branches[0].result.kind {
2817 ExpressionKind::Arithmetic(left, op, right) => {
2818 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
2819 match &left.kind {
2820 ExpressionKind::DataPath(_) => {}
2821 _ => panic!("Expected DataPath in left operand"),
2822 }
2823 match &right.kind {
2824 ExpressionKind::Literal(_) => {}
2825 _ => panic!("Expected Literal in right operand"),
2826 }
2827 }
2828 _ => panic!("Expected Arithmetic expression"),
2829 }
2830 }
2831
2832 #[test]
2833 fn test_serialize_deserialize_round_trip_equality() {
2834 use crate::planning::semantics::ExpressionKind;
2835
2836 let age_path = DataPath::new(vec![], "age".to_string());
2837 let mut data = IndexMap::new();
2838 data.insert(
2839 age_path.clone(),
2840 crate::planning::semantics::DataDefinition::Value {
2841 value: create_number_literal(0.into()),
2842 source: test_source(),
2843 },
2844 );
2845 let mut plan = ExecutionPlan {
2846 spec_name: "test".to_string(),
2847 commentary: None,
2848 data,
2849 rules: Vec::new(),
2850 max_register_count: 0,
2851 reference_evaluation_order: Vec::new(),
2852 meta: HashMap::new(),
2853 resolved_types: ResolvedSpecTypes::default(),
2854 signature_index: HashMap::new(),
2855 effective: EffectiveDate::Origin,
2856 sources: Vec::new(),
2857 };
2858
2859 let rule = ExecutableRule {
2860 path: RulePath::new(vec![], "is_adult".to_string()),
2861 name: "is_adult".to_string(),
2862 branches: vec![{
2863 let result = create_literal_expr(create_boolean_literal(true));
2864 let condition = Expression::new(
2865 ExpressionKind::Comparison(
2866 Arc::new(create_data_path_expr(age_path.clone())),
2867 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2868 Arc::new(create_literal_expr(create_number_literal(18.into()))),
2869 ),
2870 test_source(),
2871 );
2872 Branch {
2873 condition: Some(condition.clone()),
2874 result: result.clone(),
2875 source: test_source(),
2876 }
2877 }],
2878 instructions: constant_return_instructions(create_boolean_literal(true)),
2879 source_instructions: constant_return_instructions(create_boolean_literal(true)),
2880 source: test_source(),
2881 rule_type: Arc::new(primitive_boolean().clone()),
2882 };
2883
2884 plan.rules.push(rule);
2885 plan.max_register_count = plan.rules[0].instructions.register_count;
2886
2887 let deserialized = roundtrip_execution_plan(&plan);
2888 let deserialized2 = roundtrip_execution_plan(&deserialized);
2889
2890 assert_eq!(deserialized2.spec_name, plan.spec_name);
2891 assert_eq!(deserialized2.data.len(), plan.data.len());
2892 assert_eq!(deserialized2.rules.len(), plan.rules.len());
2893 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
2894 assert_eq!(
2895 deserialized2.rules[0].branches.len(),
2896 plan.rules[0].branches.len()
2897 );
2898 }
2899
2900 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
2901 ExecutionPlan {
2902 spec_name: "s".into(),
2903 commentary: None,
2904 data: IndexMap::new(),
2905 rules: Vec::new(),
2906 max_register_count: 0,
2907 reference_evaluation_order: Vec::new(),
2908 meta: HashMap::new(),
2909 resolved_types: ResolvedSpecTypes::default(),
2910 signature_index: HashMap::new(),
2911 effective,
2912 sources: Vec::new(),
2913 }
2914 }
2915
2916 #[test]
2917 fn plan_at_exact_boundary_selects_later_slice() {
2918 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2919
2920 let june = DateTimeValue {
2921 year: 2025,
2922 month: 6,
2923 day: 1,
2924 hour: 0,
2925 minute: 0,
2926 second: 0,
2927 microsecond: 0,
2928 timezone: None,
2929
2930 granularity: DateGranularity::Full,
2931 };
2932 let dec = DateTimeValue {
2933 year: 2025,
2934 month: 12,
2935 day: 1,
2936 hour: 0,
2937 minute: 0,
2938 second: 0,
2939 microsecond: 0,
2940 timezone: None,
2941
2942 granularity: DateGranularity::Full,
2943 };
2944
2945 let set = ExecutionPlanSet {
2946 spec_name: "s".into(),
2947 plans: vec![
2948 empty_plan(EffectiveDate::Origin),
2949 empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2950 empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2951 ],
2952 };
2953
2954 assert!(std::ptr::eq(
2955 set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2956 .expect("boundary instant"),
2957 &set.plans[1]
2958 ));
2959 assert!(std::ptr::eq(
2960 set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2961 .expect("dec boundary"),
2962 &set.plans[2]
2963 ));
2964 }
2965
2966 #[test]
2967 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2968 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2969
2970 let june = DateTimeValue {
2971 year: 2025,
2972 month: 6,
2973 day: 1,
2974 hour: 0,
2975 minute: 0,
2976 second: 0,
2977 microsecond: 0,
2978 timezone: None,
2979
2980 granularity: DateGranularity::Full,
2981 };
2982 let may_end = DateTimeValue {
2983 year: 2025,
2984 month: 5,
2985 day: 31,
2986 hour: 23,
2987 minute: 59,
2988 second: 59,
2989 microsecond: 0,
2990 timezone: None,
2991
2992 granularity: DateGranularity::DateTime,
2993 };
2994
2995 let set = ExecutionPlanSet {
2996 spec_name: "s".into(),
2997 plans: vec![
2998 empty_plan(EffectiveDate::Origin),
2999 empty_plan(EffectiveDate::DateTimeValue(june)),
3000 ],
3001 };
3002
3003 assert!(std::ptr::eq(
3004 set.plan_at(&EffectiveDate::DateTimeValue(may_end))
3005 .expect("may 31"),
3006 &set.plans[0]
3007 ));
3008 }
3009
3010 #[test]
3011 fn plan_at_single_plan_matches_any_instant_after_start() {
3012 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
3013
3014 let t = DateTimeValue {
3015 year: 2025,
3016 month: 3,
3017 day: 1,
3018 hour: 0,
3019 minute: 0,
3020 second: 0,
3021 microsecond: 0,
3022 timezone: None,
3023
3024 granularity: DateGranularity::Full,
3025 };
3026 let set = ExecutionPlanSet {
3027 spec_name: "s".into(),
3028 plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
3029 year: 2025,
3030 month: 1,
3031 day: 1,
3032 hour: 0,
3033 minute: 0,
3034 second: 0,
3035 microsecond: 0,
3036 timezone: None,
3037
3038 granularity: DateGranularity::Full,
3039 }))],
3040 };
3041 assert!(std::ptr::eq(
3042 set.plan_at(&EffectiveDate::DateTimeValue(t))
3043 .expect("inside single slice"),
3044 &set.plans[0]
3045 ));
3046 }
3047
3048 #[test]
3051 fn schema_json_shape_contract() {
3052 let mut engine = Engine::new();
3053 engine
3054 .load(
3055 r#"
3056 spec pricing
3057 data bridge_height: quantity
3058 -> unit meter 1
3059 -> default 100 meter
3060 data quantity: number -> minimum 0
3061 rule cost: bridge_height * quantity
3062 "#,
3063 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3064 "test.lemma",
3065 ))),
3066 )
3067 .unwrap();
3068 let now = DateTimeValue::now();
3069 let schema = engine
3070 .get_plan(None, "pricing", Some(&now))
3071 .unwrap()
3072 .schema(&DataOverlay::default());
3073
3074 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3075
3076 let bh = &value["data"]["bridge_height"];
3077 assert!(
3078 bh.is_object(),
3079 "data entry must be a named object, not tuple"
3080 );
3081 assert!(
3082 bh.get("type").is_some(),
3083 "data entry must expose `type` field"
3084 );
3085 assert!(
3086 bh.get("default").is_some(),
3087 "bridge_height exposes `-> default` as schema default suggestion"
3088 );
3089 assert!(
3090 bh.get("bound_value").is_none(),
3091 "bridge_height is not a spec-bound literal"
3092 );
3093
3094 let ty = &bh["type"];
3095 assert_eq!(
3096 ty["kind"], "quantity",
3097 "kind tag sits on the type object itself"
3098 );
3099 assert!(
3100 ty["units"].is_array(),
3101 "quantity-only fields flatten up to top level"
3102 );
3103 assert!(
3104 ty.get("options").is_none(),
3105 "text-only fields must not leak"
3106 );
3107
3108 let qty = &value["data"]["quantity"];
3109 assert_eq!(qty["type"]["kind"], "number");
3110 assert!(
3111 qty.get("default").is_none(),
3112 "quantity has no default suggestion"
3113 );
3114 assert!(
3115 qty.get("bound_value").is_none(),
3116 "quantity has no bound literal"
3117 );
3118
3119 let cost = &value["rules"]["cost"];
3120 assert_eq!(
3121 cost["kind"], "quantity",
3122 "rule types use the same flat shape"
3123 );
3124 assert!(
3125 cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
3126 "quantity rule result types expose declared units"
3127 );
3128 assert!(
3129 cost["units"][0].get("factor").is_some(),
3130 "quantity rule units use factor field"
3131 );
3132 }
3133
3134 #[test]
3135 fn schema_rule_result_units_contract() {
3136 let mut engine = Engine::new();
3137 engine
3138 .load(
3139 r#"
3140 spec units_contract
3141 data money: quantity
3142 -> unit eur 1
3143 -> unit usd 0.91
3144 data rate: ratio
3145 -> unit basis_points 10000
3146 -> unit percent 100
3147 -> default 500 basis_points
3148 rule total: money
3149 rule rate_out: rate
3150 "#,
3151 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3152 "units_contract.lemma",
3153 ))),
3154 )
3155 .unwrap();
3156 let now = DateTimeValue::now();
3157 let schema = engine
3158 .get_plan(None, "units_contract", Some(&now))
3159 .unwrap()
3160 .schema(&DataOverlay::default());
3161 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3162
3163 let money_units = &value["data"]["money"]["type"]["units"];
3164 assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
3165 assert!(money_units[0].get("name").is_some());
3166 assert!(money_units[0].get("factor").is_some());
3167 assert!(money_units[0]["factor"].get("numer").is_some());
3168 assert!(money_units[0]["factor"].get("denom").is_some());
3169
3170 let rate_units = &value["data"]["rate"]["type"]["units"];
3171 assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
3172 assert!(rate_units[0].get("name").is_some());
3173 assert!(rate_units[0].get("value").is_some());
3174 assert!(rate_units[0]["value"].get("numer").is_some());
3175 assert!(rate_units[0]["value"].get("denom").is_some());
3176
3177 let total_rule_units = &value["rules"]["total"]["units"];
3178 let money_unit_names: Vec<_> = money_units
3179 .as_array()
3180 .unwrap()
3181 .iter()
3182 .map(|u| u["name"].as_str().unwrap())
3183 .collect();
3184 let total_rule_unit_names: Vec<_> = total_rule_units
3185 .as_array()
3186 .unwrap()
3187 .iter()
3188 .map(|u| u["name"].as_str().unwrap())
3189 .collect();
3190 assert_eq!(total_rule_unit_names, money_unit_names);
3191
3192 let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
3193 let rate_unit_names: Vec<_> = rate_units
3194 .as_array()
3195 .unwrap()
3196 .iter()
3197 .map(|u| u["name"].as_str().unwrap())
3198 .collect();
3199 let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
3200 .as_array()
3201 .unwrap()
3202 .iter()
3203 .map(|u| u["name"].as_str().unwrap())
3204 .collect();
3205 assert_eq!(rate_out_rule_unit_names, rate_unit_names);
3206 }
3207
3208 #[test]
3209 fn schema_json_round_trip_preserves_shape() {
3210 let mut engine = Engine::new();
3211 engine
3212 .load(
3213 r#"
3214 spec s
3215 data age: number -> minimum 0 -> default 18
3216 data grade: text -> options "A" "B" "C"
3217 rule adult: age >= 18
3218 "#,
3219 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
3220 )
3221 .unwrap();
3222 let now = DateTimeValue::now();
3223 let schema = engine
3224 .get_plan(None, "s", Some(&now))
3225 .unwrap()
3226 .schema(&DataOverlay::default());
3227
3228 let json = serde_json::to_string(&schema).unwrap();
3229 let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
3230 assert_eq!(schema, round_tripped);
3231 }
3232}
3233
3234