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