1use crate::engine::rule::Rule;
2use std::collections::{HashMap, HashSet};
3
4#[derive(Debug, Clone)]
6pub struct DependencyAnalyzer {
7 readers: HashMap<String, Vec<String>>, writers: HashMap<String, Vec<String>>, dependencies: HashMap<String, HashSet<String>>,
13}
14
15impl Default for DependencyAnalyzer {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl DependencyAnalyzer {
22 pub fn new() -> Self {
24 Self {
25 readers: HashMap::new(),
26 writers: HashMap::new(),
27 dependencies: HashMap::new(),
28 }
29 }
30
31 pub fn analyze(&mut self, rules: &[Rule]) -> DependencyAnalysisResult {
33 self.clear();
34
35 for rule in rules {
37 self.analyze_rule_io(rule);
38 }
39
40 self.build_dependency_graph();
42
43 let conflicts = self.find_conflicts(rules);
45
46 let execution_groups = self.create_execution_groups(rules);
48 let conflicts_len = conflicts.len();
49
50 DependencyAnalysisResult {
51 total_rules: rules.len(),
52 conflicts: conflicts_len,
53 conflict_details: conflicts,
54 execution_groups,
55 can_parallelize_safely: conflicts_len == 0,
56 }
57 }
58
59 fn clear(&mut self) {
61 self.readers.clear();
62 self.writers.clear();
63 self.dependencies.clear();
64 }
65
66 fn analyze_rule_io(&mut self, rule: &Rule) {
68 let condition_reads = self.extract_condition_reads(rule);
70 for field in condition_reads {
71 self.readers
72 .entry(field)
73 .or_default()
74 .push(rule.name.clone());
75 }
76
77 let action_writes = self.extract_action_writes(rule);
79 for field in action_writes {
80 self.writers
81 .entry(field)
82 .or_default()
83 .push(rule.name.clone());
84 }
85 }
86
87 fn extract_condition_reads(&self, rule: &Rule) -> Vec<String> {
89 let mut reads = Vec::new();
90
91 Self::extract_fields_from_condition_group(&rule.conditions, &mut reads);
93
94 reads
95 }
96
97 fn extract_fields_from_condition_group(
99 condition_group: &crate::engine::rule::ConditionGroup,
100 reads: &mut Vec<String>,
101 ) {
102 match condition_group {
103 crate::engine::rule::ConditionGroup::Single(condition) => {
104 reads.push(condition.field.clone());
105 }
106 crate::engine::rule::ConditionGroup::Compound { left, right, .. } => {
107 Self::extract_fields_from_condition_group(left, reads);
108 Self::extract_fields_from_condition_group(right, reads);
109 }
110 crate::engine::rule::ConditionGroup::Not(inner) => {
111 Self::extract_fields_from_condition_group(inner, reads);
112 }
113 crate::engine::rule::ConditionGroup::Exists(inner) => {
114 Self::extract_fields_from_condition_group(inner, reads);
116 }
117 crate::engine::rule::ConditionGroup::Forall(inner) => {
118 Self::extract_fields_from_condition_group(inner, reads);
120 }
121 crate::engine::rule::ConditionGroup::Accumulate { source_pattern, extract_field, .. } => {
122 reads.push(format!("{}.{}", source_pattern, extract_field));
124 }
125 }
126 }
127
128 fn extract_action_writes(&self, rule: &Rule) -> Vec<String> {
130 let mut writes = Vec::new();
131
132 for action in &rule.actions {
134 match action {
135 crate::types::ActionType::Set { field, .. } => {
136 writes.push(field.clone());
137 }
138 crate::types::ActionType::Update { object } => {
139 writes.push(object.clone());
140 }
141 crate::types::ActionType::MethodCall { object, method, .. } => {
142 writes.push(object.clone());
144
145 if method.contains("set")
147 || method.contains("update")
148 || method.contains("modify")
149 || method.contains("change")
150 {
151 writes.push(format!("{}.{}", object, method));
152 }
153 }
154 crate::types::ActionType::Call { function, .. } => {
155 writes.extend(self.analyze_function_side_effects(function));
157 }
158 crate::types::ActionType::Custom {
159 action_type,
160 params,
161 } => {
162 if let Some(crate::types::Value::String(field)) = params.get("target_field") {
164 writes.push(field.clone());
165 }
166
167 writes.extend(self.analyze_custom_action_side_effects(action_type, params));
169 }
170 crate::types::ActionType::Log { .. } => {}
172 crate::types::ActionType::ActivateAgendaGroup { .. } => {}
174 crate::types::ActionType::ScheduleRule { .. } => {}
175 crate::types::ActionType::CompleteWorkflow { .. } => {}
176 crate::types::ActionType::SetWorkflowData { .. } => {}
177 }
178 }
179
180 writes
181 }
182
183 fn analyze_function_side_effects(&self, function_name: &str) -> Vec<String> {
185 let mut side_effects = Vec::new();
186
187 if function_name.starts_with("set") || function_name.starts_with("update") {
189 if let Some(field) = self.extract_field_from_function_name(function_name) {
191 side_effects.push(field);
192 }
193 } else if function_name.starts_with("calculate") || function_name.starts_with("compute") {
194 if let Some(field) = self.extract_field_from_function_name(function_name) {
196 side_effects.push(field);
197 }
198 } else if function_name.contains("modify") || function_name.contains("change") {
199 if let Some(field) = self.extract_field_from_function_name(function_name) {
201 side_effects.push(field);
202 }
203 }
204
205 side_effects
206 }
207
208 fn analyze_custom_action_side_effects(
210 &self,
211 action_type: &str,
212 params: &std::collections::HashMap<String, crate::types::Value>,
213 ) -> Vec<String> {
214 let mut side_effects = Vec::new();
215
216 for (key, value) in params {
218 if key == "field" || key == "target" || key == "output_field" {
219 if let crate::types::Value::String(field_name) = value {
220 side_effects.push(field_name.clone());
221 }
222 }
223 }
224
225 if action_type.contains("set")
227 || action_type.contains("update")
228 || action_type.contains("modify")
229 || action_type.contains("calculate")
230 {
231 if let Some(field) = self.extract_field_from_function_name(action_type) {
233 side_effects.push(field);
234 }
235 }
236
237 side_effects
238 }
239
240 fn extract_field_from_function_name(&self, name: &str) -> Option<String> {
242 let name = name
248 .trim_start_matches("set")
249 .trim_start_matches("update")
250 .trim_start_matches("calculate")
251 .trim_start_matches("compute")
252 .trim_start_matches("modify")
253 .trim_start_matches("change");
254
255 if name.contains("User") && name.contains("Score") {
257 Some("User.Score".to_string())
258 } else if name.contains("User") && name.contains("VIP") {
259 Some("User.IsVIP".to_string())
260 } else if name.contains("Order") && name.contains("Total") {
261 Some("Order.Total".to_string())
262 } else if name.contains("Order") && name.contains("Amount") {
263 Some("Order.Amount".to_string())
264 } else if name.contains("Discount") {
265 Some("Order.DiscountRate".to_string())
266 } else {
267 self.convert_camel_case_to_field(name)
269 }
270 }
271
272 fn convert_camel_case_to_field(&self, name: &str) -> Option<String> {
274 if name.is_empty() {
275 return None;
276 }
277
278 let mut result = String::new();
279 let chars = name.chars().peekable();
280
281 for c in chars {
282 if c.is_uppercase() && !result.is_empty() {
283 result.push('.');
284 }
285 result.push(c);
286 }
287
288 if result.contains('.') {
289 Some(result)
290 } else {
291 None
292 }
293 }
294
295 fn build_dependency_graph(&mut self) {
297 for (field, readers) in &self.readers {
298 if let Some(writers) = self.writers.get(field) {
299 for reader in readers {
302 for writer in writers {
303 if reader != writer {
304 self.dependencies
305 .entry(reader.clone())
306 .or_default()
307 .insert(writer.clone());
308 }
309 }
310 }
311 }
312 }
313 }
314
315 fn find_conflicts(&self, rules: &[Rule]) -> Vec<DependencyConflict> {
317 let mut conflicts = Vec::new();
318
319 let mut salience_groups: HashMap<i32, Vec<&Rule>> = HashMap::new();
321 for rule in rules {
322 salience_groups.entry(rule.salience).or_default().push(rule);
323 }
324
325 for (salience, group_rules) in salience_groups {
327 if group_rules.len() <= 1 {
328 continue; }
330
331 let mut field_writers: HashMap<String, Vec<String>> = HashMap::new();
333 for rule in &group_rules {
334 let writes = self.extract_action_writes(rule);
335 for field in writes {
336 field_writers
337 .entry(field)
338 .or_default()
339 .push(rule.name.clone());
340 }
341 }
342
343 for (field, writers) in field_writers {
344 if writers.len() > 1 {
345 conflicts.push(DependencyConflict {
346 conflict_type: ConflictType::WriteWrite,
347 field: field.clone(),
348 rules: writers,
349 salience,
350 description: format!("Multiple rules write to {}", field),
351 });
352 }
353 }
354
355 for rule in &group_rules {
357 let reads = self.extract_condition_reads(rule);
358 for field in &reads {
359 if let Some(writers) = self.writers.get(field) {
360 let conflicting_writers: Vec<String> = writers
361 .iter()
362 .filter(|writer| {
363 group_rules
364 .iter()
365 .any(|r| r.name == **writer && r.name != rule.name)
366 })
367 .cloned()
368 .collect();
369
370 if !conflicting_writers.is_empty() {
371 let mut involved_rules = conflicting_writers.clone();
372 involved_rules.push(rule.name.clone());
373
374 conflicts.push(DependencyConflict {
375 conflict_type: ConflictType::ReadWrite,
376 field: field.clone(),
377 rules: involved_rules,
378 salience,
379 description: format!(
380 "Rule {} reads {} while others write to it",
381 rule.name, field
382 ),
383 });
384 }
385 }
386 }
387 }
388 }
389
390 conflicts
391 }
392
393 fn create_execution_groups(&self, rules: &[Rule]) -> Vec<ExecutionGroup> {
395 let mut groups = Vec::new();
396
397 let mut salience_groups: HashMap<i32, Vec<Rule>> = HashMap::new();
399 for rule in rules {
400 salience_groups
401 .entry(rule.salience)
402 .or_default()
403 .push(rule.clone());
404 }
405
406 let mut salience_levels: Vec<_> = salience_groups.keys().copied().collect();
408 salience_levels.sort_by(|a, b| b.cmp(a)); for salience in salience_levels {
411 let rules_at_level = &salience_groups[&salience];
412
413 if rules_at_level.len() == 1 {
414 groups.push(ExecutionGroup {
416 rules: rules_at_level.clone(),
417 execution_mode: ExecutionMode::Sequential,
418 salience,
419 can_parallelize: false,
420 conflicts: Vec::new(),
421 });
422 } else {
423 let conflicts = self.find_conflicts(rules_at_level);
425 let can_parallelize = conflicts.is_empty();
426
427 groups.push(ExecutionGroup {
428 rules: rules_at_level.clone(),
429 execution_mode: if can_parallelize {
430 ExecutionMode::Parallel
431 } else {
432 ExecutionMode::Sequential
433 },
434 salience,
435 can_parallelize,
436 conflicts,
437 });
438 }
439 }
440
441 groups
442 }
443}
444
445#[derive(Debug, Clone)]
447pub struct DependencyAnalysisResult {
448 pub total_rules: usize,
450 pub conflicts: usize,
452 pub conflict_details: Vec<DependencyConflict>,
454 pub execution_groups: Vec<ExecutionGroup>,
456 pub can_parallelize_safely: bool,
458}
459
460#[derive(Debug, Clone)]
462pub struct DependencyConflict {
463 pub conflict_type: ConflictType,
465 pub field: String,
467 pub rules: Vec<String>,
469 pub salience: i32,
471 pub description: String,
473}
474
475#[derive(Debug, Clone, PartialEq)]
477pub enum ConflictType {
478 WriteWrite,
480 ReadWrite,
482 Circular,
484}
485
486#[derive(Debug, Clone)]
488pub struct ExecutionGroup {
489 pub rules: Vec<Rule>,
491 pub execution_mode: ExecutionMode,
493 pub salience: i32,
495 pub can_parallelize: bool,
497 pub conflicts: Vec<DependencyConflict>,
499}
500
501#[derive(Debug, Clone, PartialEq)]
503pub enum ExecutionMode {
504 Parallel,
506 Sequential,
508}
509
510#[derive(Debug, Clone, PartialEq)]
512pub enum ExecutionStrategy {
513 FullSequential,
515 FullParallel,
517 Hybrid,
519 ForcedSequential,
521}
522
523impl DependencyAnalysisResult {
524 pub fn get_summary(&self) -> String {
526 format!(
527 "š Dependency Analysis Summary:\n Total rules: {}\n Conflicts found: {}\n Safe for parallel: {}\n Execution groups: {}",
528 self.total_rules,
529 self.conflicts,
530 if self.can_parallelize_safely { "ā
Yes" } else { "ā No" },
531 self.execution_groups.len()
532 )
533 }
534
535 pub fn get_detailed_report(&self) -> String {
537 let mut report = self.get_summary();
538 report.push_str("\n\nš Detailed Analysis:");
539
540 for (i, group) in self.execution_groups.iter().enumerate() {
541 report.push_str(&format!(
542 "\n\nš Group {} (Salience {}):",
543 i + 1,
544 group.salience
545 ));
546 report.push_str(&format!(
547 "\n Mode: {:?} | Can parallelize: {}",
548 group.execution_mode,
549 if group.can_parallelize { "ā
" } else { "ā" }
550 ));
551 report.push_str(&format!(
552 "\n Rules: {}",
553 group
554 .rules
555 .iter()
556 .map(|r| r.name.as_str())
557 .collect::<Vec<_>>()
558 .join(", ")
559 ));
560
561 if !group.conflicts.is_empty() {
562 report.push_str("\n šØ Conflicts:");
563 for conflict in &group.conflicts {
564 report.push_str(&format!(
565 "\n - {}: {} (rules: {})",
566 match conflict.conflict_type {
567 ConflictType::WriteWrite => "Write-Write",
568 ConflictType::ReadWrite => "Read-Write",
569 ConflictType::Circular => "Circular",
570 },
571 conflict.field,
572 conflict.rules.join(", ")
573 ));
574 }
575 }
576 }
577
578 report
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::engine::rule::{Condition, ConditionGroup};
586
587 #[test]
588 fn test_dependency_analyzer_creation() {
589 let analyzer = DependencyAnalyzer::new();
590 assert!(analyzer.readers.is_empty());
591 assert!(analyzer.writers.is_empty());
592 assert!(analyzer.dependencies.is_empty());
593 }
594
595 #[test]
596 fn test_safe_rules_analysis() {
597 let mut analyzer = DependencyAnalyzer::new();
598
599 let rules = vec![
600 Rule::new(
601 "AgeValidation".to_string(),
602 ConditionGroup::Single(Condition::new(
603 "User.Age".to_string(),
604 crate::types::Operator::GreaterThan,
605 crate::types::Value::Integer(18),
606 )),
607 vec![],
608 ),
609 Rule::new(
610 "CountryCheck".to_string(),
611 ConditionGroup::Single(Condition::new(
612 "User.Country".to_string(),
613 crate::types::Operator::Equal,
614 crate::types::Value::String("US".to_string()),
615 )),
616 vec![],
617 ),
618 ];
619
620 let result = analyzer.analyze(&rules);
621 assert_eq!(result.total_rules, 2);
622 assert_eq!(result.conflicts, 0);
623 assert!(result.can_parallelize_safely);
624 }
625
626 #[test]
627 fn test_conflicting_rules_analysis() {
628 let mut analyzer = DependencyAnalyzer::new();
629
630 let rules = vec![
631 Rule::new(
632 "CalculateScore".to_string(),
633 ConditionGroup::Single(Condition::new(
634 "User.Data".to_string(),
635 crate::types::Operator::Equal,
636 crate::types::Value::String("valid".to_string()),
637 )),
638 vec![crate::types::ActionType::Set {
639 field: "User.Score".to_string(),
640 value: crate::types::Value::Integer(85),
641 }],
642 ),
643 Rule::new(
644 "CheckVIPStatus".to_string(),
645 ConditionGroup::Single(Condition::new(
646 "User.Score".to_string(),
647 crate::types::Operator::GreaterThan,
648 crate::types::Value::Integer(80),
649 )),
650 vec![crate::types::ActionType::Set {
651 field: "User.IsVIP".to_string(),
652 value: crate::types::Value::Boolean(true),
653 }],
654 ),
655 ];
656
657 let result = analyzer.analyze(&rules);
658 assert_eq!(result.total_rules, 2);
659 assert!(!result.can_parallelize_safely);
661 }
662}