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