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