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