1use crate::parsing::ast::{DateTimeValue, LemmaSpec, MetaValue};
8use crate::planning::graph::Graph;
9use crate::planning::semantics;
10use crate::planning::semantics::{
11 Expression, FactData, FactPath, LemmaType, LiteralValue, RulePath, TypeSpecification, ValueKind,
12};
13use crate::planning::types::ResolvedSpecTypes;
14use crate::Error;
15use crate::ResourceLimits;
16use crate::Source;
17use indexmap::IndexMap;
18use serde::{Deserialize, Serialize};
19use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
20use std::sync::Arc;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ExecutionPlan {
28 pub spec_name: String,
30
31 #[serde(serialize_with = "crate::serialization::serialize_resolved_fact_value_map")]
33 #[serde(deserialize_with = "crate::serialization::deserialize_resolved_fact_value_map")]
34 pub facts: IndexMap<FactPath, FactData>,
35
36 pub rules: Vec<ExecutableRule>,
38
39 pub sources: HashMap<String, String>,
41
42 pub meta: HashMap<String, MetaValue>,
44
45 pub named_types: BTreeMap<String, LemmaType>,
48
49 pub valid_from: Option<DateTimeValue>,
51
52 pub valid_to: Option<DateTimeValue>,
54}
55
56impl ExecutionPlan {
57 #[must_use]
60 pub fn plan_hash(&self) -> String {
61 crate::planning::fingerprint::fingerprint_hash(&crate::planning::fingerprint::from_plan(
62 self,
63 ))
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ExecutableRule {
72 pub path: RulePath,
74
75 pub name: String,
77
78 pub branches: Vec<Branch>,
83
84 pub needs_facts: BTreeSet<FactPath>,
86
87 pub source: Source,
89
90 pub rule_type: LemmaType,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Branch {
98 pub condition: Option<Expression>,
100
101 pub result: Expression,
103
104 pub source: Source,
106}
107
108pub(crate) fn build_execution_plan(
111 graph: &Graph,
112 resolved_types: &HashMap<Arc<LemmaSpec>, ResolvedSpecTypes>,
113 valid_from: Option<DateTimeValue>,
114 valid_to: Option<DateTimeValue>,
115) -> ExecutionPlan {
116 let facts = graph.build_facts();
117 let execution_order = graph.execution_order();
118
119 let mut executable_rules: Vec<ExecutableRule> = Vec::new();
120 let mut path_to_index: HashMap<RulePath, usize> = HashMap::new();
121
122 for rule_path in execution_order {
123 let rule_node = graph.rules().get(rule_path).expect(
124 "bug: rule from topological sort not in graph - validation should have caught this",
125 );
126
127 let mut direct_facts = HashSet::new();
128 for (condition, result) in &rule_node.branches {
129 if let Some(cond) = condition {
130 cond.collect_fact_paths(&mut direct_facts);
131 }
132 result.collect_fact_paths(&mut direct_facts);
133 }
134 let mut needs_facts: BTreeSet<FactPath> = direct_facts.into_iter().collect();
135
136 for dep in &rule_node.depends_on_rules {
137 if let Some(&dep_idx) = path_to_index.get(dep) {
138 needs_facts.extend(executable_rules[dep_idx].needs_facts.iter().cloned());
139 }
140 }
141
142 let mut executable_branches = Vec::new();
143 for (condition, result) in &rule_node.branches {
144 executable_branches.push(Branch {
145 condition: condition.clone(),
146 result: result.clone(),
147 source: rule_node.source.clone(),
148 });
149 }
150
151 path_to_index.insert(rule_path.clone(), executable_rules.len());
152 executable_rules.push(ExecutableRule {
153 path: rule_path.clone(),
154 name: rule_path.rule.clone(),
155 branches: executable_branches,
156 source: rule_node.source.clone(),
157 needs_facts,
158 rule_type: rule_node.rule_type.clone(),
159 });
160 }
161
162 let main_spec = graph.main_spec();
163 let named_types = build_type_tables(main_spec, resolved_types);
164
165 ExecutionPlan {
166 spec_name: main_spec.name.clone(),
167 facts,
168 rules: executable_rules,
169 sources: graph.sources().clone(),
170 meta: main_spec
171 .meta_fields
172 .iter()
173 .map(|f| (f.key.clone(), f.value.clone()))
174 .collect(),
175 named_types,
176 valid_from,
177 valid_to,
178 }
179}
180
181fn build_type_tables(
183 main_spec: &Arc<LemmaSpec>,
184 resolved_types: &HashMap<Arc<LemmaSpec>, ResolvedSpecTypes>,
185) -> BTreeMap<String, LemmaType> {
186 let mut named_types = BTreeMap::new();
187
188 let main_resolved = resolved_types
189 .iter()
190 .find(|(spec, _)| Arc::ptr_eq(spec, main_spec))
191 .map(|(_, types)| types);
192
193 if let Some(resolved) = main_resolved {
194 for (type_name, lemma_type) in &resolved.named_types {
195 named_types.insert(type_name.clone(), lemma_type.clone());
196 }
197 }
198
199 named_types
200}
201
202#[derive(Debug, Clone, Serialize)]
214pub struct SpecSchema {
215 pub spec: String,
217 pub facts: indexmap::IndexMap<String, (LemmaType, Option<LiteralValue>)>,
219 pub rules: indexmap::IndexMap<String, LemmaType>,
221 pub meta: HashMap<String, MetaValue>,
223}
224
225impl std::fmt::Display for SpecSchema {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 write!(f, "Spec: {}", self.spec)?;
228
229 if !self.meta.is_empty() {
230 write!(f, "\n\nMeta:")?;
231 let mut keys: Vec<&String> = self.meta.keys().collect();
233 keys.sort();
234 for key in keys {
235 write!(f, "\n {}: {}", key, self.meta.get(key).unwrap())?;
236 }
237 }
238
239 if !self.facts.is_empty() {
240 write!(f, "\n\nFacts:")?;
241 for (name, (lemma_type, default)) in &self.facts {
242 write!(f, "\n {} ({}", name, lemma_type.name())?;
243 if let Some(constraints) = format_type_constraints(&lemma_type.specifications) {
244 write!(f, ", {}", constraints)?;
245 }
246 if let Some(val) = default {
247 write!(f, ", default: {}", val)?;
248 }
249 write!(f, ")")?;
250 }
251 }
252
253 if !self.rules.is_empty() {
254 write!(f, "\n\nRules:")?;
255 for (name, rule_type) in &self.rules {
256 write!(f, "\n {} ({})", name, rule_type.name())?;
257 }
258 }
259
260 if self.facts.is_empty() && self.rules.is_empty() {
261 write!(f, "\n (no facts or rules)")?;
262 }
263
264 Ok(())
265 }
266}
267
268fn format_type_constraints(spec: &TypeSpecification) -> Option<String> {
271 let mut parts = Vec::new();
272
273 match spec {
274 TypeSpecification::Number {
275 minimum, maximum, ..
276 } => {
277 if let Some(v) = minimum {
278 parts.push(format!("minimum: {}", v));
279 }
280 if let Some(v) = maximum {
281 parts.push(format!("maximum: {}", v));
282 }
283 }
284 TypeSpecification::Scale {
285 minimum,
286 maximum,
287 decimals,
288 units,
289 ..
290 } => {
291 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
292 if !unit_names.is_empty() {
293 parts.push(format!("units: {}", unit_names.join(", ")));
294 }
295 if let Some(v) = minimum {
296 parts.push(format!("minimum: {}", v));
297 }
298 if let Some(v) = maximum {
299 parts.push(format!("maximum: {}", v));
300 }
301 if let Some(d) = decimals {
302 parts.push(format!("decimals: {}", d));
303 }
304 }
305 TypeSpecification::Ratio {
306 minimum, maximum, ..
307 } => {
308 if let Some(v) = minimum {
309 parts.push(format!("minimum: {}", v));
310 }
311 if let Some(v) = maximum {
312 parts.push(format!("maximum: {}", v));
313 }
314 }
315 TypeSpecification::Text { options, .. } => {
316 if !options.is_empty() {
317 let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
318 parts.push(format!("options: {}", quoted.join(", ")));
319 }
320 }
321 TypeSpecification::Date {
322 minimum, maximum, ..
323 } => {
324 if let Some(v) = minimum {
325 parts.push(format!("minimum: {}", v));
326 }
327 if let Some(v) = maximum {
328 parts.push(format!("maximum: {}", v));
329 }
330 }
331 TypeSpecification::Time {
332 minimum, maximum, ..
333 } => {
334 if let Some(v) = minimum {
335 parts.push(format!("minimum: {}", v));
336 }
337 if let Some(v) = maximum {
338 parts.push(format!("maximum: {}", v));
339 }
340 }
341 TypeSpecification::Boolean { .. }
342 | TypeSpecification::Duration { .. }
343 | TypeSpecification::Veto { .. }
344 | TypeSpecification::Undetermined => {}
345 }
346
347 if parts.is_empty() {
348 None
349 } else {
350 Some(parts.join(", "))
351 }
352}
353
354impl ExecutionPlan {
355 pub fn schema(&self) -> SpecSchema {
363 let mut fact_entries: Vec<(usize, String, (LemmaType, Option<LiteralValue>))> = self
364 .facts
365 .iter()
366 .filter(|(_, data)| data.schema_type().is_some())
367 .map(|(path, data)| {
368 let lemma_type = data.schema_type().unwrap().clone();
369 let value = data.explicit_value().cloned();
370 (
371 data.source().span.start,
372 path.input_key(),
373 (lemma_type, value),
374 )
375 })
376 .collect();
377 fact_entries.sort_by_key(|(pos, _, _)| *pos);
378 let fact_entries: Vec<(String, (LemmaType, Option<LiteralValue>))> = fact_entries
379 .into_iter()
380 .map(|(_, name, data)| (name, data))
381 .collect();
382
383 let rule_entries: Vec<(String, LemmaType)> = self
384 .rules
385 .iter()
386 .filter(|r| r.path.segments.is_empty())
387 .map(|r| (r.name.clone(), r.rule_type.clone()))
388 .collect();
389
390 SpecSchema {
391 spec: self.spec_name.clone(),
392 facts: fact_entries.into_iter().collect(),
393 rules: rule_entries.into_iter().collect(),
394 meta: self.meta.clone(),
395 }
396 }
397
398 pub fn schema_for_rules(&self, rule_names: &[String]) -> Result<SpecSchema, Error> {
407 let mut needed_facts = HashSet::new();
408 let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
409
410 for rule_name in rule_names {
411 let rule = self.get_rule(rule_name).ok_or_else(|| {
412 Error::request(
413 format!(
414 "Rule '{}' not found in spec '{}'",
415 rule_name, self.spec_name
416 ),
417 None::<String>,
418 )
419 })?;
420 needed_facts.extend(rule.needs_facts.iter().cloned());
421 rule_entries.push((rule.name.clone(), rule.rule_type.clone()));
422 }
423
424 let mut fact_entries: Vec<(usize, String, (LemmaType, Option<LiteralValue>))> = self
425 .facts
426 .iter()
427 .filter(|(path, _)| needed_facts.contains(path))
428 .filter(|(_, data)| data.schema_type().is_some())
429 .map(|(path, data)| {
430 let lemma_type = data.schema_type().unwrap().clone();
431 let value = data.explicit_value().cloned();
432 (
433 data.source().span.start,
434 path.input_key(),
435 (lemma_type, value),
436 )
437 })
438 .collect();
439 fact_entries.sort_by_key(|(pos, _, _)| *pos);
440 let fact_entries: Vec<(String, (LemmaType, Option<LiteralValue>))> = fact_entries
441 .into_iter()
442 .map(|(_, name, data)| (name, data))
443 .collect();
444
445 Ok(SpecSchema {
446 spec: self.spec_name.clone(),
447 facts: fact_entries.into_iter().collect(),
448 rules: rule_entries.into_iter().collect(),
449 meta: self.meta.clone(),
450 })
451 }
452
453 pub fn get_fact_path_by_str(&self, name: &str) -> Option<&FactPath> {
455 self.facts.keys().find(|path| path.input_key() == name)
456 }
457
458 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
460 self.rules
461 .iter()
462 .find(|r| r.name == name && r.path.segments.is_empty())
463 }
464
465 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
467 self.rules.iter().find(|r| &r.path == rule_path)
468 }
469
470 pub fn get_fact_value(&self, path: &FactPath) -> Option<&LiteralValue> {
472 self.facts.get(path).and_then(|d| d.value())
473 }
474
475 pub fn with_fact_values(
479 mut self,
480 values: HashMap<String, String>,
481 limits: &ResourceLimits,
482 ) -> Result<Self, Error> {
483 for (name, raw_value) in values {
484 let fact_path = self.get_fact_path_by_str(&name).ok_or_else(|| {
485 let available: Vec<String> = self.facts.keys().map(|p| p.input_key()).collect();
486 Error::request(
487 format!(
488 "Fact '{}' not found. Available facts: {}",
489 name,
490 available.join(", ")
491 ),
492 None::<String>,
493 )
494 })?;
495 let fact_path = fact_path.clone();
496
497 let fact_data = self
498 .facts
499 .get(&fact_path)
500 .expect("BUG: fact_path was just resolved from self.facts, must exist");
501
502 let fact_source = fact_data.source().clone();
503 let expected_type = fact_data.schema_type().cloned().ok_or_else(|| {
504 Error::request(
505 format!(
506 "Fact '{}' is a spec reference; cannot provide a value.",
507 name
508 ),
509 None::<String>,
510 )
511 })?;
512
513 let parsed_value = crate::planning::semantics::parse_value_from_string(
515 &raw_value,
516 &expected_type.specifications,
517 &fact_source,
518 )
519 .map_err(|e| {
520 Error::validation(
521 format!(
522 "Failed to parse fact '{}' as {}: {}",
523 name,
524 expected_type.name(),
525 e
526 ),
527 Some(fact_source.clone()),
528 None::<String>,
529 )
530 })?;
531 let semantic_value = semantics::value_to_semantic(&parsed_value).map_err(|e| {
532 Error::validation(
533 format!("Failed to convert fact '{}' value: {}", name, e),
534 Some(fact_source.clone()),
535 None::<String>,
536 )
537 })?;
538 let literal_value = LiteralValue {
539 value: semantic_value,
540 lemma_type: expected_type.clone(),
541 };
542
543 let size = literal_value.byte_size();
545 if size > limits.max_fact_value_bytes {
546 return Err(Error::resource_limit_exceeded(
547 "max_fact_value_bytes",
548 limits.max_fact_value_bytes.to_string(),
549 size.to_string(),
550 format!(
551 "Reduce the size of fact values to {} bytes or less",
552 limits.max_fact_value_bytes
553 ),
554 Some(fact_source.clone()),
555 None,
556 None,
557 ));
558 }
559
560 validate_value_against_type(&expected_type, &literal_value).map_err(|msg| {
562 Error::validation(
563 format!(
564 "Invalid value for fact {} (expected {}): {}",
565 name,
566 expected_type.name(),
567 msg
568 ),
569 Some(fact_source.clone()),
570 None::<String>,
571 )
572 })?;
573
574 self.facts.insert(
575 fact_path,
576 FactData::Value {
577 value: literal_value,
578 source: fact_source,
579 is_default: false,
580 },
581 );
582 }
583
584 Ok(self)
585 }
586}
587
588fn validate_value_against_type(
589 expected_type: &LemmaType,
590 value: &LiteralValue,
591) -> Result<(), String> {
592 use crate::planning::semantics::TypeSpecification;
593
594 let effective_decimals = |n: rust_decimal::Decimal| n.scale();
595
596 match (&expected_type.specifications, &value.value) {
597 (
598 TypeSpecification::Number {
599 minimum,
600 maximum,
601 decimals,
602 ..
603 },
604 ValueKind::Number(n),
605 ) => {
606 if let Some(min) = minimum {
607 if n < min {
608 return Err(format!("{} is below minimum {}", n, min));
609 }
610 }
611 if let Some(max) = maximum {
612 if n > max {
613 return Err(format!("{} is above maximum {}", n, max));
614 }
615 }
616 if let Some(d) = decimals {
617 if effective_decimals(*n) > u32::from(*d) {
618 return Err(format!("{} has more than {} decimals", n, d));
619 }
620 }
621 Ok(())
622 }
623 (
624 TypeSpecification::Scale {
625 minimum,
626 maximum,
627 decimals,
628 ..
629 },
630 ValueKind::Scale(n, _unit),
631 ) => {
632 if let Some(min) = minimum {
633 if n < min {
634 return Err(format!("{} is below minimum {}", n, min));
635 }
636 }
637 if let Some(max) = maximum {
638 if n > max {
639 return Err(format!("{} is above maximum {}", n, max));
640 }
641 }
642 if let Some(d) = decimals {
643 if effective_decimals(*n) > u32::from(*d) {
644 return Err(format!("{} has more than {} decimals", n, d));
645 }
646 }
647 Ok(())
648 }
649 (TypeSpecification::Text { options, .. }, ValueKind::Text(s)) => {
650 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
651 return Err(format!(
652 "'{}' is not in allowed options: {}",
653 s,
654 options.join(", ")
655 ));
656 }
657 Ok(())
658 }
659 _ => Ok(()),
661 }
662}
663
664pub(crate) fn validate_literal_facts_against_types(plan: &ExecutionPlan) -> Vec<Error> {
665 let mut errors = Vec::new();
666
667 for (fact_path, fact_data) in &plan.facts {
668 let (expected_type, lit) = match fact_data {
669 FactData::Value { value, .. } => (&value.lemma_type, value),
670 FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => continue,
671 };
672
673 if let Err(msg) = validate_value_against_type(expected_type, lit) {
674 let source = fact_data.source().clone();
675 errors.push(Error::validation(
676 format!(
677 "Invalid value for fact {} (expected {}): {}",
678 fact_path,
679 expected_type.name(),
680 msg
681 ),
682 Some(source),
683 None::<String>,
684 ));
685 }
686 }
687
688 errors
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694 use crate::parsing::ast::DateTimeValue;
695 use crate::planning::semantics::{
696 primitive_boolean, primitive_text, FactPath, LiteralValue, PathSegment, RulePath,
697 };
698 use crate::Engine;
699 use serde_json;
700 use std::str::FromStr;
701 use std::sync::Arc;
702
703 fn default_limits() -> ResourceLimits {
704 ResourceLimits::default()
705 }
706
707 fn add_lemma_code_blocking(
708 engine: &mut Engine,
709 code: &str,
710 source: &str,
711 ) -> Result<(), crate::Errors> {
712 engine.load(code, crate::SourceType::Labeled(source))
713 }
714
715 #[test]
716 fn test_with_raw_values() {
717 let mut engine = Engine::new();
718 add_lemma_code_blocking(
719 &mut engine,
720 r#"
721 spec test
722 fact age: [number -> default 25]
723 "#,
724 "test.lemma",
725 )
726 .unwrap();
727
728 let now = DateTimeValue::now();
729 let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
730 let fact_path = FactPath::new(vec![], "age".to_string());
731
732 let mut values = HashMap::new();
733 values.insert("age".to_string(), "30".to_string());
734
735 let updated_plan = plan.with_fact_values(values, &default_limits()).unwrap();
736 let updated_value = updated_plan.get_fact_value(&fact_path).unwrap();
737 match &updated_value.value {
738 crate::planning::semantics::ValueKind::Number(n) => {
739 assert_eq!(n, &rust_decimal::Decimal::from(30))
740 }
741 other => panic!("Expected number literal, got {:?}", other),
742 }
743 }
744
745 #[test]
746 fn test_with_raw_values_type_mismatch() {
747 let mut engine = Engine::new();
748 add_lemma_code_blocking(
749 &mut engine,
750 r#"
751 spec test
752 fact age: [number]
753 "#,
754 "test.lemma",
755 )
756 .unwrap();
757
758 let now = DateTimeValue::now();
759 let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
760
761 let mut values = HashMap::new();
762 values.insert("age".to_string(), "thirty".to_string());
763
764 assert!(plan.with_fact_values(values, &default_limits()).is_err());
765 }
766
767 #[test]
768 fn test_with_raw_values_unknown_fact() {
769 let mut engine = Engine::new();
770 add_lemma_code_blocking(
771 &mut engine,
772 r#"
773 spec test
774 fact known: [number]
775 "#,
776 "test.lemma",
777 )
778 .unwrap();
779
780 let now = DateTimeValue::now();
781 let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
782
783 let mut values = HashMap::new();
784 values.insert("unknown".to_string(), "30".to_string());
785
786 assert!(plan.with_fact_values(values, &default_limits()).is_err());
787 }
788
789 #[test]
790 fn test_with_raw_values_nested() {
791 let mut engine = Engine::new();
792 add_lemma_code_blocking(
793 &mut engine,
794 r#"
795 spec private
796 fact base_price: [number]
797
798 spec test
799 fact rules: spec private
800 "#,
801 "test.lemma",
802 )
803 .unwrap();
804
805 let now = DateTimeValue::now();
806 let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
807
808 let mut values = HashMap::new();
809 values.insert("rules.base_price".to_string(), "100".to_string());
810
811 let updated_plan = plan.with_fact_values(values, &default_limits()).unwrap();
812 let fact_path = FactPath {
813 segments: vec![PathSegment {
814 fact: "rules".to_string(),
815 spec: "private".to_string(),
816 }],
817 fact: "base_price".to_string(),
818 };
819 let updated_value = updated_plan.get_fact_value(&fact_path).unwrap();
820 match &updated_value.value {
821 crate::planning::semantics::ValueKind::Number(n) => {
822 assert_eq!(n, &rust_decimal::Decimal::from(100))
823 }
824 other => panic!("Expected number literal, got {:?}", other),
825 }
826 }
827
828 fn test_source() -> crate::Source {
829 use crate::parsing::ast::Span;
830 crate::Source::new(
831 "<test>",
832 Span {
833 start: 0,
834 end: 0,
835 line: 1,
836 col: 0,
837 },
838 )
839 }
840
841 fn create_literal_expr(value: LiteralValue) -> Expression {
842 Expression::new(
843 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
844 test_source(),
845 )
846 }
847
848 fn create_fact_path_expr(path: FactPath) -> Expression {
849 Expression::new(
850 crate::planning::semantics::ExpressionKind::FactPath(path),
851 test_source(),
852 )
853 }
854
855 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
856 LiteralValue::number(n)
857 }
858
859 fn create_boolean_literal(b: bool) -> LiteralValue {
860 LiteralValue::from_bool(b)
861 }
862
863 fn create_text_literal(s: String) -> LiteralValue {
864 LiteralValue::text(s)
865 }
866
867 #[test]
868 fn with_values_should_enforce_number_maximum_constraint() {
869 let fact_path = FactPath::new(vec![], "x".to_string());
872
873 let max10 = crate::planning::semantics::LemmaType::primitive(
874 crate::planning::semantics::TypeSpecification::Number {
875 minimum: None,
876 maximum: Some(rust_decimal::Decimal::from_str("10").unwrap()),
877 decimals: None,
878 precision: None,
879 help: String::new(),
880 default: None,
881 },
882 );
883 let source = Source::new(
884 "<test>",
885 crate::parsing::ast::Span {
886 start: 0,
887 end: 0,
888 line: 1,
889 col: 0,
890 },
891 );
892 let mut facts = IndexMap::new();
893 facts.insert(
894 fact_path.clone(),
895 crate::planning::semantics::FactData::Value {
896 value: crate::planning::semantics::LiteralValue::number_with_type(
897 0.into(),
898 max10.clone(),
899 ),
900 source: source.clone(),
901 is_default: false,
902 },
903 );
904
905 let plan = ExecutionPlan {
906 spec_name: "test".to_string(),
907 facts,
908 rules: Vec::new(),
909 sources: HashMap::from([("<test>".to_string(), "".to_string())]),
910 meta: HashMap::new(),
911 named_types: BTreeMap::new(),
912 valid_from: None,
913 valid_to: None,
914 };
915
916 let mut values = HashMap::new();
917 values.insert("x".to_string(), "11".to_string());
918
919 assert!(
920 plan.with_fact_values(values, &default_limits()).is_err(),
921 "Providing x=11 should fail due to maximum 10"
922 );
923 }
924
925 #[test]
926 fn with_values_should_enforce_text_enum_options() {
927 let fact_path = FactPath::new(vec![], "tier".to_string());
929
930 let tier = crate::planning::semantics::LemmaType::primitive(
931 crate::planning::semantics::TypeSpecification::Text {
932 minimum: None,
933 maximum: None,
934 length: None,
935 options: vec!["silver".to_string(), "gold".to_string()],
936 help: String::new(),
937 default: None,
938 },
939 );
940 let source = Source::new(
941 "<test>",
942 crate::parsing::ast::Span {
943 start: 0,
944 end: 0,
945 line: 1,
946 col: 0,
947 },
948 );
949 let mut facts = IndexMap::new();
950 facts.insert(
951 fact_path.clone(),
952 crate::planning::semantics::FactData::Value {
953 value: crate::planning::semantics::LiteralValue::text_with_type(
954 "silver".to_string(),
955 tier.clone(),
956 ),
957 source,
958 is_default: false,
959 },
960 );
961
962 let plan = ExecutionPlan {
963 spec_name: "test".to_string(),
964 facts,
965 rules: Vec::new(),
966 sources: HashMap::from([("<test>".to_string(), "".to_string())]),
967 meta: HashMap::new(),
968 named_types: BTreeMap::new(),
969 valid_from: None,
970 valid_to: None,
971 };
972
973 let mut values = HashMap::new();
974 values.insert("tier".to_string(), "platinum".to_string());
975
976 assert!(
977 plan.with_fact_values(values, &default_limits()).is_err(),
978 "Invalid enum value should be rejected (tier='platinum')"
979 );
980 }
981
982 #[test]
983 fn with_values_should_enforce_scale_decimals() {
984 let fact_path = FactPath::new(vec![], "price".to_string());
987
988 let money = crate::planning::semantics::LemmaType::primitive(
989 crate::planning::semantics::TypeSpecification::Scale {
990 minimum: None,
991 maximum: None,
992 decimals: Some(2),
993 precision: None,
994 units: crate::planning::semantics::ScaleUnits::from(vec![
995 crate::planning::semantics::ScaleUnit {
996 name: "eur".to_string(),
997 value: rust_decimal::Decimal::from_str("1.0").unwrap(),
998 },
999 ]),
1000 help: String::new(),
1001 default: None,
1002 },
1003 );
1004 let source = Source::new(
1005 "<test>",
1006 crate::parsing::ast::Span {
1007 start: 0,
1008 end: 0,
1009 line: 1,
1010 col: 0,
1011 },
1012 );
1013 let mut facts = IndexMap::new();
1014 facts.insert(
1015 fact_path.clone(),
1016 crate::planning::semantics::FactData::Value {
1017 value: crate::planning::semantics::LiteralValue::scale_with_type(
1018 rust_decimal::Decimal::from_str("0").unwrap(),
1019 "eur".to_string(),
1020 money.clone(),
1021 ),
1022 source,
1023 is_default: false,
1024 },
1025 );
1026
1027 let plan = ExecutionPlan {
1028 spec_name: "test".to_string(),
1029 facts,
1030 rules: Vec::new(),
1031 sources: HashMap::from([("<test>".to_string(), "".to_string())]),
1032 meta: HashMap::new(),
1033 named_types: BTreeMap::new(),
1034 valid_from: None,
1035 valid_to: None,
1036 };
1037
1038 let mut values = HashMap::new();
1039 values.insert("price".to_string(), "1.234 eur".to_string());
1040
1041 assert!(
1042 plan.with_fact_values(values, &default_limits()).is_err(),
1043 "Scale decimals=2 should reject 1.234 eur"
1044 );
1045 }
1046
1047 #[test]
1048 fn test_serialize_deserialize_execution_plan() {
1049 let fact_path = FactPath {
1050 segments: vec![],
1051 fact: "age".to_string(),
1052 };
1053 let mut facts = IndexMap::new();
1054 facts.insert(
1055 fact_path.clone(),
1056 crate::planning::semantics::FactData::Value {
1057 value: create_number_literal(0.into()),
1058 source: test_source(),
1059 is_default: false,
1060 },
1061 );
1062 let plan = ExecutionPlan {
1063 spec_name: "test".to_string(),
1064 facts,
1065 rules: Vec::new(),
1066 sources: {
1067 let mut s = HashMap::new();
1068 s.insert("test.lemma".to_string(), "fact age: number".to_string());
1069 s
1070 },
1071 meta: HashMap::new(),
1072 named_types: BTreeMap::new(),
1073 valid_from: None,
1074 valid_to: None,
1075 };
1076
1077 let json = serde_json::to_string(&plan).expect("Should serialize");
1078 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1079
1080 assert_eq!(deserialized.spec_name, plan.spec_name);
1081 assert_eq!(deserialized.facts.len(), plan.facts.len());
1082 assert_eq!(deserialized.rules.len(), plan.rules.len());
1083 assert_eq!(deserialized.sources.len(), plan.sources.len());
1084 }
1085
1086 #[test]
1087 fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
1088 let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
1089 let imported_type = crate::planning::semantics::LemmaType::new(
1090 "salary".to_string(),
1091 TypeSpecification::scale(),
1092 crate::planning::semantics::TypeExtends::Custom {
1093 parent: "money".to_string(),
1094 family: "money".to_string(),
1095 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
1096 spec: Arc::clone(&dep_spec),
1097 resolved_plan_hash: "a1b2c3d4".to_string(),
1098 },
1099 },
1100 );
1101
1102 let mut named_types = BTreeMap::new();
1103 named_types.insert("salary".to_string(), imported_type);
1104
1105 let plan = ExecutionPlan {
1106 spec_name: "test".to_string(),
1107 facts: IndexMap::new(),
1108 rules: Vec::new(),
1109 sources: HashMap::new(),
1110 meta: HashMap::new(),
1111 named_types,
1112 valid_from: None,
1113 valid_to: None,
1114 };
1115
1116 let json = serde_json::to_string(&plan).expect("Should serialize");
1117 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1118
1119 let recovered = deserialized
1120 .named_types
1121 .get("salary")
1122 .expect("salary type should be present");
1123 match &recovered.extends {
1124 crate::planning::semantics::TypeExtends::Custom {
1125 defining_spec:
1126 crate::planning::semantics::TypeDefiningSpec::Import {
1127 spec,
1128 resolved_plan_hash,
1129 },
1130 ..
1131 } => {
1132 assert_eq!(spec.name, "examples");
1133 assert_eq!(resolved_plan_hash, "a1b2c3d4");
1134 }
1135 other => panic!(
1136 "Expected imported defining_spec after round-trip, got {:?}",
1137 other
1138 ),
1139 }
1140 }
1141
1142 #[test]
1143 fn test_serialize_deserialize_plan_with_rules() {
1144 use crate::planning::semantics::ExpressionKind;
1145
1146 let age_path = FactPath::new(vec![], "age".to_string());
1147 let mut facts = IndexMap::new();
1148 facts.insert(
1149 age_path.clone(),
1150 crate::planning::semantics::FactData::Value {
1151 value: create_number_literal(0.into()),
1152 source: test_source(),
1153 is_default: false,
1154 },
1155 );
1156 let mut plan = ExecutionPlan {
1157 spec_name: "test".to_string(),
1158 facts,
1159 rules: Vec::new(),
1160 sources: HashMap::new(),
1161 meta: HashMap::new(),
1162 named_types: BTreeMap::new(),
1163 valid_from: None,
1164 valid_to: None,
1165 };
1166
1167 let rule = ExecutableRule {
1168 path: RulePath::new(vec![], "can_drive".to_string()),
1169 name: "can_drive".to_string(),
1170 branches: vec![Branch {
1171 condition: Some(Expression::new(
1172 ExpressionKind::Comparison(
1173 Arc::new(create_fact_path_expr(age_path.clone())),
1174 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1175 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1176 ),
1177 test_source(),
1178 )),
1179 result: create_literal_expr(create_boolean_literal(true)),
1180 source: test_source(),
1181 }],
1182 needs_facts: BTreeSet::from([age_path]),
1183 source: test_source(),
1184 rule_type: primitive_boolean().clone(),
1185 };
1186
1187 plan.rules.push(rule);
1188
1189 let json = serde_json::to_string(&plan).expect("Should serialize");
1190 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1191
1192 assert_eq!(deserialized.spec_name, plan.spec_name);
1193 assert_eq!(deserialized.facts.len(), plan.facts.len());
1194 assert_eq!(deserialized.rules.len(), plan.rules.len());
1195 assert_eq!(deserialized.rules[0].name, "can_drive");
1196 assert_eq!(deserialized.rules[0].branches.len(), 1);
1197 assert_eq!(deserialized.rules[0].needs_facts.len(), 1);
1198 }
1199
1200 #[test]
1201 fn test_serialize_deserialize_plan_with_nested_fact_paths() {
1202 use crate::planning::semantics::PathSegment;
1203 let fact_path = FactPath {
1204 segments: vec![PathSegment {
1205 fact: "employee".to_string(),
1206 spec: "private".to_string(),
1207 }],
1208 fact: "salary".to_string(),
1209 };
1210
1211 let mut facts = IndexMap::new();
1212 facts.insert(
1213 fact_path.clone(),
1214 crate::planning::semantics::FactData::Value {
1215 value: create_number_literal(0.into()),
1216 source: test_source(),
1217 is_default: false,
1218 },
1219 );
1220 let plan = ExecutionPlan {
1221 spec_name: "test".to_string(),
1222 facts,
1223 rules: Vec::new(),
1224 sources: HashMap::new(),
1225 meta: HashMap::new(),
1226 named_types: BTreeMap::new(),
1227 valid_from: None,
1228 valid_to: None,
1229 };
1230
1231 let json = serde_json::to_string(&plan).expect("Should serialize");
1232 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1233
1234 assert_eq!(deserialized.facts.len(), 1);
1235 let (deserialized_path, _) = deserialized.facts.iter().next().unwrap();
1236 assert_eq!(deserialized_path.segments.len(), 1);
1237 assert_eq!(deserialized_path.segments[0].fact, "employee");
1238 assert_eq!(deserialized_path.fact, "salary");
1239 }
1240
1241 #[test]
1242 fn test_serialize_deserialize_plan_with_multiple_fact_types() {
1243 let name_path = FactPath::new(vec![], "name".to_string());
1244 let age_path = FactPath::new(vec![], "age".to_string());
1245 let active_path = FactPath::new(vec![], "active".to_string());
1246
1247 let mut facts = IndexMap::new();
1248 facts.insert(
1249 name_path.clone(),
1250 crate::planning::semantics::FactData::Value {
1251 value: create_text_literal("Alice".to_string()),
1252 source: test_source(),
1253 is_default: false,
1254 },
1255 );
1256 facts.insert(
1257 age_path.clone(),
1258 crate::planning::semantics::FactData::Value {
1259 value: create_number_literal(30.into()),
1260 source: test_source(),
1261 is_default: false,
1262 },
1263 );
1264 facts.insert(
1265 active_path.clone(),
1266 crate::planning::semantics::FactData::Value {
1267 value: create_boolean_literal(true),
1268 source: test_source(),
1269 is_default: false,
1270 },
1271 );
1272
1273 let plan = ExecutionPlan {
1274 spec_name: "test".to_string(),
1275 facts,
1276 rules: Vec::new(),
1277 sources: HashMap::new(),
1278 meta: HashMap::new(),
1279 named_types: BTreeMap::new(),
1280 valid_from: None,
1281 valid_to: None,
1282 };
1283
1284 let json = serde_json::to_string(&plan).expect("Should serialize");
1285 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1286
1287 assert_eq!(deserialized.facts.len(), 3);
1288
1289 assert_eq!(
1290 deserialized.get_fact_value(&name_path).unwrap().value,
1291 crate::planning::semantics::ValueKind::Text("Alice".to_string())
1292 );
1293 assert_eq!(
1294 deserialized.get_fact_value(&age_path).unwrap().value,
1295 crate::planning::semantics::ValueKind::Number(30.into())
1296 );
1297 assert_eq!(
1298 deserialized.get_fact_value(&active_path).unwrap().value,
1299 crate::planning::semantics::ValueKind::Boolean(true)
1300 );
1301 }
1302
1303 #[test]
1304 fn test_serialize_deserialize_plan_with_multiple_branches() {
1305 use crate::planning::semantics::ExpressionKind;
1306
1307 let points_path = FactPath::new(vec![], "points".to_string());
1308 let mut facts = IndexMap::new();
1309 facts.insert(
1310 points_path.clone(),
1311 crate::planning::semantics::FactData::Value {
1312 value: create_number_literal(0.into()),
1313 source: test_source(),
1314 is_default: false,
1315 },
1316 );
1317 let mut plan = ExecutionPlan {
1318 spec_name: "test".to_string(),
1319 facts,
1320 rules: Vec::new(),
1321 sources: HashMap::new(),
1322 meta: HashMap::new(),
1323 named_types: BTreeMap::new(),
1324 valid_from: None,
1325 valid_to: None,
1326 };
1327
1328 let rule = ExecutableRule {
1329 path: RulePath::new(vec![], "tier".to_string()),
1330 name: "tier".to_string(),
1331 branches: vec![
1332 Branch {
1333 condition: None,
1334 result: create_literal_expr(create_text_literal("bronze".to_string())),
1335 source: test_source(),
1336 },
1337 Branch {
1338 condition: Some(Expression::new(
1339 ExpressionKind::Comparison(
1340 Arc::new(create_fact_path_expr(points_path.clone())),
1341 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1342 Arc::new(create_literal_expr(create_number_literal(100.into()))),
1343 ),
1344 test_source(),
1345 )),
1346 result: create_literal_expr(create_text_literal("silver".to_string())),
1347 source: test_source(),
1348 },
1349 Branch {
1350 condition: Some(Expression::new(
1351 ExpressionKind::Comparison(
1352 Arc::new(create_fact_path_expr(points_path.clone())),
1353 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1354 Arc::new(create_literal_expr(create_number_literal(500.into()))),
1355 ),
1356 test_source(),
1357 )),
1358 result: create_literal_expr(create_text_literal("gold".to_string())),
1359 source: test_source(),
1360 },
1361 ],
1362 needs_facts: BTreeSet::from([points_path]),
1363 source: test_source(),
1364 rule_type: primitive_text().clone(),
1365 };
1366
1367 plan.rules.push(rule);
1368
1369 let json = serde_json::to_string(&plan).expect("Should serialize");
1370 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1371
1372 assert_eq!(deserialized.rules.len(), 1);
1373 assert_eq!(deserialized.rules[0].branches.len(), 3);
1374 assert!(deserialized.rules[0].branches[0].condition.is_none());
1375 assert!(deserialized.rules[0].branches[1].condition.is_some());
1376 assert!(deserialized.rules[0].branches[2].condition.is_some());
1377 }
1378
1379 #[test]
1380 fn test_serialize_deserialize_empty_plan() {
1381 let plan = ExecutionPlan {
1382 spec_name: "empty".to_string(),
1383 facts: IndexMap::new(),
1384 rules: Vec::new(),
1385 sources: HashMap::new(),
1386 meta: HashMap::new(),
1387 named_types: BTreeMap::new(),
1388 valid_from: None,
1389 valid_to: None,
1390 };
1391
1392 let json = serde_json::to_string(&plan).expect("Should serialize");
1393 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1394
1395 assert_eq!(deserialized.spec_name, "empty");
1396 assert_eq!(deserialized.facts.len(), 0);
1397 assert_eq!(deserialized.rules.len(), 0);
1398 assert_eq!(deserialized.sources.len(), 0);
1399 }
1400
1401 #[test]
1402 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
1403 use crate::planning::semantics::ExpressionKind;
1404
1405 let x_path = FactPath::new(vec![], "x".to_string());
1406 let mut facts = IndexMap::new();
1407 facts.insert(
1408 x_path.clone(),
1409 crate::planning::semantics::FactData::Value {
1410 value: create_number_literal(0.into()),
1411 source: test_source(),
1412 is_default: false,
1413 },
1414 );
1415 let mut plan = ExecutionPlan {
1416 spec_name: "test".to_string(),
1417 facts,
1418 rules: Vec::new(),
1419 sources: HashMap::new(),
1420 meta: HashMap::new(),
1421 named_types: BTreeMap::new(),
1422 valid_from: None,
1423 valid_to: None,
1424 };
1425
1426 let rule = ExecutableRule {
1427 path: RulePath::new(vec![], "doubled".to_string()),
1428 name: "doubled".to_string(),
1429 branches: vec![Branch {
1430 condition: None,
1431 result: Expression::new(
1432 ExpressionKind::Arithmetic(
1433 Arc::new(create_fact_path_expr(x_path.clone())),
1434 crate::parsing::ast::ArithmeticComputation::Multiply,
1435 Arc::new(create_literal_expr(create_number_literal(2.into()))),
1436 ),
1437 test_source(),
1438 ),
1439 source: test_source(),
1440 }],
1441 needs_facts: BTreeSet::from([x_path]),
1442 source: test_source(),
1443 rule_type: crate::planning::semantics::primitive_number().clone(),
1444 };
1445
1446 plan.rules.push(rule);
1447
1448 let json = serde_json::to_string(&plan).expect("Should serialize");
1449 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1450
1451 assert_eq!(deserialized.rules.len(), 1);
1452 match &deserialized.rules[0].branches[0].result.kind {
1453 ExpressionKind::Arithmetic(left, op, right) => {
1454 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
1455 match &left.kind {
1456 ExpressionKind::FactPath(_) => {}
1457 _ => panic!("Expected FactPath in left operand"),
1458 }
1459 match &right.kind {
1460 ExpressionKind::Literal(_) => {}
1461 _ => panic!("Expected Literal in right operand"),
1462 }
1463 }
1464 _ => panic!("Expected Arithmetic expression"),
1465 }
1466 }
1467
1468 #[test]
1469 fn test_serialize_deserialize_round_trip_equality() {
1470 use crate::planning::semantics::ExpressionKind;
1471
1472 let age_path = FactPath::new(vec![], "age".to_string());
1473 let mut facts = IndexMap::new();
1474 facts.insert(
1475 age_path.clone(),
1476 crate::planning::semantics::FactData::Value {
1477 value: create_number_literal(0.into()),
1478 source: test_source(),
1479 is_default: false,
1480 },
1481 );
1482 let mut plan = ExecutionPlan {
1483 spec_name: "test".to_string(),
1484 facts,
1485 rules: Vec::new(),
1486 sources: {
1487 let mut s = HashMap::new();
1488 s.insert("test.lemma".to_string(), "fact age: number".to_string());
1489 s
1490 },
1491 meta: HashMap::new(),
1492 named_types: BTreeMap::new(),
1493 valid_from: None,
1494 valid_to: None,
1495 };
1496
1497 let rule = ExecutableRule {
1498 path: RulePath::new(vec![], "is_adult".to_string()),
1499 name: "is_adult".to_string(),
1500 branches: vec![Branch {
1501 condition: Some(Expression::new(
1502 ExpressionKind::Comparison(
1503 Arc::new(create_fact_path_expr(age_path.clone())),
1504 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1505 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1506 ),
1507 test_source(),
1508 )),
1509 result: create_literal_expr(create_boolean_literal(true)),
1510 source: test_source(),
1511 }],
1512 needs_facts: BTreeSet::from([age_path]),
1513 source: test_source(),
1514 rule_type: primitive_boolean().clone(),
1515 };
1516
1517 plan.rules.push(rule);
1518
1519 let json = serde_json::to_string(&plan).expect("Should serialize");
1520 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1521
1522 let json2 = serde_json::to_string(&deserialized).expect("Should serialize again");
1523 let deserialized2: ExecutionPlan =
1524 serde_json::from_str(&json2).expect("Should deserialize again");
1525
1526 assert_eq!(deserialized2.spec_name, plan.spec_name);
1527 assert_eq!(deserialized2.facts.len(), plan.facts.len());
1528 assert_eq!(deserialized2.rules.len(), plan.rules.len());
1529 assert_eq!(deserialized2.sources.len(), plan.sources.len());
1530 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
1531 assert_eq!(
1532 deserialized2.rules[0].branches.len(),
1533 plan.rules[0].branches.len()
1534 );
1535 }
1536}