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<(usize, 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 value = data.explicit_value().cloned();
367 (
368 data.source().span.start,
369 path.input_key(),
370 (lemma_type, value),
371 )
372 })
373 .collect();
374 fact_entries.sort_by_key(|(pos, _, _)| *pos);
375 let fact_entries: Vec<(String, (LemmaType, Option<LiteralValue>))> = fact_entries
376 .into_iter()
377 .map(|(_, name, data)| (name, data))
378 .collect();
379
380 let rule_entries: Vec<(String, LemmaType)> = self
381 .rules
382 .iter()
383 .filter(|r| r.path.segments.is_empty())
384 .map(|r| (r.name.clone(), r.rule_type.clone()))
385 .collect();
386
387 SpecSchema {
388 spec: self.spec_name.clone(),
389 facts: fact_entries.into_iter().collect(),
390 rules: rule_entries.into_iter().collect(),
391 meta: self.meta.clone(),
392 }
393 }
394
395 pub fn schema_for_rules(&self, rule_names: &[String]) -> Result<SpecSchema, Error> {
404 let mut needed_facts = HashSet::new();
405 let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
406
407 for rule_name in rule_names {
408 let rule = self.get_rule(rule_name).ok_or_else(|| {
409 Error::request(
410 format!(
411 "Rule '{}' not found in spec '{}'",
412 rule_name, self.spec_name
413 ),
414 None::<String>,
415 )
416 })?;
417 needed_facts.extend(rule.needs_facts.iter().cloned());
418 rule_entries.push((rule.name.clone(), rule.rule_type.clone()));
419 }
420
421 let mut fact_entries: Vec<(usize, String, (LemmaType, Option<LiteralValue>))> = self
422 .facts
423 .iter()
424 .filter(|(path, _)| needed_facts.contains(path))
425 .filter(|(_, data)| data.schema_type().is_some())
426 .map(|(path, data)| {
427 let lemma_type = data.schema_type().unwrap().clone();
428 let value = data.explicit_value().cloned();
429 (
430 data.source().span.start,
431 path.input_key(),
432 (lemma_type, value),
433 )
434 })
435 .collect();
436 fact_entries.sort_by_key(|(pos, _, _)| *pos);
437 let fact_entries: Vec<(String, (LemmaType, Option<LiteralValue>))> = fact_entries
438 .into_iter()
439 .map(|(_, name, data)| (name, data))
440 .collect();
441
442 Ok(SpecSchema {
443 spec: self.spec_name.clone(),
444 facts: fact_entries.into_iter().collect(),
445 rules: rule_entries.into_iter().collect(),
446 meta: self.meta.clone(),
447 })
448 }
449
450 pub fn get_fact_path_by_str(&self, name: &str) -> Option<&FactPath> {
452 self.facts.keys().find(|path| path.input_key() == name)
453 }
454
455 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
457 self.rules
458 .iter()
459 .find(|r| r.name == name && r.path.segments.is_empty())
460 }
461
462 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
464 self.rules.iter().find(|r| &r.path == rule_path)
465 }
466
467 pub fn get_fact_value(&self, path: &FactPath) -> Option<&LiteralValue> {
469 self.facts.get(path).and_then(|d| d.value())
470 }
471
472 pub fn with_fact_values(
476 mut self,
477 values: HashMap<String, String>,
478 limits: &ResourceLimits,
479 ) -> Result<Self, Error> {
480 for (name, raw_value) in values {
481 let fact_path = self.get_fact_path_by_str(&name).ok_or_else(|| {
482 let available: Vec<String> = self.facts.keys().map(|p| p.input_key()).collect();
483 Error::request(
484 format!(
485 "Fact '{}' not found. Available facts: {}",
486 name,
487 available.join(", ")
488 ),
489 None::<String>,
490 )
491 })?;
492 let fact_path = fact_path.clone();
493
494 let fact_data = self
495 .facts
496 .get(&fact_path)
497 .expect("BUG: fact_path was just resolved from self.facts, must exist");
498
499 let fact_source = fact_data.source().clone();
500 let expected_type = fact_data.schema_type().cloned().ok_or_else(|| {
501 Error::request(
502 format!(
503 "Fact '{}' is a spec reference; cannot provide a value.",
504 name
505 ),
506 None::<String>,
507 )
508 })?;
509
510 let parsed_value = crate::planning::semantics::parse_value_from_string(
512 &raw_value,
513 &expected_type.specifications,
514 &fact_source,
515 )
516 .map_err(|e| {
517 Error::validation(
518 format!(
519 "Failed to parse fact '{}' as {}: {}",
520 name,
521 expected_type.name(),
522 e
523 ),
524 Some(fact_source.clone()),
525 None::<String>,
526 )
527 })?;
528 let semantic_value = semantics::value_to_semantic(&parsed_value).map_err(|e| {
529 Error::validation(
530 format!("Failed to convert fact '{}' value: {}", name, e),
531 Some(fact_source.clone()),
532 None::<String>,
533 )
534 })?;
535 let literal_value = LiteralValue {
536 value: semantic_value,
537 lemma_type: expected_type.clone(),
538 };
539
540 let size = literal_value.byte_size();
542 if size > limits.max_fact_value_bytes {
543 return Err(Error::resource_limit_exceeded(
544 "max_fact_value_bytes",
545 limits.max_fact_value_bytes.to_string(),
546 size.to_string(),
547 format!(
548 "Reduce the size of fact values to {} bytes or less",
549 limits.max_fact_value_bytes
550 ),
551 Some(fact_source.clone()),
552 ));
553 }
554
555 validate_value_against_type(&expected_type, &literal_value).map_err(|msg| {
557 Error::validation(
558 format!(
559 "Invalid value for fact {} (expected {}): {}",
560 name,
561 expected_type.name(),
562 msg
563 ),
564 Some(fact_source.clone()),
565 None::<String>,
566 )
567 })?;
568
569 self.facts.insert(
570 fact_path,
571 FactData::Value {
572 value: literal_value,
573 source: fact_source,
574 is_default: false,
575 },
576 );
577 }
578
579 Ok(self)
580 }
581}
582
583fn validate_value_against_type(
584 expected_type: &LemmaType,
585 value: &LiteralValue,
586) -> Result<(), String> {
587 use crate::planning::semantics::TypeSpecification;
588
589 let effective_decimals = |n: rust_decimal::Decimal| n.scale();
590
591 match (&expected_type.specifications, &value.value) {
592 (
593 TypeSpecification::Number {
594 minimum,
595 maximum,
596 decimals,
597 ..
598 },
599 ValueKind::Number(n),
600 ) => {
601 if let Some(min) = minimum {
602 if n < min {
603 return Err(format!("{} is below minimum {}", n, min));
604 }
605 }
606 if let Some(max) = maximum {
607 if n > max {
608 return Err(format!("{} is above maximum {}", n, max));
609 }
610 }
611 if let Some(d) = decimals {
612 if effective_decimals(*n) > u32::from(*d) {
613 return Err(format!("{} has more than {} decimals", n, d));
614 }
615 }
616 Ok(())
617 }
618 (
619 TypeSpecification::Scale {
620 minimum,
621 maximum,
622 decimals,
623 ..
624 },
625 ValueKind::Scale(n, _unit),
626 ) => {
627 if let Some(min) = minimum {
628 if n < min {
629 return Err(format!("{} is below minimum {}", n, min));
630 }
631 }
632 if let Some(max) = maximum {
633 if n > max {
634 return Err(format!("{} is above maximum {}", n, max));
635 }
636 }
637 if let Some(d) = decimals {
638 if effective_decimals(*n) > u32::from(*d) {
639 return Err(format!("{} has more than {} decimals", n, d));
640 }
641 }
642 Ok(())
643 }
644 (TypeSpecification::Text { options, .. }, ValueKind::Text(s)) => {
645 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
646 return Err(format!(
647 "'{}' is not in allowed options: {}",
648 s,
649 options.join(", ")
650 ));
651 }
652 Ok(())
653 }
654 _ => Ok(()),
656 }
657}
658
659pub(crate) fn validate_literal_facts_against_types(plan: &ExecutionPlan) -> Vec<Error> {
660 let mut errors = Vec::new();
661
662 for (fact_path, fact_data) in &plan.facts {
663 let (expected_type, lit) = match fact_data {
664 FactData::Value { value, .. } => (&value.lemma_type, value),
665 FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => continue,
666 };
667
668 if let Err(msg) = validate_value_against_type(expected_type, lit) {
669 let source = fact_data.source().clone();
670 errors.push(Error::validation(
671 format!(
672 "Invalid value for fact {} (expected {}): {}",
673 fact_path,
674 expected_type.name(),
675 msg
676 ),
677 Some(source),
678 None::<String>,
679 ));
680 }
681 }
682
683 errors
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use crate::parsing::ast::DateTimeValue;
690 use crate::planning::semantics::{
691 primitive_boolean, primitive_text, FactPath, LiteralValue, PathSegment, RulePath,
692 };
693 use crate::Engine;
694 use serde_json;
695 use std::str::FromStr;
696 use std::sync::Arc;
697
698 fn default_limits() -> ResourceLimits {
699 ResourceLimits::default()
700 }
701
702 fn add_lemma_code_blocking(
703 engine: &mut Engine,
704 code: &str,
705 source: &str,
706 ) -> Result<(), Vec<crate::Error>> {
707 let files: std::collections::HashMap<String, String> =
708 std::iter::once((source.to_string(), code.to_string())).collect();
709 engine.add_lemma_files(files)
710 }
711
712 #[test]
713 fn test_with_raw_values() {
714 let mut engine = Engine::new();
715 add_lemma_code_blocking(
716 &mut engine,
717 r#"
718 spec test
719 fact age: [number -> default 25]
720 "#,
721 "test.lemma",
722 )
723 .unwrap();
724
725 let now = DateTimeValue::now();
726 let plan = engine
727 .get_execution_plan("test", None, &now)
728 .unwrap()
729 .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
760 .get_execution_plan("test", None, &now)
761 .unwrap()
762 .clone();
763
764 let mut values = HashMap::new();
765 values.insert("age".to_string(), "thirty".to_string());
766
767 assert!(plan.with_fact_values(values, &default_limits()).is_err());
768 }
769
770 #[test]
771 fn test_with_raw_values_unknown_fact() {
772 let mut engine = Engine::new();
773 add_lemma_code_blocking(
774 &mut engine,
775 r#"
776 spec test
777 fact known: [number]
778 "#,
779 "test.lemma",
780 )
781 .unwrap();
782
783 let now = DateTimeValue::now();
784 let plan = engine
785 .get_execution_plan("test", None, &now)
786 .unwrap()
787 .clone();
788
789 let mut values = HashMap::new();
790 values.insert("unknown".to_string(), "30".to_string());
791
792 assert!(plan.with_fact_values(values, &default_limits()).is_err());
793 }
794
795 #[test]
796 fn test_with_raw_values_nested() {
797 let mut engine = Engine::new();
798 add_lemma_code_blocking(
799 &mut engine,
800 r#"
801 spec private
802 fact base_price: [number]
803
804 spec test
805 fact rules: spec private
806 "#,
807 "test.lemma",
808 )
809 .unwrap();
810
811 let now = DateTimeValue::now();
812 let plan = engine
813 .get_execution_plan("test", None, &now)
814 .unwrap()
815 .clone();
816
817 let mut values = HashMap::new();
818 values.insert("rules.base_price".to_string(), "100".to_string());
819
820 let updated_plan = plan.with_fact_values(values, &default_limits()).unwrap();
821 let fact_path = FactPath {
822 segments: vec![PathSegment {
823 fact: "rules".to_string(),
824 spec: "private".to_string(),
825 }],
826 fact: "base_price".to_string(),
827 };
828 let updated_value = updated_plan.get_fact_value(&fact_path).unwrap();
829 match &updated_value.value {
830 crate::planning::semantics::ValueKind::Number(n) => {
831 assert_eq!(n, &rust_decimal::Decimal::from(100))
832 }
833 other => panic!("Expected number literal, got {:?}", other),
834 }
835 }
836
837 fn test_source() -> crate::Source {
838 use crate::parsing::ast::Span;
839 crate::Source {
840 attribute: "<test>".to_string(),
841 span: Span {
842 start: 0,
843 end: 0,
844 line: 1,
845 col: 0,
846 },
847 source_text: Arc::from("spec test\nfact x: 1\nrule result: x"),
848 }
849 }
850
851 fn create_literal_expr(value: LiteralValue) -> Expression {
852 Expression::new(
853 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
854 test_source(),
855 )
856 }
857
858 fn create_fact_path_expr(path: FactPath) -> Expression {
859 Expression::new(
860 crate::planning::semantics::ExpressionKind::FactPath(path),
861 test_source(),
862 )
863 }
864
865 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
866 LiteralValue::number(n)
867 }
868
869 fn create_boolean_literal(b: bool) -> LiteralValue {
870 LiteralValue::from_bool(b)
871 }
872
873 fn create_text_literal(s: String) -> LiteralValue {
874 LiteralValue::text(s)
875 }
876
877 #[test]
878 fn with_values_should_enforce_number_maximum_constraint() {
879 let fact_path = FactPath::new(vec![], "x".to_string());
882
883 let max10 = crate::planning::semantics::LemmaType::primitive(
884 crate::planning::semantics::TypeSpecification::Number {
885 minimum: None,
886 maximum: Some(rust_decimal::Decimal::from_str("10").unwrap()),
887 decimals: None,
888 precision: None,
889 help: String::new(),
890 default: None,
891 },
892 );
893 let source = Source::new(
894 "<test>",
895 crate::parsing::ast::Span {
896 start: 0,
897 end: 0,
898 line: 1,
899 col: 0,
900 },
901 Arc::from("spec test\nfact x: 1\nrule result: x"),
902 );
903 let mut facts = IndexMap::new();
904 facts.insert(
905 fact_path.clone(),
906 crate::planning::semantics::FactData::Value {
907 value: crate::planning::semantics::LiteralValue::number_with_type(
908 0.into(),
909 max10.clone(),
910 ),
911 source: source.clone(),
912 is_default: false,
913 },
914 );
915
916 let plan = ExecutionPlan {
917 spec_name: "test".to_string(),
918 facts,
919 rules: Vec::new(),
920 sources: HashMap::from([("<test>".to_string(), "".to_string())]),
921 meta: HashMap::new(),
922 valid_from: None,
923 valid_to: None,
924 };
925
926 let mut values = HashMap::new();
927 values.insert("x".to_string(), "11".to_string());
928
929 assert!(
930 plan.with_fact_values(values, &default_limits()).is_err(),
931 "Providing x=11 should fail due to maximum 10"
932 );
933 }
934
935 #[test]
936 fn with_values_should_enforce_text_enum_options() {
937 let fact_path = FactPath::new(vec![], "tier".to_string());
939
940 let tier = crate::planning::semantics::LemmaType::primitive(
941 crate::planning::semantics::TypeSpecification::Text {
942 minimum: None,
943 maximum: None,
944 length: None,
945 options: vec!["silver".to_string(), "gold".to_string()],
946 help: String::new(),
947 default: None,
948 },
949 );
950 let source = Source::new(
951 "<test>",
952 crate::parsing::ast::Span {
953 start: 0,
954 end: 0,
955 line: 1,
956 col: 0,
957 },
958 Arc::from("spec test\nfact x: 1\nrule result: x"),
959 );
960 let mut facts = IndexMap::new();
961 facts.insert(
962 fact_path.clone(),
963 crate::planning::semantics::FactData::Value {
964 value: crate::planning::semantics::LiteralValue::text_with_type(
965 "silver".to_string(),
966 tier.clone(),
967 ),
968 source,
969 is_default: false,
970 },
971 );
972
973 let plan = ExecutionPlan {
974 spec_name: "test".to_string(),
975 facts,
976 rules: Vec::new(),
977 sources: HashMap::from([("<test>".to_string(), "".to_string())]),
978 meta: HashMap::new(),
979 valid_from: None,
980 valid_to: None,
981 };
982
983 let mut values = HashMap::new();
984 values.insert("tier".to_string(), "platinum".to_string());
985
986 assert!(
987 plan.with_fact_values(values, &default_limits()).is_err(),
988 "Invalid enum value should be rejected (tier='platinum')"
989 );
990 }
991
992 #[test]
993 fn with_values_should_enforce_scale_decimals() {
994 let fact_path = FactPath::new(vec![], "price".to_string());
997
998 let money = crate::planning::semantics::LemmaType::primitive(
999 crate::planning::semantics::TypeSpecification::Scale {
1000 minimum: None,
1001 maximum: None,
1002 decimals: Some(2),
1003 precision: None,
1004 units: crate::planning::semantics::ScaleUnits::from(vec![
1005 crate::planning::semantics::ScaleUnit {
1006 name: "eur".to_string(),
1007 value: rust_decimal::Decimal::from_str("1.0").unwrap(),
1008 },
1009 ]),
1010 help: String::new(),
1011 default: None,
1012 },
1013 );
1014 let source = Source::new(
1015 "<test>",
1016 crate::parsing::ast::Span {
1017 start: 0,
1018 end: 0,
1019 line: 1,
1020 col: 0,
1021 },
1022 Arc::from("spec test\nfact x: 1\nrule result: x"),
1023 );
1024 let mut facts = IndexMap::new();
1025 facts.insert(
1026 fact_path.clone(),
1027 crate::planning::semantics::FactData::Value {
1028 value: crate::planning::semantics::LiteralValue::scale_with_type(
1029 rust_decimal::Decimal::from_str("0").unwrap(),
1030 "eur".to_string(),
1031 money.clone(),
1032 ),
1033 source,
1034 is_default: false,
1035 },
1036 );
1037
1038 let plan = ExecutionPlan {
1039 spec_name: "test".to_string(),
1040 facts,
1041 rules: Vec::new(),
1042 sources: HashMap::from([("<test>".to_string(), "".to_string())]),
1043 meta: HashMap::new(),
1044 valid_from: None,
1045 valid_to: None,
1046 };
1047
1048 let mut values = HashMap::new();
1049 values.insert("price".to_string(), "1.234 eur".to_string());
1050
1051 assert!(
1052 plan.with_fact_values(values, &default_limits()).is_err(),
1053 "Scale decimals=2 should reject 1.234 eur"
1054 );
1055 }
1056
1057 #[test]
1058 fn test_serialize_deserialize_execution_plan() {
1059 let fact_path = FactPath {
1060 segments: vec![],
1061 fact: "age".to_string(),
1062 };
1063 let mut facts = IndexMap::new();
1064 facts.insert(
1065 fact_path.clone(),
1066 crate::planning::semantics::FactData::Value {
1067 value: create_number_literal(0.into()),
1068 source: test_source(),
1069 is_default: false,
1070 },
1071 );
1072 let plan = ExecutionPlan {
1073 spec_name: "test".to_string(),
1074 facts,
1075 rules: Vec::new(),
1076 sources: {
1077 let mut s = HashMap::new();
1078 s.insert("test.lemma".to_string(), "fact age: number".to_string());
1079 s
1080 },
1081 meta: HashMap::new(),
1082 valid_from: None,
1083 valid_to: None,
1084 };
1085
1086 let json = serde_json::to_string(&plan).expect("Should serialize");
1087 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1088
1089 assert_eq!(deserialized.spec_name, plan.spec_name);
1090 assert_eq!(deserialized.facts.len(), plan.facts.len());
1091 assert_eq!(deserialized.rules.len(), plan.rules.len());
1092 assert_eq!(deserialized.sources.len(), plan.sources.len());
1093 }
1094
1095 #[test]
1096 fn test_serialize_deserialize_plan_with_rules() {
1097 use crate::planning::semantics::ExpressionKind;
1098
1099 let age_path = FactPath::new(vec![], "age".to_string());
1100 let mut facts = IndexMap::new();
1101 facts.insert(
1102 age_path.clone(),
1103 crate::planning::semantics::FactData::Value {
1104 value: create_number_literal(0.into()),
1105 source: test_source(),
1106 is_default: false,
1107 },
1108 );
1109 let mut plan = ExecutionPlan {
1110 spec_name: "test".to_string(),
1111 facts,
1112 rules: Vec::new(),
1113 sources: HashMap::new(),
1114 meta: HashMap::new(),
1115 valid_from: None,
1116 valid_to: None,
1117 };
1118
1119 let rule = ExecutableRule {
1120 path: RulePath::new(vec![], "can_drive".to_string()),
1121 name: "can_drive".to_string(),
1122 branches: vec![Branch {
1123 condition: Some(Expression::new(
1124 ExpressionKind::Comparison(
1125 Arc::new(create_fact_path_expr(age_path.clone())),
1126 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1127 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1128 ),
1129 test_source(),
1130 )),
1131 result: create_literal_expr(create_boolean_literal(true)),
1132 source: test_source(),
1133 }],
1134 needs_facts: {
1135 let mut set = HashSet::new();
1136 set.insert(age_path);
1137 set
1138 },
1139 source: test_source(),
1140 rule_type: primitive_boolean().clone(),
1141 };
1142
1143 plan.rules.push(rule);
1144
1145 let json = serde_json::to_string(&plan).expect("Should serialize");
1146 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1147
1148 assert_eq!(deserialized.spec_name, plan.spec_name);
1149 assert_eq!(deserialized.facts.len(), plan.facts.len());
1150 assert_eq!(deserialized.rules.len(), plan.rules.len());
1151 assert_eq!(deserialized.rules[0].name, "can_drive");
1152 assert_eq!(deserialized.rules[0].branches.len(), 1);
1153 assert_eq!(deserialized.rules[0].needs_facts.len(), 1);
1154 }
1155
1156 #[test]
1157 fn test_serialize_deserialize_plan_with_nested_fact_paths() {
1158 use crate::planning::semantics::PathSegment;
1159 let fact_path = FactPath {
1160 segments: vec![PathSegment {
1161 fact: "employee".to_string(),
1162 spec: "private".to_string(),
1163 }],
1164 fact: "salary".to_string(),
1165 };
1166
1167 let mut facts = IndexMap::new();
1168 facts.insert(
1169 fact_path.clone(),
1170 crate::planning::semantics::FactData::Value {
1171 value: create_number_literal(0.into()),
1172 source: test_source(),
1173 is_default: false,
1174 },
1175 );
1176 let plan = ExecutionPlan {
1177 spec_name: "test".to_string(),
1178 facts,
1179 rules: Vec::new(),
1180 sources: HashMap::new(),
1181 meta: HashMap::new(),
1182 valid_from: None,
1183 valid_to: None,
1184 };
1185
1186 let json = serde_json::to_string(&plan).expect("Should serialize");
1187 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1188
1189 assert_eq!(deserialized.facts.len(), 1);
1190 let (deserialized_path, _) = deserialized.facts.iter().next().unwrap();
1191 assert_eq!(deserialized_path.segments.len(), 1);
1192 assert_eq!(deserialized_path.segments[0].fact, "employee");
1193 assert_eq!(deserialized_path.fact, "salary");
1194 }
1195
1196 #[test]
1197 fn test_serialize_deserialize_plan_with_multiple_fact_types() {
1198 let name_path = FactPath::new(vec![], "name".to_string());
1199 let age_path = FactPath::new(vec![], "age".to_string());
1200 let active_path = FactPath::new(vec![], "active".to_string());
1201
1202 let mut facts = IndexMap::new();
1203 facts.insert(
1204 name_path.clone(),
1205 crate::planning::semantics::FactData::Value {
1206 value: create_text_literal("Alice".to_string()),
1207 source: test_source(),
1208 is_default: false,
1209 },
1210 );
1211 facts.insert(
1212 age_path.clone(),
1213 crate::planning::semantics::FactData::Value {
1214 value: create_number_literal(30.into()),
1215 source: test_source(),
1216 is_default: false,
1217 },
1218 );
1219 facts.insert(
1220 active_path.clone(),
1221 crate::planning::semantics::FactData::Value {
1222 value: create_boolean_literal(true),
1223 source: test_source(),
1224 is_default: false,
1225 },
1226 );
1227
1228 let plan = ExecutionPlan {
1229 spec_name: "test".to_string(),
1230 facts,
1231 rules: Vec::new(),
1232 sources: HashMap::new(),
1233 meta: HashMap::new(),
1234 valid_from: None,
1235 valid_to: None,
1236 };
1237
1238 let json = serde_json::to_string(&plan).expect("Should serialize");
1239 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1240
1241 assert_eq!(deserialized.facts.len(), 3);
1242
1243 assert_eq!(
1244 deserialized.get_fact_value(&name_path).unwrap().value,
1245 crate::planning::semantics::ValueKind::Text("Alice".to_string())
1246 );
1247 assert_eq!(
1248 deserialized.get_fact_value(&age_path).unwrap().value,
1249 crate::planning::semantics::ValueKind::Number(30.into())
1250 );
1251 assert_eq!(
1252 deserialized.get_fact_value(&active_path).unwrap().value,
1253 crate::planning::semantics::ValueKind::Boolean(true)
1254 );
1255 }
1256
1257 #[test]
1258 fn test_serialize_deserialize_plan_with_multiple_branches() {
1259 use crate::planning::semantics::ExpressionKind;
1260
1261 let points_path = FactPath::new(vec![], "points".to_string());
1262 let mut facts = IndexMap::new();
1263 facts.insert(
1264 points_path.clone(),
1265 crate::planning::semantics::FactData::Value {
1266 value: create_number_literal(0.into()),
1267 source: test_source(),
1268 is_default: false,
1269 },
1270 );
1271 let mut plan = ExecutionPlan {
1272 spec_name: "test".to_string(),
1273 facts,
1274 rules: Vec::new(),
1275 sources: HashMap::new(),
1276 meta: HashMap::new(),
1277 valid_from: None,
1278 valid_to: None,
1279 };
1280
1281 let rule = ExecutableRule {
1282 path: RulePath::new(vec![], "tier".to_string()),
1283 name: "tier".to_string(),
1284 branches: vec![
1285 Branch {
1286 condition: None,
1287 result: create_literal_expr(create_text_literal("bronze".to_string())),
1288 source: test_source(),
1289 },
1290 Branch {
1291 condition: Some(Expression::new(
1292 ExpressionKind::Comparison(
1293 Arc::new(create_fact_path_expr(points_path.clone())),
1294 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1295 Arc::new(create_literal_expr(create_number_literal(100.into()))),
1296 ),
1297 test_source(),
1298 )),
1299 result: create_literal_expr(create_text_literal("silver".to_string())),
1300 source: test_source(),
1301 },
1302 Branch {
1303 condition: Some(Expression::new(
1304 ExpressionKind::Comparison(
1305 Arc::new(create_fact_path_expr(points_path.clone())),
1306 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1307 Arc::new(create_literal_expr(create_number_literal(500.into()))),
1308 ),
1309 test_source(),
1310 )),
1311 result: create_literal_expr(create_text_literal("gold".to_string())),
1312 source: test_source(),
1313 },
1314 ],
1315 needs_facts: {
1316 let mut set = HashSet::new();
1317 set.insert(points_path);
1318 set
1319 },
1320 source: test_source(),
1321 rule_type: primitive_text().clone(),
1322 };
1323
1324 plan.rules.push(rule);
1325
1326 let json = serde_json::to_string(&plan).expect("Should serialize");
1327 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1328
1329 assert_eq!(deserialized.rules.len(), 1);
1330 assert_eq!(deserialized.rules[0].branches.len(), 3);
1331 assert!(deserialized.rules[0].branches[0].condition.is_none());
1332 assert!(deserialized.rules[0].branches[1].condition.is_some());
1333 assert!(deserialized.rules[0].branches[2].condition.is_some());
1334 }
1335
1336 #[test]
1337 fn test_serialize_deserialize_empty_plan() {
1338 let plan = ExecutionPlan {
1339 spec_name: "empty".to_string(),
1340 facts: IndexMap::new(),
1341 rules: Vec::new(),
1342 sources: HashMap::new(),
1343 meta: HashMap::new(),
1344 valid_from: None,
1345 valid_to: None,
1346 };
1347
1348 let json = serde_json::to_string(&plan).expect("Should serialize");
1349 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1350
1351 assert_eq!(deserialized.spec_name, "empty");
1352 assert_eq!(deserialized.facts.len(), 0);
1353 assert_eq!(deserialized.rules.len(), 0);
1354 assert_eq!(deserialized.sources.len(), 0);
1355 }
1356
1357 #[test]
1358 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
1359 use crate::planning::semantics::ExpressionKind;
1360
1361 let x_path = FactPath::new(vec![], "x".to_string());
1362 let mut facts = IndexMap::new();
1363 facts.insert(
1364 x_path.clone(),
1365 crate::planning::semantics::FactData::Value {
1366 value: create_number_literal(0.into()),
1367 source: test_source(),
1368 is_default: false,
1369 },
1370 );
1371 let mut plan = ExecutionPlan {
1372 spec_name: "test".to_string(),
1373 facts,
1374 rules: Vec::new(),
1375 sources: HashMap::new(),
1376 meta: HashMap::new(),
1377 valid_from: None,
1378 valid_to: None,
1379 };
1380
1381 let rule = ExecutableRule {
1382 path: RulePath::new(vec![], "doubled".to_string()),
1383 name: "doubled".to_string(),
1384 branches: vec![Branch {
1385 condition: None,
1386 result: Expression::new(
1387 ExpressionKind::Arithmetic(
1388 Arc::new(create_fact_path_expr(x_path.clone())),
1389 crate::parsing::ast::ArithmeticComputation::Multiply,
1390 Arc::new(create_literal_expr(create_number_literal(2.into()))),
1391 ),
1392 test_source(),
1393 ),
1394 source: test_source(),
1395 }],
1396 needs_facts: {
1397 let mut set = HashSet::new();
1398 set.insert(x_path);
1399 set
1400 },
1401 source: test_source(),
1402 rule_type: crate::planning::semantics::primitive_number().clone(),
1403 };
1404
1405 plan.rules.push(rule);
1406
1407 let json = serde_json::to_string(&plan).expect("Should serialize");
1408 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1409
1410 assert_eq!(deserialized.rules.len(), 1);
1411 match &deserialized.rules[0].branches[0].result.kind {
1412 ExpressionKind::Arithmetic(left, op, right) => {
1413 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
1414 match &left.kind {
1415 ExpressionKind::FactPath(_) => {}
1416 _ => panic!("Expected FactPath in left operand"),
1417 }
1418 match &right.kind {
1419 ExpressionKind::Literal(_) => {}
1420 _ => panic!("Expected Literal in right operand"),
1421 }
1422 }
1423 _ => panic!("Expected Arithmetic expression"),
1424 }
1425 }
1426
1427 #[test]
1428 fn test_serialize_deserialize_round_trip_equality() {
1429 use crate::planning::semantics::ExpressionKind;
1430
1431 let age_path = FactPath::new(vec![], "age".to_string());
1432 let mut facts = IndexMap::new();
1433 facts.insert(
1434 age_path.clone(),
1435 crate::planning::semantics::FactData::Value {
1436 value: create_number_literal(0.into()),
1437 source: test_source(),
1438 is_default: false,
1439 },
1440 );
1441 let mut plan = ExecutionPlan {
1442 spec_name: "test".to_string(),
1443 facts,
1444 rules: Vec::new(),
1445 sources: {
1446 let mut s = HashMap::new();
1447 s.insert("test.lemma".to_string(), "fact age: number".to_string());
1448 s
1449 },
1450 meta: HashMap::new(),
1451 valid_from: None,
1452 valid_to: None,
1453 };
1454
1455 let rule = ExecutableRule {
1456 path: RulePath::new(vec![], "is_adult".to_string()),
1457 name: "is_adult".to_string(),
1458 branches: vec![Branch {
1459 condition: Some(Expression::new(
1460 ExpressionKind::Comparison(
1461 Arc::new(create_fact_path_expr(age_path.clone())),
1462 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1463 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1464 ),
1465 test_source(),
1466 )),
1467 result: create_literal_expr(create_boolean_literal(true)),
1468 source: test_source(),
1469 }],
1470 needs_facts: {
1471 let mut set = HashSet::new();
1472 set.insert(age_path);
1473 set
1474 },
1475 source: test_source(),
1476 rule_type: primitive_boolean().clone(),
1477 };
1478
1479 plan.rules.push(rule);
1480
1481 let json = serde_json::to_string(&plan).expect("Should serialize");
1482 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1483
1484 let json2 = serde_json::to_string(&deserialized).expect("Should serialize again");
1485 let deserialized2: ExecutionPlan =
1486 serde_json::from_str(&json2).expect("Should deserialize again");
1487
1488 assert_eq!(deserialized2.spec_name, plan.spec_name);
1489 assert_eq!(deserialized2.facts.len(), plan.facts.len());
1490 assert_eq!(deserialized2.rules.len(), plan.rules.len());
1491 assert_eq!(deserialized2.sources.len(), plan.sources.len());
1492 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
1493 assert_eq!(
1494 deserialized2.rules[0].branches.len(),
1495 plan.rules[0].branches.len()
1496 );
1497 }
1498}