Skip to main content

mockforge_contracts/consumer_contracts/
detector.rs

1//! Consumer breaking change detector
2//!
3//! This module provides functionality for detecting breaking changes that affect
4//! specific consumers based on their actual usage patterns.
5
6use crate::consumer_contracts::types::{ConsumerUsage, ConsumerViolation};
7use crate::consumer_contracts::usage_recorder::UsageRecorder;
8use crate::diff_types::{ContractDiffResult, Mismatch, MismatchType};
9use std::sync::Arc;
10use uuid::Uuid;
11
12/// Detector for consumer-specific breaking changes
13#[derive(Debug, Clone)]
14pub struct ConsumerBreakingChangeDetector {
15    usage_recorder: Arc<UsageRecorder>,
16}
17
18impl ConsumerBreakingChangeDetector {
19    /// Create a new consumer breaking change detector
20    pub fn new(usage_recorder: Arc<UsageRecorder>) -> Self {
21        Self { usage_recorder }
22    }
23
24    /// Detect violations for a consumer based on contract diff result
25    pub async fn detect_violations(
26        &self,
27        consumer_id: &str,
28        endpoint: &str,
29        method: &str,
30        diff_result: &ContractDiffResult,
31        incident_id: Option<String>,
32    ) -> Vec<ConsumerViolation> {
33        // Get consumer usage for this endpoint
34        let usage = self.usage_recorder.get_endpoint_usage(consumer_id, endpoint, method).await;
35
36        let Some(usage) = usage else {
37            // No usage recorded, can't detect violations
38            return vec![];
39        };
40        let mut violations = Vec::new();
41
42        // Check each mismatch to see if it affects fields the consumer uses
43        for mismatch in &diff_result.mismatches {
44            if self.is_violation_for_consumer(&usage, mismatch) {
45                let violated_fields = self.extract_violated_fields(&usage, mismatch);
46
47                if !violated_fields.is_empty() {
48                    violations.push(ConsumerViolation {
49                        id: Uuid::new_v4().to_string(),
50                        consumer_id: consumer_id.to_string(),
51                        incident_id: incident_id.clone(),
52                        endpoint: endpoint.to_string(),
53                        method: method.to_string(),
54                        violated_fields,
55                        detected_at: chrono::Utc::now().timestamp(),
56                    });
57                }
58            }
59        }
60
61        violations
62    }
63
64    /// Check if a mismatch is a violation for a specific consumer
65    fn is_violation_for_consumer(&self, usage: &ConsumerUsage, mismatch: &Mismatch) -> bool {
66        // Check if the mismatch affects fields the consumer uses
67        match mismatch.mismatch_type {
68            MismatchType::MissingRequiredField => {
69                // If a required field is missing and the consumer uses it, it's a violation
70                Self::field_in_usage(&mismatch.path, &usage.fields_used)
71            }
72            MismatchType::TypeMismatch => {
73                // Type mismatch affects the consumer if they use this field
74                Self::field_in_usage(&mismatch.path, &usage.fields_used)
75            }
76            MismatchType::UnexpectedField => {
77                // Unexpected fields are usually not violations (they're additions)
78                false
79            }
80            MismatchType::FormatMismatch => {
81                // Format mismatch affects the consumer if they use this field
82                Self::field_in_usage(&mismatch.path, &usage.fields_used)
83            }
84            MismatchType::ConstraintViolation => {
85                // Constraint violation affects the consumer if they use this field
86                Self::field_in_usage(&mismatch.path, &usage.fields_used)
87            }
88            _ => {
89                // Other mismatch types might affect the consumer
90                // For now, check if path matches any used field
91                Self::field_in_usage(&mismatch.path, &usage.fields_used)
92            }
93        }
94    }
95
96    /// Extract violated fields from a mismatch
97    fn extract_violated_fields(&self, usage: &ConsumerUsage, mismatch: &Mismatch) -> Vec<String> {
98        let mut violated = Vec::new();
99
100        // Check if the mismatch path matches any used field
101        if Self::field_in_usage(&mismatch.path, &usage.fields_used) {
102            violated.push(mismatch.path.clone());
103
104            // Also check for nested fields
105            for field in &usage.fields_used {
106                if field.starts_with(&mismatch.path) {
107                    violated.push(field.clone());
108                }
109            }
110        }
111
112        violated
113    }
114
115    /// Check if a field path is in the usage list
116    fn field_in_usage(field_path: &str, fields_used: &[String]) -> bool {
117        // Exact match
118        if fields_used.contains(&field_path.to_string()) {
119            return true;
120        }
121
122        // Check if any used field is a child of the mismatch path
123        for used_field in fields_used {
124            if used_field.starts_with(field_path) {
125                return true;
126            }
127        }
128
129        // Check if mismatch path is a parent of any used field
130        for used_field in fields_used {
131            if field_path.starts_with(used_field) {
132                return true;
133            }
134        }
135
136        false
137    }
138}