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