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::{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_for_validation_message(
1495 expected_type: &crate::planning::semantics::LemmaType,
1496 magnitude: &RationalInteger,
1497 ) -> String {
1498 use crate::computation::rational::rational_to_display_str;
1499 expected_type
1500 .try_materialize_rational_as_decimal_string(magnitude)
1501 .unwrap_or_else(|_| rational_to_display_str(magnitude))
1502 }
1503
1504 match (&expected_type.specifications, &value.value) {
1505 (
1506 TypeSpecification::Number {
1507 minimum,
1508 maximum,
1509 decimals,
1510 ..
1511 },
1512 ValueKind::Number(n),
1513 ) => {
1514 if let Some(d) = decimals {
1515 if exceeds_decimal_places(n, *d) {
1516 return Err(format!(
1517 "{} exceeds decimals constraint {d}",
1518 format_rational_for_validation_message(expected_type, n)
1519 ));
1520 }
1521 }
1522 if let Some(min) = minimum {
1523 if n < min {
1524 return Err(format!(
1525 "{} is below minimum {}",
1526 format_rational_for_validation_message(expected_type, n),
1527 format_rational_for_validation_message(expected_type, min)
1528 ));
1529 }
1530 }
1531 if let Some(max) = maximum {
1532 if n > max {
1533 return Err(format!(
1534 "{} is above maximum {}",
1535 format_rational_for_validation_message(expected_type, n),
1536 format_rational_for_validation_message(expected_type, max)
1537 ));
1538 }
1539 }
1540 Ok(())
1541 }
1542 (
1543 TypeSpecification::Quantity {
1544 minimum,
1545 maximum,
1546 decimals,
1547 units,
1548 ..
1549 },
1550 ValueKind::Quantity(magnitude, signature),
1551 ) => {
1552 use crate::computation::rational::checked_div;
1553 use crate::planning::semantics::quantity_declared_bound_canonical;
1554 let unit = signature
1555 .first()
1556 .map(|(n, _)| n.as_str())
1557 .expect("BUG: Quantity value has empty signature in execution plan validation");
1558 let quantity_unit = units.get(unit)?;
1559 let factor = &quantity_unit.factor;
1560 let in_unit = checked_div(magnitude, factor).map_err(|failure| {
1561 format!("cannot de-canonicalize quantity for validation: {failure}")
1562 })?;
1563 if let Some(d) = decimals {
1564 if exceeds_decimal_places(&in_unit, *d) {
1565 return Err(format!(
1566 "{} {unit} exceeds decimals constraint {d}",
1567 format_rational_for_validation_message(expected_type, &in_unit)
1568 ));
1569 }
1570 }
1571 if let Some(bound) = minimum {
1572 let canonical_min = quantity_declared_bound_canonical(
1573 bound,
1574 units,
1575 expected_type.name().as_str(),
1576 "minimum",
1577 )?;
1578 if magnitude < &canonical_min {
1579 let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
1580 format!("cannot de-canonicalize minimum for validation: {failure}")
1581 })?;
1582 let value_display = format!(
1583 "{} {}",
1584 format_rational_for_validation_message(expected_type, &in_unit),
1585 unit
1586 );
1587 let bound_display = format!(
1588 "{} {}",
1589 format_rational_for_validation_message(expected_type, &min_in_unit),
1590 quantity_unit.name
1591 );
1592 return Err(format!("{value_display} is below minimum {bound_display}"));
1593 }
1594 }
1595 if let Some(bound) = maximum {
1596 let canonical_max = quantity_declared_bound_canonical(
1597 bound,
1598 units,
1599 expected_type.name().as_str(),
1600 "maximum",
1601 )?;
1602 if magnitude > &canonical_max {
1603 let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
1604 format!("cannot de-canonicalize maximum for validation: {failure}")
1605 })?;
1606 let value_display = format!(
1607 "{} {}",
1608 format_rational_for_validation_message(expected_type, &in_unit),
1609 unit
1610 );
1611 let bound_display = format!(
1612 "{} {}",
1613 format_rational_for_validation_message(expected_type, &max_in_unit),
1614 quantity_unit.name
1615 );
1616 return Err(format!("{value_display} is above maximum {bound_display}"));
1617 }
1618 }
1619 Ok(())
1620 }
1621 (
1622 TypeSpecification::Text {
1623 length, options, ..
1624 },
1625 ValueKind::Text(s),
1626 ) => {
1627 let len = s.chars().count();
1628 if let Some(exact) = length {
1629 if len != *exact {
1630 return Err(format!(
1631 "'{}' has length {} but required length is {}",
1632 s, len, exact
1633 ));
1634 }
1635 }
1636 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
1637 return Err(format!(
1638 "'{}' is not in allowed options: {}",
1639 s,
1640 options.join(", ")
1641 ));
1642 }
1643 Ok(())
1644 }
1645 (
1646 TypeSpecification::Ratio {
1647 minimum,
1648 maximum,
1649 decimals,
1650 units,
1651 ..
1652 },
1653 ValueKind::Ratio(r, unit_name),
1654 ) => {
1655 use crate::computation::rational::checked_mul;
1656
1657 if let Some(d) = decimals {
1658 if exceeds_decimal_places(r, *d) {
1659 return Err(format!(
1660 "{} exceeds decimals constraint {d}",
1661 format_rational_for_validation_message(expected_type, r)
1662 ));
1663 }
1664 }
1665 if let Some(type_minimum) = minimum {
1666 if r < type_minimum {
1667 let message = match unit_name.as_deref() {
1668 Some(unit) => {
1669 let ratio_unit = units.get(unit)?;
1670 let value_per_unit = checked_mul(r, &ratio_unit.value)
1671 .map_err(|failure| failure.to_string())?;
1672 let bound_per_unit = ratio_unit.minimum.clone().expect(
1673 "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
1674 );
1675 format!(
1676 "{} {unit} is below minimum {} {unit}",
1677 format_rational_for_validation_message(
1678 expected_type,
1679 &value_per_unit
1680 ),
1681 format_rational_for_validation_message(
1682 expected_type,
1683 &bound_per_unit.clone()
1684 ),
1685 )
1686 }
1687 None => format!(
1688 "{} is below minimum {}",
1689 format_rational_for_validation_message(expected_type, r),
1690 format_rational_for_validation_message(expected_type, type_minimum),
1691 ),
1692 };
1693 return Err(message);
1694 }
1695 }
1696 if let Some(type_maximum) = maximum {
1697 if r > type_maximum {
1698 let message = match unit_name.as_deref() {
1699 Some(unit) => {
1700 let ratio_unit = units.get(unit)?;
1701 let value_per_unit = checked_mul(r, &ratio_unit.value)
1702 .map_err(|failure| failure.to_string())?;
1703 let bound_per_unit = ratio_unit.maximum.clone().expect(
1704 "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
1705 );
1706 format!(
1707 "{} {unit} is above maximum {} {unit}",
1708 format_rational_for_validation_message(
1709 expected_type,
1710 &value_per_unit
1711 ),
1712 format_rational_for_validation_message(
1713 expected_type,
1714 &bound_per_unit.clone()
1715 ),
1716 )
1717 }
1718 None => format!(
1719 "{} is above maximum {}",
1720 format_rational_for_validation_message(expected_type, r),
1721 format_rational_for_validation_message(expected_type, type_maximum),
1722 ),
1723 };
1724 return Err(message);
1725 }
1726 }
1727 Ok(())
1728 }
1729 (
1730 TypeSpecification::Date {
1731 minimum, maximum, ..
1732 },
1733 ValueKind::Date(dt),
1734 ) => {
1735 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
1736 use std::cmp::Ordering;
1737 if let Some(min) = minimum {
1738 let min_sem = date_time_to_semantic(min);
1739 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
1740 return Err(format!("{} is below minimum {}", dt, min));
1741 }
1742 }
1743 if let Some(max) = maximum {
1744 let max_sem = date_time_to_semantic(max);
1745 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1746 return Err(format!("{} is above maximum {}", dt, max));
1747 }
1748 }
1749 Ok(())
1750 }
1751 (
1752 TypeSpecification::Time {
1753 minimum, maximum, ..
1754 },
1755 ValueKind::Time(t),
1756 ) => {
1757 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1758 use std::cmp::Ordering;
1759 if let Some(min) = minimum {
1760 let min_sem = time_to_semantic(min);
1761 if compare_semantic_times(t, &min_sem) == Ordering::Less {
1762 return Err(format!("{} is below minimum {}", t, min));
1763 }
1764 }
1765 if let Some(max) = maximum {
1766 let max_sem = time_to_semantic(max);
1767 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1768 return Err(format!("{} is above maximum {}", t, max));
1769 }
1770 }
1771 Ok(())
1772 }
1773 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1774 | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1775 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1776 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
1777 | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1778 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1779 | (TypeSpecification::Veto { .. }, _)
1780 | (TypeSpecification::Undetermined, _) => Ok(()),
1781 (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
1782 "BUG: validate_value_against_type called with mismatched type/value: \
1783 spec={:?}, value={:?} — typing must be enforced before validation",
1784 spec, value_kind
1785 ),
1786 (_, _) => Ok(()),
1787 }
1788}
1789
1790pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1791 let mut errors = Vec::new();
1792
1793 for (data_path, data_definition) in &plan.data {
1794 let (expected_type, lit) = match data_definition {
1795 DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1796 DataDefinition::TypeDeclaration { .. }
1797 | DataDefinition::Import { .. }
1798 | DataDefinition::Reference { .. } => continue,
1799 };
1800
1801 if let Err(msg) = validate_value_against_type(expected_type, lit) {
1802 let source = data_definition.source().clone();
1803 errors.push(Error::validation(
1804 format!(
1805 "Invalid value for data {} (expected {}): {}",
1806 data_path,
1807 expected_type.name().as_str(),
1808 msg
1809 ),
1810 Some(source),
1811 None::<String>,
1812 ));
1813 }
1814 }
1815
1816 errors
1817}
1818
1819pub(crate) fn validate_unit_conversion_targets(plan: &ExecutionPlan) -> Result<(), Error> {
1820 for rule in &plan.rules {
1821 for instructions in [&rule.instructions, &rule.source_instructions] {
1822 for insn in &instructions.code {
1823 let Instruction::UnitConversion { target, .. } = insn else {
1824 continue;
1825 };
1826 let Some((unit_name, owning_type)) =
1827 crate::computation::units::conversion_target_declares_unit(target)
1828 else {
1829 continue;
1830 };
1831 if crate::computation::units::owning_type_declares_unit_name(
1832 owning_type.as_ref(),
1833 unit_name,
1834 ) {
1835 continue;
1836 }
1837 return Err(Error::validation(
1838 format!(
1839 "Unit conversion target '{unit_name}' is not declared on owning type '{}'",
1840 owning_type.name()
1841 ),
1842 None::<Source>,
1843 Some(plan.spec_name.clone()),
1844 ));
1845 }
1846 }
1847 }
1848 Ok(())
1849}
1850
1851pub(crate) fn validate_unit_index_references(plan: &ExecutionPlan) -> Result<(), Error> {
1852 validate_unit_conversion_targets(plan)
1853}
1854
1855#[derive(Debug, Clone, Serialize, Deserialize)]
1866pub struct ExecutionPlanSerialized {
1867 pub spec_name: String,
1868 #[serde(skip_serializing_if = "Option::is_none", default)]
1869 pub commentary: Option<String>,
1870 #[serde(
1871 serialize_with = "serialize_resolved_data_value_map",
1872 deserialize_with = "deserialize_resolved_data_value_map"
1873 )]
1874 pub data: IndexMap<DataPath, DataDefinition>,
1875 #[serde(default)]
1876 pub rules: Vec<ExecutableRule>,
1877 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1878 pub reference_evaluation_order: Vec<DataPath>,
1879 #[serde(default)]
1880 pub meta: HashMap<String, MetaValue>,
1881 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1884 pub unit_index: HashMap<String, Arc<LemmaType>>,
1885 pub effective: EffectiveDate,
1886 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1887 pub sources: SpecSources,
1888}
1889
1890impl From<&ExecutionPlan> for ExecutionPlanSerialized {
1891 fn from(plan: &ExecutionPlan) -> Self {
1892 Self {
1893 spec_name: plan.spec_name.clone(),
1894 commentary: plan.commentary.clone(),
1895 data: plan.data.clone(),
1896 rules: plan.rules.clone(),
1897 reference_evaluation_order: plan.reference_evaluation_order.clone(),
1898 meta: plan.meta.clone(),
1899 unit_index: plan.resolved_types.unit_index.clone(),
1900 effective: plan.effective.clone(),
1901 sources: plan.sources.clone(),
1902 }
1903 }
1904}
1905
1906impl TryFrom<ExecutionPlanSerialized> for ExecutionPlan {
1907 type Error = crate::Error;
1908
1909 fn try_from(serialized: ExecutionPlanSerialized) -> Result<Self, Self::Error> {
1910 let signature_index = crate::planning::graph::build_signature_index(
1911 &serialized.spec_name,
1912 &serialized.unit_index,
1913 )?;
1914 for rule in &serialized.rules {
1918 validate_instructions(&rule.instructions).map_err(|message| {
1919 crate::Error::request(
1920 format!(
1921 "Serialized execution plan for spec '{}' contains invalid instructions for rule '{}': {message}",
1922 serialized.spec_name, rule.name
1923 ),
1924 None::<String>,
1925 )
1926 })?;
1927 validate_instructions(&rule.source_instructions).map_err(|message| {
1928 crate::Error::request(
1929 format!(
1930 "Serialized execution plan for spec '{}' contains invalid source instructions for rule '{}': {message}",
1931 serialized.spec_name, rule.name
1932 ),
1933 None::<String>,
1934 )
1935 })?;
1936 }
1937 let max_register_count = serialized
1938 .rules
1939 .iter()
1940 .map(|rule| rule.instructions.register_count)
1941 .max()
1942 .unwrap_or(0);
1943 let plan = Self {
1944 spec_name: serialized.spec_name,
1945 commentary: serialized.commentary,
1946 data: serialized.data,
1947 rules: serialized.rules,
1948 max_register_count,
1949 reference_evaluation_order: serialized.reference_evaluation_order,
1950 meta: serialized.meta,
1951 resolved_types: ResolvedSpecTypes {
1952 unit_index: serialized.unit_index,
1953 ..ResolvedSpecTypes::default()
1954 },
1955 signature_index,
1956 effective: serialized.effective,
1957 sources: serialized.sources,
1958 };
1959 validate_unit_index_references(&plan).map_err(|error| {
1960 crate::Error::request(
1961 format!(
1962 "Serialized execution plan for spec '{}' is invalid: {}",
1963 plan.spec_name,
1964 error.message()
1965 ),
1966 None::<String>,
1967 )
1968 })?;
1969 Ok(plan)
1970 }
1971}
1972
1973fn serialize_resolved_data_value_map<S>(
1974 map: &IndexMap<DataPath, DataDefinition>,
1975 serializer: S,
1976) -> Result<S::Ok, S::Error>
1977where
1978 S: Serializer,
1979{
1980 let entries: Vec<(&DataPath, &DataDefinition)> = map.iter().collect();
1981 entries.serialize(serializer)
1982}
1983
1984fn deserialize_resolved_data_value_map<'de, D>(
1985 deserializer: D,
1986) -> Result<IndexMap<DataPath, DataDefinition>, D::Error>
1987where
1988 D: Deserializer<'de>,
1989{
1990 let entries: Vec<(DataPath, DataDefinition)> = Vec::deserialize(deserializer)?;
1991 Ok(entries.into_iter().collect())
1992}
1993
1994#[cfg(test)]
1995mod tests {
1996 use super::*;
1997 use crate::computation::rational::{rational_new, rational_zero};
1998 use crate::literals::DateGranularity;
1999 use crate::parsing::ast::DateTimeValue;
2000 use crate::planning::semantics::{
2001 primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
2002 };
2003 use crate::Engine;
2004 use serde_json;
2005 use std::str::FromStr;
2006 use std::sync::Arc;
2007
2008 fn default_limits() -> ResourceLimits {
2009 ResourceLimits::default()
2010 }
2011
2012 fn roundtrip_execution_plan(plan: &ExecutionPlan) -> ExecutionPlan {
2013 let serialized = ExecutionPlanSerialized::from(plan);
2014 let json = serde_json::to_string(&serialized).expect("Should serialize");
2015 let back: ExecutionPlanSerialized =
2016 serde_json::from_str(&json).expect("Should deserialize");
2017 ExecutionPlan::try_from(back).expect("Should reconstruct")
2018 }
2019
2020 fn input_data(pairs: &[(&str, &str)]) -> HashMap<String, DataValueInput> {
2021 pairs
2022 .iter()
2023 .map(|(k, v)| (k.to_string(), DataValueInput::convenience(*v)))
2024 .collect()
2025 }
2026
2027 #[test]
2028 fn test_with_raw_values() {
2029 let mut engine = Engine::new();
2030 engine
2031 .load(
2032 r#"
2033 spec test
2034 data age: number -> default 25
2035 "#,
2036 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2037 "test.lemma",
2038 ))),
2039 )
2040 .unwrap();
2041
2042 let now = DateTimeValue::now();
2043 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2044 let data_path = DataPath::new(vec![], "age".to_string());
2045
2046 let values = input_data(&[("age", "30")]);
2047
2048 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2049 let updated_value = overlay.values.get(&data_path).unwrap();
2050 match &updated_value.value {
2051 crate::planning::semantics::ValueKind::Number(n) => {
2052 assert_eq!(n, &rational_new(30, 1));
2053 }
2054 other => panic!("Expected number literal, got {:?}", other),
2055 }
2056 }
2057
2058 #[test]
2059 fn test_with_raw_values_type_mismatch() {
2060 let mut engine = Engine::new();
2061 engine
2062 .load(
2063 r#"
2064 spec test
2065 data age: number
2066 "#,
2067 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2068 "test.lemma",
2069 ))),
2070 )
2071 .unwrap();
2072
2073 let now = DateTimeValue::now();
2074 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2075
2076 let values = input_data(&[("age", "thirty")]);
2077
2078 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2079 let data_path = DataPath::new(vec![], "age".to_string());
2080 match overlay.violated.get(&data_path) {
2081 Some(reason) => {
2082 assert!(
2083 reason.contains("number"),
2084 "type mismatch must record violation reason, got: {reason}"
2085 );
2086 }
2087 None => panic!("expected violated data for age=thirty"),
2088 }
2089 }
2090
2091 #[test]
2092 fn test_with_raw_values_unknown_data() {
2093 let mut engine = Engine::new();
2094 engine
2095 .load(
2096 r#"
2097 spec test
2098 data known: number
2099 "#,
2100 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2101 "test.lemma",
2102 ))),
2103 )
2104 .unwrap();
2105
2106 let now = DateTimeValue::now();
2107 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2108
2109 let values = input_data(&[("unknown", "30")]);
2110
2111 assert!(DataOverlay::resolve(plan, values, &default_limits()).is_err());
2112 }
2113
2114 #[test]
2115 fn test_with_raw_values_nested() {
2116 let mut engine = Engine::new();
2117 engine
2118 .load(
2119 r#"
2120 spec private
2121 data base_price: number
2122
2123 spec test
2124 uses rules: private
2125 "#,
2126 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2127 "test.lemma",
2128 ))),
2129 )
2130 .unwrap();
2131
2132 let now = DateTimeValue::now();
2133 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2134
2135 let values = input_data(&[("rules.base_price", "100")]);
2136
2137 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2138 let data_path = DataPath {
2139 segments: vec![PathSegment {
2140 data: "rules".to_string(),
2141 spec: "private".to_string(),
2142 }],
2143 data: "base_price".to_string(),
2144 };
2145 let updated_value = overlay.values.get(&data_path).unwrap();
2146 match &updated_value.value {
2147 crate::planning::semantics::ValueKind::Number(n) => {
2148 assert_eq!(n, &rational_new(100, 1));
2149 }
2150 other => panic!("Expected number literal, got {:?}", other),
2151 }
2152 }
2153
2154 fn test_source() -> Source {
2155 use crate::parsing::ast::Span;
2156 Source::new(
2157 crate::parsing::source::SourceType::Volatile,
2158 Span {
2159 start: 0,
2160 end: 0,
2161 line: 1,
2162 col: 0,
2163 },
2164 )
2165 }
2166
2167 fn create_literal_expr(value: LiteralValue) -> Expression {
2168 Expression::new(
2169 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
2170 test_source(),
2171 )
2172 }
2173
2174 fn create_data_path_expr(path: DataPath) -> Expression {
2175 Expression::new(
2176 crate::planning::semantics::ExpressionKind::DataPath(path),
2177 test_source(),
2178 )
2179 }
2180
2181 fn constant_return_instructions(literal: LiteralValue) -> Instructions {
2182 Instructions {
2183 version: INSTRUCTIONS_VERSION,
2184 register_count: 1,
2185 register_types: vec![Arc::clone(&literal.lemma_type)],
2186 constants: vec![literal],
2187 data_manifest: Vec::new(),
2188 veto_messages: Vec::new(),
2189 arm_tags: Vec::new(),
2190 conversion_tags: Vec::new(),
2191 code: vec![
2192 Instruction::LoadConstant {
2193 destination_register: 0,
2194 constant_index: 0,
2195 },
2196 Instruction::Return { source_register: 0 },
2197 ],
2198 }
2199 }
2200
2201 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
2202 LiteralValue::number_from_decimal(n)
2203 }
2204
2205 fn create_boolean_literal(b: bool) -> LiteralValue {
2206 LiteralValue::from_bool(b)
2207 }
2208
2209 fn create_text_literal(s: String) -> LiteralValue {
2210 LiteralValue::text(s)
2211 }
2212
2213 #[test]
2214 fn with_values_should_enforce_number_maximum_constraint() {
2215 let data_path = DataPath::new(vec![], "x".to_string());
2218
2219 let max10 = crate::planning::semantics::LemmaType::primitive(
2220 crate::planning::semantics::TypeSpecification::Number {
2221 minimum: None,
2222 maximum: Some(rational_new(10, 1)),
2223 decimals: None,
2224 help: String::new(),
2225 },
2226 );
2227 let source = Source::new(
2228 crate::parsing::source::SourceType::Volatile,
2229 crate::parsing::ast::Span {
2230 start: 0,
2231 end: 0,
2232 line: 1,
2233 col: 0,
2234 },
2235 );
2236 let mut data = IndexMap::new();
2237 data.insert(
2238 data_path.clone(),
2239 crate::planning::semantics::DataDefinition::Value {
2240 value: crate::planning::semantics::LiteralValue::number_with_type(
2241 rational_new(0, 1),
2242 Arc::new(max10.clone()),
2243 ),
2244 source: source.clone(),
2245 },
2246 );
2247
2248 let plan = ExecutionPlan {
2249 spec_name: "test".to_string(),
2250 commentary: None,
2251 data,
2252 rules: Vec::new(),
2253 max_register_count: 0,
2254 reference_evaluation_order: Vec::new(),
2255 meta: HashMap::new(),
2256 resolved_types: ResolvedSpecTypes::default(),
2257 signature_index: HashMap::new(),
2258 effective: EffectiveDate::Origin,
2259 sources: Vec::new(),
2260 };
2261
2262 let values = input_data(&[("x", "11")]);
2263
2264 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2265 match overlay.violated.get(&data_path) {
2266 Some(reason) => {
2267 assert!(
2268 reason.contains("maximum") || reason.contains("10"),
2269 "x=11 must violate maximum 10, got: {reason}"
2270 );
2271 }
2272 None => panic!("expected violated data for x=11"),
2273 }
2274 }
2275
2276 #[test]
2277 fn with_values_should_enforce_text_enum_options() {
2278 let data_path = DataPath::new(vec![], "tier".to_string());
2280
2281 let tier = crate::planning::semantics::LemmaType::primitive(
2282 crate::planning::semantics::TypeSpecification::Text {
2283 length: None,
2284 options: vec!["silver".to_string(), "gold".to_string()],
2285 help: String::new(),
2286 },
2287 );
2288 let source = Source::new(
2289 crate::parsing::source::SourceType::Volatile,
2290 crate::parsing::ast::Span {
2291 start: 0,
2292 end: 0,
2293 line: 1,
2294 col: 0,
2295 },
2296 );
2297 let mut data = IndexMap::new();
2298 data.insert(
2299 data_path.clone(),
2300 crate::planning::semantics::DataDefinition::Value {
2301 value: crate::planning::semantics::LiteralValue::text_with_type(
2302 "silver".to_string(),
2303 Arc::new(tier.clone()),
2304 ),
2305 source,
2306 },
2307 );
2308
2309 let plan = ExecutionPlan {
2310 spec_name: "test".to_string(),
2311 commentary: None,
2312 data,
2313 rules: Vec::new(),
2314 max_register_count: 0,
2315 reference_evaluation_order: Vec::new(),
2316 meta: HashMap::new(),
2317 resolved_types: ResolvedSpecTypes::default(),
2318 signature_index: HashMap::new(),
2319 effective: EffectiveDate::Origin,
2320 sources: Vec::new(),
2321 };
2322
2323 let values = input_data(&[("tier", "platinum")]);
2324
2325 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2326 match overlay.violated.get(&data_path) {
2327 Some(reason) => {
2328 assert!(
2329 reason.contains("allowed options") || reason.contains("platinum"),
2330 "invalid enum must record violation, got: {reason}"
2331 );
2332 }
2333 None => panic!("expected violated data for tier=platinum"),
2334 }
2335 }
2336
2337 #[test]
2338 fn with_values_should_enforce_quantity_decimals() {
2339 let data_path = DataPath::new(vec![], "price".to_string());
2342
2343 let money = crate::planning::semantics::LemmaType::primitive(
2344 crate::planning::semantics::TypeSpecification::Quantity {
2345 minimum: None,
2346 maximum: None,
2347 decimals: Some(2),
2348 units: crate::planning::semantics::QuantityUnits::from(vec![
2349 crate::planning::semantics::QuantityUnit::from_decimal_factor(
2350 "eur".to_string(),
2351 rust_decimal::Decimal::from_str("1.0").unwrap(),
2352 Vec::new(),
2353 )
2354 .expect("eur unit factor must be exact decimal"),
2355 ]),
2356 traits: Vec::new(),
2357 decomposition: None,
2358 help: String::new(),
2359 },
2360 );
2361 let source = Source::new(
2362 crate::parsing::source::SourceType::Volatile,
2363 crate::parsing::ast::Span {
2364 start: 0,
2365 end: 0,
2366 line: 1,
2367 col: 0,
2368 },
2369 );
2370 let mut data = IndexMap::new();
2371 data.insert(
2372 data_path.clone(),
2373 crate::planning::semantics::DataDefinition::Value {
2374 value: crate::planning::semantics::LiteralValue::quantity_with_type(
2375 rational_zero(),
2376 "eur".to_string(),
2377 Arc::new(money.clone()),
2378 ),
2379 source,
2380 },
2381 );
2382
2383 let plan = ExecutionPlan {
2384 spec_name: "test".to_string(),
2385 commentary: None,
2386 data,
2387 rules: Vec::new(),
2388 max_register_count: 0,
2389 reference_evaluation_order: Vec::new(),
2390 meta: HashMap::new(),
2391 resolved_types: ResolvedSpecTypes::default(),
2392 signature_index: HashMap::new(),
2393 effective: EffectiveDate::Origin,
2394 sources: Vec::new(),
2395 };
2396
2397 let values = input_data(&[("price", "1.234 eur")]);
2398
2399 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2400 match overlay.violated.get(&data_path) {
2401 Some(reason) => {
2402 assert!(
2403 reason.contains("decimals") || reason.contains("decimal"),
2404 "1.234 eur must violate decimals=2, got: {reason}"
2405 );
2406 }
2407 None => panic!("expected violated data for price=1.234 eur"),
2408 }
2409 }
2410
2411 #[test]
2412 fn test_serialize_deserialize_execution_plan() {
2413 let data_path = DataPath {
2414 segments: vec![],
2415 data: "age".to_string(),
2416 };
2417 let mut data = IndexMap::new();
2418 data.insert(
2419 data_path.clone(),
2420 crate::planning::semantics::DataDefinition::Value {
2421 value: create_number_literal(0.into()),
2422 source: test_source(),
2423 },
2424 );
2425 let plan = ExecutionPlan {
2426 spec_name: "test".to_string(),
2427 commentary: None,
2428 data,
2429 rules: Vec::new(),
2430 max_register_count: 0,
2431 reference_evaluation_order: Vec::new(),
2432 meta: HashMap::new(),
2433 resolved_types: ResolvedSpecTypes::default(),
2434 signature_index: HashMap::new(),
2435 effective: EffectiveDate::Origin,
2436 sources: Vec::new(),
2437 };
2438
2439 let deserialized = roundtrip_execution_plan(&plan);
2440
2441 assert_eq!(deserialized.spec_name, plan.spec_name);
2442 assert_eq!(deserialized.data.len(), plan.data.len());
2443 assert_eq!(deserialized.rules.len(), plan.rules.len());
2444 }
2445
2446 #[test]
2447 fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
2448 let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
2449 let imported_type = crate::planning::semantics::LemmaType::new(
2450 "salary".to_string(),
2451 TypeSpecification::quantity(),
2452 crate::planning::semantics::TypeExtends::Custom {
2453 parent: "money".to_string(),
2454 family: "money".to_string(),
2455 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
2456 spec: Arc::clone(&dep_spec),
2457 },
2458 },
2459 );
2460
2461 let salary_path = DataPath::new(vec![], "salary".to_string());
2462 let mut data = IndexMap::new();
2463 data.insert(
2464 salary_path,
2465 crate::planning::semantics::DataDefinition::TypeDeclaration {
2466 resolved_type: Arc::new(imported_type),
2467 declared_default: None,
2468 source: test_source(),
2469 },
2470 );
2471
2472 let plan = ExecutionPlan {
2473 spec_name: "test".to_string(),
2474 commentary: None,
2475 data,
2476 rules: Vec::new(),
2477 max_register_count: 0,
2478 reference_evaluation_order: Vec::new(),
2479 meta: HashMap::new(),
2480 resolved_types: ResolvedSpecTypes::default(),
2481 signature_index: HashMap::new(),
2482 effective: EffectiveDate::Origin,
2483 sources: Vec::new(),
2484 };
2485
2486 let deserialized = roundtrip_execution_plan(&plan);
2487
2488 let recovered = deserialized
2489 .data
2490 .get(&DataPath::new(vec![], "salary".to_string()))
2491 .and_then(|d| d.schema_type())
2492 .expect("salary type should be present in plan.data");
2493 match &recovered.extends {
2494 crate::planning::semantics::TypeExtends::Custom {
2495 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
2496 ..
2497 } => {
2498 assert_eq!(spec.name, "examples");
2499 }
2500 other => panic!(
2501 "Expected imported defining_spec after round-trip, got {:?}",
2502 other
2503 ),
2504 }
2505 }
2506
2507 #[test]
2508 fn test_serialize_deserialize_plan_with_rules() {
2509 use crate::planning::semantics::ExpressionKind;
2510
2511 let age_path = DataPath::new(vec![], "age".to_string());
2512 let mut data = IndexMap::new();
2513 data.insert(
2514 age_path.clone(),
2515 crate::planning::semantics::DataDefinition::Value {
2516 value: create_number_literal(0.into()),
2517 source: test_source(),
2518 },
2519 );
2520 let mut plan = ExecutionPlan {
2521 spec_name: "test".to_string(),
2522 commentary: None,
2523 data,
2524 rules: Vec::new(),
2525 max_register_count: 0,
2526 reference_evaluation_order: Vec::new(),
2527 meta: HashMap::new(),
2528 resolved_types: ResolvedSpecTypes::default(),
2529 signature_index: HashMap::new(),
2530 effective: EffectiveDate::Origin,
2531 sources: Vec::new(),
2532 };
2533
2534 let rule = ExecutableRule {
2535 path: RulePath::new(vec![], "can_drive".to_string()),
2536 name: "can_drive".to_string(),
2537 branches: vec![{
2538 let result = create_literal_expr(create_boolean_literal(true));
2539 let condition = Expression::new(
2540 ExpressionKind::Comparison(
2541 Arc::new(create_data_path_expr(age_path.clone())),
2542 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2543 Arc::new(create_literal_expr(create_number_literal(18.into()))),
2544 ),
2545 test_source(),
2546 );
2547 Branch {
2548 condition: Some(condition.clone()),
2549 result: result.clone(),
2550 source: test_source(),
2551 }
2552 }],
2553 instructions: constant_return_instructions(create_boolean_literal(true)),
2554 source_instructions: constant_return_instructions(create_boolean_literal(true)),
2555 source: test_source(),
2556 rule_type: Arc::new(primitive_boolean().clone()),
2557 };
2558
2559 plan.rules.push(rule);
2560 plan.max_register_count = plan.rules[0].instructions.register_count;
2561
2562 let deserialized = roundtrip_execution_plan(&plan);
2563
2564 assert_eq!(deserialized.spec_name, plan.spec_name);
2565 assert_eq!(deserialized.data.len(), plan.data.len());
2566 assert_eq!(deserialized.rules.len(), plan.rules.len());
2567 assert_eq!(deserialized.rules[0].name, "can_drive");
2568 assert_eq!(deserialized.rules[0].branches.len(), 1);
2569 }
2570
2571 #[test]
2572 fn test_serialize_deserialize_plan_with_nested_data_paths() {
2573 use crate::planning::semantics::PathSegment;
2574 let data_path = DataPath {
2575 segments: vec![PathSegment {
2576 data: "employee".to_string(),
2577 spec: "private".to_string(),
2578 }],
2579 data: "salary".to_string(),
2580 };
2581
2582 let mut data = IndexMap::new();
2583 data.insert(
2584 data_path.clone(),
2585 crate::planning::semantics::DataDefinition::Value {
2586 value: create_number_literal(0.into()),
2587 source: test_source(),
2588 },
2589 );
2590 let plan = ExecutionPlan {
2591 spec_name: "test".to_string(),
2592 commentary: None,
2593 data,
2594 rules: Vec::new(),
2595 max_register_count: 0,
2596 reference_evaluation_order: Vec::new(),
2597 meta: HashMap::new(),
2598 resolved_types: ResolvedSpecTypes::default(),
2599 signature_index: HashMap::new(),
2600 effective: EffectiveDate::Origin,
2601 sources: Vec::new(),
2602 };
2603
2604 let deserialized = roundtrip_execution_plan(&plan);
2605
2606 assert_eq!(deserialized.data.len(), 1);
2607 let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
2608 assert_eq!(deserialized_path.segments.len(), 1);
2609 assert_eq!(deserialized_path.segments[0].data, "employee");
2610 assert_eq!(deserialized_path.data, "salary");
2611 }
2612
2613 #[test]
2614 fn test_serialize_deserialize_plan_with_multiple_data_types() {
2615 let name_path = DataPath::new(vec![], "name".to_string());
2616 let age_path = DataPath::new(vec![], "age".to_string());
2617 let active_path = DataPath::new(vec![], "active".to_string());
2618
2619 let mut data = IndexMap::new();
2620 data.insert(
2621 name_path.clone(),
2622 crate::planning::semantics::DataDefinition::Value {
2623 value: create_text_literal("Alice".to_string()),
2624 source: test_source(),
2625 },
2626 );
2627 data.insert(
2628 age_path.clone(),
2629 crate::planning::semantics::DataDefinition::Value {
2630 value: create_number_literal(30.into()),
2631 source: test_source(),
2632 },
2633 );
2634 data.insert(
2635 active_path.clone(),
2636 crate::planning::semantics::DataDefinition::Value {
2637 value: create_boolean_literal(true),
2638 source: test_source(),
2639 },
2640 );
2641
2642 let plan = ExecutionPlan {
2643 spec_name: "test".to_string(),
2644 commentary: None,
2645 data,
2646 rules: Vec::new(),
2647 max_register_count: 0,
2648 reference_evaluation_order: Vec::new(),
2649 meta: HashMap::new(),
2650 resolved_types: ResolvedSpecTypes::default(),
2651 signature_index: HashMap::new(),
2652 effective: EffectiveDate::Origin,
2653 sources: Vec::new(),
2654 };
2655
2656 let deserialized = roundtrip_execution_plan(&plan);
2657
2658 assert_eq!(deserialized.data.len(), 3);
2659
2660 assert_eq!(
2661 deserialized.get_data_value(&name_path).unwrap().value,
2662 crate::planning::semantics::ValueKind::Text("Alice".to_string())
2663 );
2664 assert_eq!(
2665 deserialized.get_data_value(&age_path).unwrap().value,
2666 crate::planning::semantics::ValueKind::Number(rational_new(30, 1))
2667 );
2668 assert_eq!(
2669 deserialized.get_data_value(&active_path).unwrap().value,
2670 crate::planning::semantics::ValueKind::Boolean(true)
2671 );
2672 }
2673
2674 #[test]
2675 fn test_serialize_deserialize_plan_with_multiple_branches() {
2676 use crate::planning::semantics::ExpressionKind;
2677
2678 let points_path = DataPath::new(vec![], "points".to_string());
2679 let mut data = IndexMap::new();
2680 data.insert(
2681 points_path.clone(),
2682 crate::planning::semantics::DataDefinition::Value {
2683 value: create_number_literal(0.into()),
2684 source: test_source(),
2685 },
2686 );
2687 let mut plan = ExecutionPlan {
2688 spec_name: "test".to_string(),
2689 commentary: None,
2690 data,
2691 rules: Vec::new(),
2692 max_register_count: 0,
2693 reference_evaluation_order: Vec::new(),
2694 meta: HashMap::new(),
2695 resolved_types: ResolvedSpecTypes::default(),
2696 signature_index: HashMap::new(),
2697 effective: EffectiveDate::Origin,
2698 sources: Vec::new(),
2699 };
2700
2701 let rule = ExecutableRule {
2702 path: RulePath::new(vec![], "tier".to_string()),
2703 name: "tier".to_string(),
2704 branches: vec![
2705 {
2706 let result = create_literal_expr(create_text_literal("bronze".to_string()));
2707 Branch {
2708 condition: None,
2709 result: result.clone(),
2710 source: test_source(),
2711 }
2712 },
2713 {
2714 let result = create_literal_expr(create_text_literal("silver".to_string()));
2715 Branch {
2716 condition: Some(Expression::new(
2717 ExpressionKind::Comparison(
2718 Arc::new(create_data_path_expr(points_path.clone())),
2719 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2720 Arc::new(create_literal_expr(create_number_literal(100.into()))),
2721 ),
2722 test_source(),
2723 )),
2724 result: result.clone(),
2725 source: test_source(),
2726 }
2727 },
2728 {
2729 let result = create_literal_expr(create_text_literal("gold".to_string()));
2730 Branch {
2731 condition: Some(Expression::new(
2732 ExpressionKind::Comparison(
2733 Arc::new(create_data_path_expr(points_path.clone())),
2734 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2735 Arc::new(create_literal_expr(create_number_literal(500.into()))),
2736 ),
2737 test_source(),
2738 )),
2739 result: result.clone(),
2740 source: test_source(),
2741 }
2742 },
2743 ],
2744 instructions: constant_return_instructions(create_text_literal("bronze".to_string())),
2745 source_instructions: constant_return_instructions(create_text_literal(
2746 "bronze".to_string(),
2747 )),
2748 source: test_source(),
2749 rule_type: Arc::new(primitive_text().clone()),
2750 };
2751
2752 plan.rules.push(rule);
2753 plan.max_register_count = plan.rules[0].instructions.register_count;
2754
2755 let deserialized = roundtrip_execution_plan(&plan);
2756
2757 assert_eq!(deserialized.rules.len(), 1);
2758 assert_eq!(deserialized.rules[0].branches.len(), 3);
2759 assert!(deserialized.rules[0].branches[0].condition.is_none());
2760 assert!(deserialized.rules[0].branches[1].condition.is_some());
2761 assert!(deserialized.rules[0].branches[2].condition.is_some());
2762 }
2763
2764 #[test]
2765 fn test_serialize_deserialize_empty_plan() {
2766 let plan = ExecutionPlan {
2767 spec_name: "empty".to_string(),
2768 commentary: None,
2769 data: IndexMap::new(),
2770 rules: Vec::new(),
2771 max_register_count: 0,
2772 reference_evaluation_order: Vec::new(),
2773 meta: HashMap::new(),
2774 resolved_types: ResolvedSpecTypes::default(),
2775 signature_index: HashMap::new(),
2776 effective: EffectiveDate::Origin,
2777 sources: Vec::new(),
2778 };
2779
2780 let deserialized = roundtrip_execution_plan(&plan);
2781
2782 assert_eq!(deserialized.spec_name, "empty");
2783 assert_eq!(deserialized.data.len(), 0);
2784 assert_eq!(deserialized.rules.len(), 0);
2785 }
2786
2787 #[test]
2788 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
2789 use crate::planning::semantics::ExpressionKind;
2790
2791 let x_path = DataPath::new(vec![], "x".to_string());
2792 let mut data = IndexMap::new();
2793 data.insert(
2794 x_path.clone(),
2795 crate::planning::semantics::DataDefinition::Value {
2796 value: create_number_literal(0.into()),
2797 source: test_source(),
2798 },
2799 );
2800 let mut plan = ExecutionPlan {
2801 spec_name: "test".to_string(),
2802 commentary: None,
2803 data,
2804 rules: Vec::new(),
2805 max_register_count: 0,
2806 reference_evaluation_order: Vec::new(),
2807 meta: HashMap::new(),
2808 resolved_types: ResolvedSpecTypes::default(),
2809 signature_index: HashMap::new(),
2810 effective: EffectiveDate::Origin,
2811 sources: Vec::new(),
2812 };
2813
2814 let rule = ExecutableRule {
2815 path: RulePath::new(vec![], "doubled".to_string()),
2816 name: "doubled".to_string(),
2817 branches: vec![{
2818 let result = Expression::new(
2819 ExpressionKind::Arithmetic(
2820 Arc::new(create_data_path_expr(x_path.clone())),
2821 crate::parsing::ast::ArithmeticComputation::Multiply,
2822 Arc::new(create_literal_expr(create_number_literal(2.into()))),
2823 ),
2824 test_source(),
2825 );
2826 Branch {
2827 condition: None,
2828 result: result.clone(),
2829 source: test_source(),
2830 }
2831 }],
2832 instructions: constant_return_instructions(create_number_literal(0.into())),
2833 source_instructions: constant_return_instructions(create_number_literal(0.into())),
2834 source: test_source(),
2835 rule_type: Arc::new(crate::planning::semantics::primitive_number().clone()),
2836 };
2837
2838 plan.rules.push(rule);
2839 plan.max_register_count = plan.rules[0].instructions.register_count;
2840
2841 let deserialized = roundtrip_execution_plan(&plan);
2842
2843 assert_eq!(deserialized.rules.len(), 1);
2844 match &deserialized.rules[0].branches[0].result.kind {
2845 ExpressionKind::Arithmetic(left, op, right) => {
2846 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
2847 match &left.kind {
2848 ExpressionKind::DataPath(_) => {}
2849 _ => panic!("Expected DataPath in left operand"),
2850 }
2851 match &right.kind {
2852 ExpressionKind::Literal(_) => {}
2853 _ => panic!("Expected Literal in right operand"),
2854 }
2855 }
2856 _ => panic!("Expected Arithmetic expression"),
2857 }
2858 }
2859
2860 #[test]
2861 fn test_serialize_deserialize_round_trip_equality() {
2862 use crate::planning::semantics::ExpressionKind;
2863
2864 let age_path = DataPath::new(vec![], "age".to_string());
2865 let mut data = IndexMap::new();
2866 data.insert(
2867 age_path.clone(),
2868 crate::planning::semantics::DataDefinition::Value {
2869 value: create_number_literal(0.into()),
2870 source: test_source(),
2871 },
2872 );
2873 let mut plan = ExecutionPlan {
2874 spec_name: "test".to_string(),
2875 commentary: None,
2876 data,
2877 rules: Vec::new(),
2878 max_register_count: 0,
2879 reference_evaluation_order: Vec::new(),
2880 meta: HashMap::new(),
2881 resolved_types: ResolvedSpecTypes::default(),
2882 signature_index: HashMap::new(),
2883 effective: EffectiveDate::Origin,
2884 sources: Vec::new(),
2885 };
2886
2887 let rule = ExecutableRule {
2888 path: RulePath::new(vec![], "is_adult".to_string()),
2889 name: "is_adult".to_string(),
2890 branches: vec![{
2891 let result = create_literal_expr(create_boolean_literal(true));
2892 let condition = Expression::new(
2893 ExpressionKind::Comparison(
2894 Arc::new(create_data_path_expr(age_path.clone())),
2895 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2896 Arc::new(create_literal_expr(create_number_literal(18.into()))),
2897 ),
2898 test_source(),
2899 );
2900 Branch {
2901 condition: Some(condition.clone()),
2902 result: result.clone(),
2903 source: test_source(),
2904 }
2905 }],
2906 instructions: constant_return_instructions(create_boolean_literal(true)),
2907 source_instructions: constant_return_instructions(create_boolean_literal(true)),
2908 source: test_source(),
2909 rule_type: Arc::new(primitive_boolean().clone()),
2910 };
2911
2912 plan.rules.push(rule);
2913 plan.max_register_count = plan.rules[0].instructions.register_count;
2914
2915 let deserialized = roundtrip_execution_plan(&plan);
2916 let deserialized2 = roundtrip_execution_plan(&deserialized);
2917
2918 assert_eq!(deserialized2.spec_name, plan.spec_name);
2919 assert_eq!(deserialized2.data.len(), plan.data.len());
2920 assert_eq!(deserialized2.rules.len(), plan.rules.len());
2921 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
2922 assert_eq!(
2923 deserialized2.rules[0].branches.len(),
2924 plan.rules[0].branches.len()
2925 );
2926 }
2927
2928 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
2929 ExecutionPlan {
2930 spec_name: "s".into(),
2931 commentary: None,
2932 data: IndexMap::new(),
2933 rules: Vec::new(),
2934 max_register_count: 0,
2935 reference_evaluation_order: Vec::new(),
2936 meta: HashMap::new(),
2937 resolved_types: ResolvedSpecTypes::default(),
2938 signature_index: HashMap::new(),
2939 effective,
2940 sources: Vec::new(),
2941 }
2942 }
2943
2944 #[test]
2945 fn plan_at_exact_boundary_selects_later_slice() {
2946 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2947
2948 let june = DateTimeValue {
2949 year: 2025,
2950 month: 6,
2951 day: 1,
2952 hour: 0,
2953 minute: 0,
2954 second: 0,
2955 microsecond: 0,
2956 timezone: None,
2957
2958 granularity: DateGranularity::Full,
2959 };
2960 let dec = DateTimeValue {
2961 year: 2025,
2962 month: 12,
2963 day: 1,
2964 hour: 0,
2965 minute: 0,
2966 second: 0,
2967 microsecond: 0,
2968 timezone: None,
2969
2970 granularity: DateGranularity::Full,
2971 };
2972
2973 let set = ExecutionPlanSet {
2974 spec_name: "s".into(),
2975 plans: vec![
2976 empty_plan(EffectiveDate::Origin),
2977 empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2978 empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2979 ],
2980 };
2981
2982 assert!(std::ptr::eq(
2983 set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2984 .expect("boundary instant"),
2985 &set.plans[1]
2986 ));
2987 assert!(std::ptr::eq(
2988 set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2989 .expect("dec boundary"),
2990 &set.plans[2]
2991 ));
2992 }
2993
2994 #[test]
2995 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2996 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2997
2998 let june = DateTimeValue {
2999 year: 2025,
3000 month: 6,
3001 day: 1,
3002 hour: 0,
3003 minute: 0,
3004 second: 0,
3005 microsecond: 0,
3006 timezone: None,
3007
3008 granularity: DateGranularity::Full,
3009 };
3010 let may_end = DateTimeValue {
3011 year: 2025,
3012 month: 5,
3013 day: 31,
3014 hour: 23,
3015 minute: 59,
3016 second: 59,
3017 microsecond: 0,
3018 timezone: None,
3019
3020 granularity: DateGranularity::DateTime,
3021 };
3022
3023 let set = ExecutionPlanSet {
3024 spec_name: "s".into(),
3025 plans: vec![
3026 empty_plan(EffectiveDate::Origin),
3027 empty_plan(EffectiveDate::DateTimeValue(june)),
3028 ],
3029 };
3030
3031 assert!(std::ptr::eq(
3032 set.plan_at(&EffectiveDate::DateTimeValue(may_end))
3033 .expect("may 31"),
3034 &set.plans[0]
3035 ));
3036 }
3037
3038 #[test]
3039 fn plan_at_single_plan_matches_any_instant_after_start() {
3040 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
3041
3042 let t = DateTimeValue {
3043 year: 2025,
3044 month: 3,
3045 day: 1,
3046 hour: 0,
3047 minute: 0,
3048 second: 0,
3049 microsecond: 0,
3050 timezone: None,
3051
3052 granularity: DateGranularity::Full,
3053 };
3054 let set = ExecutionPlanSet {
3055 spec_name: "s".into(),
3056 plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
3057 year: 2025,
3058 month: 1,
3059 day: 1,
3060 hour: 0,
3061 minute: 0,
3062 second: 0,
3063 microsecond: 0,
3064 timezone: None,
3065
3066 granularity: DateGranularity::Full,
3067 }))],
3068 };
3069 assert!(std::ptr::eq(
3070 set.plan_at(&EffectiveDate::DateTimeValue(t))
3071 .expect("inside single slice"),
3072 &set.plans[0]
3073 ));
3074 }
3075
3076 #[test]
3079 fn schema_json_shape_contract() {
3080 let mut engine = Engine::new();
3081 engine
3082 .load(
3083 r#"
3084 spec pricing
3085 data bridge_height: quantity
3086 -> unit meter 1
3087 -> default 100 meter
3088 data quantity: number -> minimum 0
3089 rule cost: bridge_height * quantity
3090 "#,
3091 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3092 "test.lemma",
3093 ))),
3094 )
3095 .unwrap();
3096 let now = DateTimeValue::now();
3097 let schema = engine
3098 .get_plan(None, "pricing", Some(&now))
3099 .unwrap()
3100 .schema(&DataOverlay::default());
3101
3102 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3103
3104 let bh = &value["data"]["bridge_height"];
3105 assert!(
3106 bh.is_object(),
3107 "data entry must be a named object, not tuple"
3108 );
3109 assert!(
3110 bh.get("type").is_some(),
3111 "data entry must expose `type` field"
3112 );
3113 assert!(
3114 bh.get("default").is_some(),
3115 "bridge_height exposes `-> default` as schema default suggestion"
3116 );
3117 assert!(
3118 bh.get("bound_value").is_none(),
3119 "bridge_height is not a spec-bound literal"
3120 );
3121
3122 let ty = &bh["type"];
3123 assert_eq!(
3124 ty["kind"], "quantity",
3125 "kind tag sits on the type object itself"
3126 );
3127 assert!(
3128 ty["units"].is_array(),
3129 "quantity-only fields flatten up to top level"
3130 );
3131 assert!(
3132 ty.get("options").is_none(),
3133 "text-only fields must not leak"
3134 );
3135
3136 let qty = &value["data"]["quantity"];
3137 assert_eq!(qty["type"]["kind"], "number");
3138 assert!(
3139 qty.get("default").is_none(),
3140 "quantity has no default suggestion"
3141 );
3142 assert!(
3143 qty.get("bound_value").is_none(),
3144 "quantity has no bound literal"
3145 );
3146
3147 let cost = &value["rules"]["cost"];
3148 assert_eq!(
3149 cost["kind"], "quantity",
3150 "rule types use the same flat shape"
3151 );
3152 assert!(
3153 cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
3154 "quantity rule result types expose declared units"
3155 );
3156 assert!(
3157 cost["units"][0].get("factor").is_some(),
3158 "quantity rule units use factor field"
3159 );
3160 }
3161
3162 #[test]
3163 fn schema_rule_result_units_contract() {
3164 let mut engine = Engine::new();
3165 engine
3166 .load(
3167 r#"
3168 spec units_contract
3169 data money: quantity
3170 -> unit eur 1
3171 -> unit usd 0.91
3172 data rate: ratio
3173 -> unit basis_points 10000
3174 -> unit percent 100
3175 -> default 500 basis_points
3176 rule total: money
3177 rule rate_out: rate
3178 "#,
3179 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3180 "units_contract.lemma",
3181 ))),
3182 )
3183 .unwrap();
3184 let now = DateTimeValue::now();
3185 let schema = engine
3186 .get_plan(None, "units_contract", Some(&now))
3187 .unwrap()
3188 .schema(&DataOverlay::default());
3189 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3190
3191 let money_units = &value["data"]["money"]["type"]["units"];
3192 assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
3193 assert!(money_units[0].get("name").is_some());
3194 assert!(money_units[0].get("factor").is_some());
3195 assert!(money_units[0]["factor"].get("numer").is_some());
3196 assert!(money_units[0]["factor"].get("denom").is_some());
3197
3198 let rate_units = &value["data"]["rate"]["type"]["units"];
3199 assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
3200 assert!(rate_units[0].get("name").is_some());
3201 assert!(rate_units[0].get("value").is_some());
3202 assert!(rate_units[0]["value"].get("numer").is_some());
3203 assert!(rate_units[0]["value"].get("denom").is_some());
3204
3205 let total_rule_units = &value["rules"]["total"]["units"];
3206 let money_unit_names: Vec<_> = money_units
3207 .as_array()
3208 .unwrap()
3209 .iter()
3210 .map(|u| u["name"].as_str().unwrap())
3211 .collect();
3212 let total_rule_unit_names: Vec<_> = total_rule_units
3213 .as_array()
3214 .unwrap()
3215 .iter()
3216 .map(|u| u["name"].as_str().unwrap())
3217 .collect();
3218 assert_eq!(total_rule_unit_names, money_unit_names);
3219
3220 let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
3221 let rate_unit_names: Vec<_> = rate_units
3222 .as_array()
3223 .unwrap()
3224 .iter()
3225 .map(|u| u["name"].as_str().unwrap())
3226 .collect();
3227 let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
3228 .as_array()
3229 .unwrap()
3230 .iter()
3231 .map(|u| u["name"].as_str().unwrap())
3232 .collect();
3233 assert_eq!(rate_out_rule_unit_names, rate_unit_names);
3234 }
3235
3236 #[test]
3237 fn schema_json_round_trip_preserves_shape() {
3238 let mut engine = Engine::new();
3239 engine
3240 .load(
3241 r#"
3242 spec s
3243 data age: number -> minimum 0 -> default 18
3244 data grade: text -> options "A" "B" "C"
3245 rule adult: age >= 18
3246 "#,
3247 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
3248 )
3249 .unwrap();
3250 let now = DateTimeValue::now();
3251 let schema = engine
3252 .get_plan(None, "s", Some(&now))
3253 .unwrap()
3254 .schema(&DataOverlay::default());
3255
3256 let json = serde_json::to_string(&schema).unwrap();
3257 let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
3258 assert_eq!(schema, round_tripped);
3259 }
3260}
3261
3262