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