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