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 }
122 }
123
124 fn extract_action_writes(&self, rule: &Rule) -> Vec<String> {
126 let mut writes = Vec::new();
127
128 for action in &rule.actions {
130 match action {
131 crate::types::ActionType::Set { field, .. } => {
132 writes.push(field.clone());
133 }
134 crate::types::ActionType::Update { object } => {
135 writes.push(object.clone());
136 }
137 crate::types::ActionType::MethodCall { object, method, .. } => {
138 writes.push(object.clone());
140
141 if method.contains("set")
143 || method.contains("update")
144 || method.contains("modify")
145 || method.contains("change")
146 {
147 writes.push(format!("{}.{}", object, method));
148 }
149 }
150 crate::types::ActionType::Call { function, .. } => {
151 writes.extend(self.analyze_function_side_effects(function));
153 }
154 crate::types::ActionType::Custom {
155 action_type,
156 params,
157 } => {
158 if let Some(crate::types::Value::String(field)) = params.get("target_field") {
160 writes.push(field.clone());
161 }
162
163 writes.extend(self.analyze_custom_action_side_effects(action_type, params));
165 }
166 crate::types::ActionType::Log { .. } => {}
168 crate::types::ActionType::ActivateAgendaGroup { .. } => {}
170 crate::types::ActionType::ScheduleRule { .. } => {}
171 crate::types::ActionType::CompleteWorkflow { .. } => {}
172 crate::types::ActionType::SetWorkflowData { .. } => {}
173 }
174 }
175
176 writes
177 }
178
179 fn analyze_function_side_effects(&self, function_name: &str) -> Vec<String> {
181 let mut side_effects = Vec::new();
182
183 if function_name.starts_with("set") || function_name.starts_with("update") {
185 if let Some(field) = self.extract_field_from_function_name(function_name) {
187 side_effects.push(field);
188 }
189 } else if function_name.starts_with("calculate") || function_name.starts_with("compute") {
190 if let Some(field) = self.extract_field_from_function_name(function_name) {
192 side_effects.push(field);
193 }
194 } else if function_name.contains("modify") || function_name.contains("change") {
195 if let Some(field) = self.extract_field_from_function_name(function_name) {
197 side_effects.push(field);
198 }
199 }
200
201 side_effects
202 }
203
204 fn analyze_custom_action_side_effects(
206 &self,
207 action_type: &str,
208 params: &std::collections::HashMap<String, crate::types::Value>,
209 ) -> Vec<String> {
210 let mut side_effects = Vec::new();
211
212 for (key, value) in params {
214 if key == "field" || key == "target" || key == "output_field" {
215 if let crate::types::Value::String(field_name) = value {
216 side_effects.push(field_name.clone());
217 }
218 }
219 }
220
221 if action_type.contains("set")
223 || action_type.contains("update")
224 || action_type.contains("modify")
225 || action_type.contains("calculate")
226 {
227 if let Some(field) = self.extract_field_from_function_name(action_type) {
229 side_effects.push(field);
230 }
231 }
232
233 side_effects
234 }
235
236 fn extract_field_from_function_name(&self, name: &str) -> Option<String> {
238 let name = name
244 .trim_start_matches("set")
245 .trim_start_matches("update")
246 .trim_start_matches("calculate")
247 .trim_start_matches("compute")
248 .trim_start_matches("modify")
249 .trim_start_matches("change");
250
251 if name.contains("User") && name.contains("Score") {
253 Some("User.Score".to_string())
254 } else if name.contains("User") && name.contains("VIP") {
255 Some("User.IsVIP".to_string())
256 } else if name.contains("Order") && name.contains("Total") {
257 Some("Order.Total".to_string())
258 } else if name.contains("Order") && name.contains("Amount") {
259 Some("Order.Amount".to_string())
260 } else if name.contains("Discount") {
261 Some("Order.DiscountRate".to_string())
262 } else {
263 self.convert_camel_case_to_field(name)
265 }
266 }
267
268 fn convert_camel_case_to_field(&self, name: &str) -> Option<String> {
270 if name.is_empty() {
271 return None;
272 }
273
274 let mut result = String::new();
275 let chars = name.chars().peekable();
276
277 for c in chars {
278 if c.is_uppercase() && !result.is_empty() {
279 result.push('.');
280 }
281 result.push(c);
282 }
283
284 if result.contains('.') {
285 Some(result)
286 } else {
287 None
288 }
289 }
290
291 fn build_dependency_graph(&mut self) {
293 for (field, readers) in &self.readers {
294 if let Some(writers) = self.writers.get(field) {
295 for reader in readers {
298 for writer in writers {
299 if reader != writer {
300 self.dependencies
301 .entry(reader.clone())
302 .or_default()
303 .insert(writer.clone());
304 }
305 }
306 }
307 }
308 }
309 }
310
311 fn find_conflicts(&self, rules: &[Rule]) -> Vec<DependencyConflict> {
313 let mut conflicts = Vec::new();
314
315 let mut salience_groups: HashMap<i32, Vec<&Rule>> = HashMap::new();
317 for rule in rules {
318 salience_groups.entry(rule.salience).or_default().push(rule);
319 }
320
321 for (salience, group_rules) in salience_groups {
323 if group_rules.len() <= 1 {
324 continue; }
326
327 let mut field_writers: HashMap<String, Vec<String>> = HashMap::new();
329 for rule in &group_rules {
330 let writes = self.extract_action_writes(rule);
331 for field in writes {
332 field_writers
333 .entry(field)
334 .or_default()
335 .push(rule.name.clone());
336 }
337 }
338
339 for (field, writers) in field_writers {
340 if writers.len() > 1 {
341 conflicts.push(DependencyConflict {
342 conflict_type: ConflictType::WriteWrite,
343 field: field.clone(),
344 rules: writers,
345 salience,
346 description: format!("Multiple rules write to {}", field),
347 });
348 }
349 }
350
351 for rule in &group_rules {
353 let reads = self.extract_condition_reads(rule);
354 for field in &reads {
355 if let Some(writers) = self.writers.get(field) {
356 let conflicting_writers: Vec<String> = writers
357 .iter()
358 .filter(|writer| {
359 group_rules
360 .iter()
361 .any(|r| r.name == **writer && r.name != rule.name)
362 })
363 .cloned()
364 .collect();
365
366 if !conflicting_writers.is_empty() {
367 let mut involved_rules = conflicting_writers.clone();
368 involved_rules.push(rule.name.clone());
369
370 conflicts.push(DependencyConflict {
371 conflict_type: ConflictType::ReadWrite,
372 field: field.clone(),
373 rules: involved_rules,
374 salience,
375 description: format!(
376 "Rule {} reads {} while others write to it",
377 rule.name, field
378 ),
379 });
380 }
381 }
382 }
383 }
384 }
385
386 conflicts
387 }
388
389 fn create_execution_groups(&self, rules: &[Rule]) -> Vec<ExecutionGroup> {
391 let mut groups = Vec::new();
392
393 let mut salience_groups: HashMap<i32, Vec<Rule>> = HashMap::new();
395 for rule in rules {
396 salience_groups
397 .entry(rule.salience)
398 .or_default()
399 .push(rule.clone());
400 }
401
402 let mut salience_levels: Vec<_> = salience_groups.keys().copied().collect();
404 salience_levels.sort_by(|a, b| b.cmp(a)); for salience in salience_levels {
407 let rules_at_level = &salience_groups[&salience];
408
409 if rules_at_level.len() == 1 {
410 groups.push(ExecutionGroup {
412 rules: rules_at_level.clone(),
413 execution_mode: ExecutionMode::Sequential,
414 salience,
415 can_parallelize: false,
416 conflicts: Vec::new(),
417 });
418 } else {
419 let conflicts = self.find_conflicts(rules_at_level);
421 let can_parallelize = conflicts.is_empty();
422
423 groups.push(ExecutionGroup {
424 rules: rules_at_level.clone(),
425 execution_mode: if can_parallelize {
426 ExecutionMode::Parallel
427 } else {
428 ExecutionMode::Sequential
429 },
430 salience,
431 can_parallelize,
432 conflicts,
433 });
434 }
435 }
436
437 groups
438 }
439}
440
441#[derive(Debug, Clone)]
443pub struct DependencyAnalysisResult {
444 pub total_rules: usize,
446 pub conflicts: usize,
448 pub conflict_details: Vec<DependencyConflict>,
450 pub execution_groups: Vec<ExecutionGroup>,
452 pub can_parallelize_safely: bool,
454}
455
456#[derive(Debug, Clone)]
458pub struct DependencyConflict {
459 pub conflict_type: ConflictType,
461 pub field: String,
463 pub rules: Vec<String>,
465 pub salience: i32,
467 pub description: String,
469}
470
471#[derive(Debug, Clone, PartialEq)]
473pub enum ConflictType {
474 WriteWrite,
476 ReadWrite,
478 Circular,
480}
481
482#[derive(Debug, Clone)]
484pub struct ExecutionGroup {
485 pub rules: Vec<Rule>,
487 pub execution_mode: ExecutionMode,
489 pub salience: i32,
491 pub can_parallelize: bool,
493 pub conflicts: Vec<DependencyConflict>,
495}
496
497#[derive(Debug, Clone, PartialEq)]
499pub enum ExecutionMode {
500 Parallel,
502 Sequential,
504}
505
506#[derive(Debug, Clone, PartialEq)]
508pub enum ExecutionStrategy {
509 FullSequential,
511 FullParallel,
513 Hybrid,
515 ForcedSequential,
517}
518
519impl DependencyAnalysisResult {
520 pub fn get_summary(&self) -> String {
522 format!(
523 "š Dependency Analysis Summary:\n Total rules: {}\n Conflicts found: {}\n Safe for parallel: {}\n Execution groups: {}",
524 self.total_rules,
525 self.conflicts,
526 if self.can_parallelize_safely { "ā
Yes" } else { "ā No" },
527 self.execution_groups.len()
528 )
529 }
530
531 pub fn get_detailed_report(&self) -> String {
533 let mut report = self.get_summary();
534 report.push_str("\n\nš Detailed Analysis:");
535
536 for (i, group) in self.execution_groups.iter().enumerate() {
537 report.push_str(&format!(
538 "\n\nš Group {} (Salience {}):",
539 i + 1,
540 group.salience
541 ));
542 report.push_str(&format!(
543 "\n Mode: {:?} | Can parallelize: {}",
544 group.execution_mode,
545 if group.can_parallelize { "ā
" } else { "ā" }
546 ));
547 report.push_str(&format!(
548 "\n Rules: {}",
549 group
550 .rules
551 .iter()
552 .map(|r| r.name.as_str())
553 .collect::<Vec<_>>()
554 .join(", ")
555 ));
556
557 if !group.conflicts.is_empty() {
558 report.push_str("\n šØ Conflicts:");
559 for conflict in &group.conflicts {
560 report.push_str(&format!(
561 "\n - {}: {} (rules: {})",
562 match conflict.conflict_type {
563 ConflictType::WriteWrite => "Write-Write",
564 ConflictType::ReadWrite => "Read-Write",
565 ConflictType::Circular => "Circular",
566 },
567 conflict.field,
568 conflict.rules.join(", ")
569 ));
570 }
571 }
572 }
573
574 report
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581 use crate::engine::rule::{Condition, ConditionGroup};
582
583 #[test]
584 fn test_dependency_analyzer_creation() {
585 let analyzer = DependencyAnalyzer::new();
586 assert!(analyzer.readers.is_empty());
587 assert!(analyzer.writers.is_empty());
588 assert!(analyzer.dependencies.is_empty());
589 }
590
591 #[test]
592 fn test_safe_rules_analysis() {
593 let mut analyzer = DependencyAnalyzer::new();
594
595 let rules = vec![
596 Rule::new(
597 "AgeValidation".to_string(),
598 ConditionGroup::Single(Condition::new(
599 "User.Age".to_string(),
600 crate::types::Operator::GreaterThan,
601 crate::types::Value::Integer(18),
602 )),
603 vec![],
604 ),
605 Rule::new(
606 "CountryCheck".to_string(),
607 ConditionGroup::Single(Condition::new(
608 "User.Country".to_string(),
609 crate::types::Operator::Equal,
610 crate::types::Value::String("US".to_string()),
611 )),
612 vec![],
613 ),
614 ];
615
616 let result = analyzer.analyze(&rules);
617 assert_eq!(result.total_rules, 2);
618 assert_eq!(result.conflicts, 0);
619 assert!(result.can_parallelize_safely);
620 }
621
622 #[test]
623 fn test_conflicting_rules_analysis() {
624 let mut analyzer = DependencyAnalyzer::new();
625
626 let rules = vec![
627 Rule::new(
628 "CalculateScore".to_string(),
629 ConditionGroup::Single(Condition::new(
630 "User.Data".to_string(),
631 crate::types::Operator::Equal,
632 crate::types::Value::String("valid".to_string()),
633 )),
634 vec![crate::types::ActionType::Set {
635 field: "User.Score".to_string(),
636 value: crate::types::Value::Integer(85),
637 }],
638 ),
639 Rule::new(
640 "CheckVIPStatus".to_string(),
641 ConditionGroup::Single(Condition::new(
642 "User.Score".to_string(),
643 crate::types::Operator::GreaterThan,
644 crate::types::Value::Integer(80),
645 )),
646 vec![crate::types::ActionType::Set {
647 field: "User.IsVIP".to_string(),
648 value: crate::types::Value::Boolean(true),
649 }],
650 ),
651 ];
652
653 let result = analyzer.analyze(&rules);
654 assert_eq!(result.total_rules, 2);
655 assert!(!result.can_parallelize_safely);
657 }
658}