mockforge_contracts/consumer_contracts/
detector.rs1use 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#[derive(Debug, Clone)]
14pub struct ConsumerBreakingChangeDetector {
15 usage_recorder: Arc<UsageRecorder>,
16}
17
18impl ConsumerBreakingChangeDetector {
19 pub fn new(usage_recorder: Arc<UsageRecorder>) -> Self {
21 Self { usage_recorder }
22 }
23
24 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 let usage = self.usage_recorder.get_endpoint_usage(consumer_id, endpoint, method).await;
35
36 let Some(usage) = usage else {
37 return vec![];
39 };
40 let mut violations = Vec::new();
41
42 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 fn is_violation_for_consumer(&self, usage: &ConsumerUsage, mismatch: &Mismatch) -> bool {
66 match mismatch.mismatch_type {
68 MismatchType::MissingRequiredField => {
69 Self::field_in_usage(&mismatch.path, &usage.fields_used)
71 }
72 MismatchType::TypeMismatch => {
73 Self::field_in_usage(&mismatch.path, &usage.fields_used)
75 }
76 MismatchType::UnexpectedField => {
77 false
79 }
80 MismatchType::FormatMismatch => {
81 Self::field_in_usage(&mismatch.path, &usage.fields_used)
83 }
84 MismatchType::ConstraintViolation => {
85 Self::field_in_usage(&mismatch.path, &usage.fields_used)
87 }
88 _ => {
89 Self::field_in_usage(&mismatch.path, &usage.fields_used)
92 }
93 }
94 }
95
96 fn extract_violated_fields(&self, usage: &ConsumerUsage, mismatch: &Mismatch) -> Vec<String> {
98 let mut violated = Vec::new();
99
100 if Self::field_in_usage(&mismatch.path, &usage.fields_used) {
102 violated.push(mismatch.path.clone());
103
104 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 fn field_in_usage(field_path: &str, fields_used: &[String]) -> bool {
117 if fields_used.contains(&field_path.to_string()) {
119 return true;
120 }
121
122 for used_field in fields_used {
124 if used_field.starts_with(field_path) {
125 return true;
126 }
127 }
128
129 for used_field in fields_used {
131 if field_path.starts_with(used_field) {
132 return true;
133 }
134 }
135
136 false
137 }
138}