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