use crate::ai_contract_diff::{ContractDiffResult, Mismatch, MismatchType};
use crate::consumer_contracts::types::{ConsumerUsage, ConsumerViolation};
use crate::consumer_contracts::usage_recorder::UsageRecorder;
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct ConsumerBreakingChangeDetector {
usage_recorder: Arc<UsageRecorder>,
}
impl ConsumerBreakingChangeDetector {
pub fn new(usage_recorder: Arc<UsageRecorder>) -> Self {
Self { usage_recorder }
}
pub async fn detect_violations(
&self,
consumer_id: &str,
endpoint: &str,
method: &str,
diff_result: &ContractDiffResult,
incident_id: Option<String>,
) -> Vec<ConsumerViolation> {
let usage = self.usage_recorder.get_endpoint_usage(consumer_id, endpoint, method).await;
if usage.is_none() {
return vec![];
}
let usage = usage.unwrap();
let mut violations = Vec::new();
for mismatch in &diff_result.mismatches {
if self.is_violation_for_consumer(&usage, mismatch) {
let violated_fields = self.extract_violated_fields(&usage, mismatch);
if !violated_fields.is_empty() {
violations.push(ConsumerViolation {
id: Uuid::new_v4().to_string(),
consumer_id: consumer_id.to_string(),
incident_id: incident_id.clone(),
endpoint: endpoint.to_string(),
method: method.to_string(),
violated_fields,
detected_at: chrono::Utc::now().timestamp(),
});
}
}
}
violations
}
fn is_violation_for_consumer(&self, usage: &ConsumerUsage, mismatch: &Mismatch) -> bool {
match mismatch.mismatch_type {
MismatchType::MissingRequiredField => {
Self::field_in_usage(&mismatch.path, &usage.fields_used)
}
MismatchType::TypeMismatch => {
Self::field_in_usage(&mismatch.path, &usage.fields_used)
}
MismatchType::UnexpectedField => {
false
}
MismatchType::FormatMismatch => {
Self::field_in_usage(&mismatch.path, &usage.fields_used)
}
MismatchType::ConstraintViolation => {
Self::field_in_usage(&mismatch.path, &usage.fields_used)
}
_ => {
Self::field_in_usage(&mismatch.path, &usage.fields_used)
}
}
}
fn extract_violated_fields(&self, usage: &ConsumerUsage, mismatch: &Mismatch) -> Vec<String> {
let mut violated = Vec::new();
if Self::field_in_usage(&mismatch.path, &usage.fields_used) {
violated.push(mismatch.path.clone());
for field in &usage.fields_used {
if field.starts_with(&mismatch.path) {
violated.push(field.clone());
}
}
}
violated
}
fn field_in_usage(field_path: &str, fields_used: &[String]) -> bool {
if fields_used.contains(&field_path.to_string()) {
return true;
}
for used_field in fields_used {
if used_field.starts_with(field_path) {
return true;
}
}
for used_field in fields_used {
if field_path.starts_with(used_field) {
return true;
}
}
false
}
}