use crate::ai_contract_diff::{
CapturedRequest, ContractDiffAnalyzer, ContractDiffConfig, ContractDiffResult, DiffMetadata,
Mismatch, MismatchSeverity, MismatchType,
};
use crate::contract_validation::ContractValidator;
use crate::intelligent_behavior::{config::IntelligentBehaviorConfig, llm_client::LlmClient};
use crate::{OpenApiSpec, Result};
use chrono::Utc;
use serde::{Deserialize, Serialize};
pub struct ContractDiffHandler {
#[allow(dead_code)]
llm_client: LlmClient,
analyzer: ContractDiffAnalyzer,
#[allow(dead_code)]
config: IntelligentBehaviorConfig,
}
impl ContractDiffHandler {
pub fn new() -> Result<Self> {
let config = IntelligentBehaviorConfig::default();
let llm_client = LlmClient::new(config.behavior_model.clone());
let diff_config = ContractDiffConfig {
enabled: true,
llm_provider: config.behavior_model.llm_provider.clone(),
llm_model: config.behavior_model.model.clone(),
confidence_threshold: 0.5,
..Default::default()
};
let analyzer = ContractDiffAnalyzer::new(diff_config)?;
Ok(Self {
llm_client,
analyzer,
config,
})
}
pub fn with_config(config: IntelligentBehaviorConfig) -> Result<Self> {
let llm_client = LlmClient::new(config.behavior_model.clone());
let diff_config = ContractDiffConfig {
enabled: true,
llm_provider: config.behavior_model.llm_provider.clone(),
llm_model: config.behavior_model.model.clone(),
confidence_threshold: 0.5,
..Default::default()
};
let analyzer = ContractDiffAnalyzer::new(diff_config)?;
Ok(Self {
llm_client,
analyzer,
config,
})
}
pub async fn analyze_from_query(
&self,
query: &str,
spec: Option<&OpenApiSpec>,
captured_request: Option<CapturedRequest>,
) -> Result<ContractDiffQueryResult> {
let intent = self.parse_query_intent(query).await?;
match intent {
ContractDiffIntent::AnalyzeRequest { request_id, filters } => {
if let Some(request) = captured_request {
if let Some(spec) = spec {
let result = self.analyzer.analyze(&request, spec).await?;
let breaking_changes = self.extract_breaking_changes(&result);
let summary = self.generate_summary(&result, &filters).await?;
Ok(ContractDiffQueryResult {
intent: ContractDiffIntent::AnalyzeRequest {
request_id: None,
filters: filters.clone(),
},
result: Some(result),
summary,
breaking_changes,
link_to_viewer: Some(format!("/contract-diff?request_id={}", request_id.unwrap_or_default())),
})
} else {
Err(crate::Error::internal("OpenAPI spec is required for analysis"))
}
} else {
Err(crate::Error::internal("Captured request is required for analysis"))
}
}
ContractDiffIntent::CompareVersions { spec1_path, spec2_path, filters } => {
Ok(ContractDiffQueryResult {
intent: ContractDiffIntent::CompareVersions {
spec1_path: spec1_path.clone(),
spec2_path: spec2_path.clone(),
filters: filters.clone(),
},
result: None,
summary: format!(
"To compare versions, please provide both OpenAPI specifications. Spec 1: {}, Spec 2: {}",
spec1_path.unwrap_or_else(|| "not specified".to_string()),
spec2_path.unwrap_or_else(|| "not specified".to_string())
),
breaking_changes: Vec::new(),
link_to_viewer: Some("/contract-diff/compare".to_string()),
})
}
ContractDiffIntent::SummarizeDrift { filters } => {
Ok(ContractDiffQueryResult {
intent: ContractDiffIntent::SummarizeDrift { filters: filters.clone() },
result: None,
summary: "Drift summary would be generated from recent contract diff analyses. Use the Contract Diff page to view detailed drift history.".to_string(),
breaking_changes: Vec::new(),
link_to_viewer: Some("/contract-diff".to_string()),
})
}
ContractDiffIntent::FindBreakingChanges { filters } => {
if let Some(_spec) = spec {
Ok(ContractDiffQueryResult {
intent: ContractDiffIntent::FindBreakingChanges { filters: filters.clone() },
result: None,
summary: "Breaking changes analysis requires comparing against a previous contract version. Use the Contract Diff page to compare versions.".to_string(),
breaking_changes: Vec::new(),
link_to_viewer: Some("/contract-diff".to_string()),
})
} else {
Err(crate::Error::internal("OpenAPI spec is required for breaking changes analysis"))
}
}
ContractDiffIntent::Unknown => {
Ok(ContractDiffQueryResult {
intent: ContractDiffIntent::Unknown,
result: None,
summary: "I can help with contract diff analysis! Try asking:\n- \"Analyze the last captured request\"\n- \"Show me breaking changes\"\n- \"Compare contract versions\"\n- \"Summarize drift for mobile endpoints\"".to_string(),
breaking_changes: Vec::new(),
link_to_viewer: None,
})
}
}
}
pub async fn compare_versions(
&self,
spec1: &OpenApiSpec,
spec2: &OpenApiSpec,
) -> Result<ContractDiffResult> {
let validator = ContractValidator::new();
let validation_result = validator.compare_specs(spec1, spec2);
let mismatches: Vec<Mismatch> = validation_result
.errors
.iter()
.map(|err| Mismatch {
mismatch_type: if err.is_breaking_change {
MismatchType::SchemaMismatch
} else {
MismatchType::ConstraintViolation
},
path: err.path.clone(),
method: None,
expected: err.expected.clone(),
actual: err.actual.clone(),
description: err.message.clone(),
severity: if err.is_breaking_change {
MismatchSeverity::Critical
} else {
MismatchSeverity::High
},
confidence: 1.0,
context: std::collections::HashMap::new(),
})
.chain(validation_result.breaking_changes.iter().map(|bc| Mismatch {
mismatch_type: MismatchType::SchemaMismatch,
path: bc.path.clone(),
method: None,
expected: None,
actual: None,
description: bc.description.clone(),
severity: MismatchSeverity::Critical,
confidence: 1.0,
context: std::collections::HashMap::new(),
}))
.collect();
Ok(ContractDiffResult {
matches: validation_result.passed,
confidence: 1.0,
mismatches,
recommendations: Vec::new(),
corrections: Vec::new(),
metadata: DiffMetadata {
analyzed_at: Utc::now(),
request_source: "version_comparison".to_string(),
contract_version: None,
contract_format: "openapi-3.0".to_string(),
endpoint_path: "/".to_string(),
http_method: "ALL".to_string(),
request_count: 0,
llm_provider: None,
llm_model: None,
},
})
}
pub async fn summarize_drift(
&self,
results: &[ContractDiffResult],
filters: &ContractDiffFilters,
) -> Result<String> {
if results.is_empty() {
return Ok("No contract drift detected in recent analyses.".to_string());
}
let total_mismatches: usize = results.iter().map(|r| r.mismatches.len()).sum();
let breaking_count = results
.iter()
.flat_map(|r| &r.mismatches)
.filter(|m| m.severity == crate::ai_contract_diff::MismatchSeverity::Critical)
.count();
let mut summary = format!(
"Contract drift summary:\n- Total analyses: {}\n- Total mismatches: {}\n- Breaking changes: {}",
results.len(),
total_mismatches,
breaking_count
);
if let Some(ref endpoint_filter) = filters.endpoint_filter {
summary.push_str(&format!("\n- Filtered by endpoint: {}", endpoint_filter));
}
if filters.breaking_only {
summary.push_str("\n- Showing breaking changes only");
}
Ok(summary)
}
pub fn find_breaking_changes(&self, result: &ContractDiffResult) -> Vec<BreakingChange> {
result
.mismatches
.iter()
.filter(|m| m.severity == crate::ai_contract_diff::MismatchSeverity::Critical)
.map(|m| BreakingChange {
path: m.path.clone(),
method: m.method.clone(),
description: m.description.clone(),
impact: "High - This change will break existing clients".to_string(),
})
.collect()
}
fn extract_breaking_changes(&self, result: &ContractDiffResult) -> Vec<BreakingChange> {
self.find_breaking_changes(result)
}
async fn generate_summary(
&self,
result: &ContractDiffResult,
filters: &ContractDiffFilters,
) -> Result<String> {
if result.matches {
return Ok("Contract validation passed - no mismatches detected.".to_string());
}
let mut summary =
format!("Found {} mismatch(es) between request and contract.", result.mismatches.len());
if filters.breaking_only {
let breaking = result
.mismatches
.iter()
.filter(|m| m.severity == crate::ai_contract_diff::MismatchSeverity::Critical)
.count();
summary = format!("Found {} breaking change(s).", breaking);
}
if !result.recommendations.is_empty() {
summary.push_str(&format!(
"\n\n{} AI-powered recommendation(s) available.",
result.recommendations.len()
));
}
if !result.corrections.is_empty() {
summary.push_str(&format!(
"\n\n{} correction proposal(s) available.",
result.corrections.len()
));
}
Ok(summary)
}
async fn parse_query_intent(&self, query: &str) -> Result<ContractDiffIntent> {
let query_lower = query.to_lowercase();
if query_lower.contains("analyze") || query_lower.contains("check") {
let request_id = self.extract_request_id(query);
let filters = self.extract_filters(query);
return Ok(ContractDiffIntent::AnalyzeRequest {
request_id,
filters,
});
}
if query_lower.contains("compare") || query_lower.contains("diff") {
let (spec1, spec2) = self.extract_spec_paths(query);
let filters = self.extract_filters(query);
return Ok(ContractDiffIntent::CompareVersions {
spec1_path: spec1,
spec2_path: spec2,
filters,
});
}
if query_lower.contains("summarize")
|| query_lower.contains("summary")
|| query_lower.contains("drift")
{
let filters = self.extract_filters(query);
return Ok(ContractDiffIntent::SummarizeDrift { filters });
}
if query_lower.contains("breaking") || query_lower.contains("breaking change") {
let filters = self.extract_filters(query);
return Ok(ContractDiffIntent::FindBreakingChanges { filters });
}
Ok(ContractDiffIntent::Unknown)
}
fn extract_request_id(&self, query: &str) -> Option<String> {
for word in query.split_whitespace() {
if word.len() > 10 {
return Some(word.to_string());
}
}
None
}
fn extract_spec_paths(&self, query: &str) -> (Option<String>, Option<String>) {
let words: Vec<&str> = query.split_whitespace().collect();
let mut paths = Vec::new();
for word in words {
if word.ends_with(".yaml")
|| word.ends_with(".yml")
|| word.ends_with(".json")
|| word.starts_with("http")
{
paths.push(word.to_string());
}
}
match paths.len() {
0 => (None, None),
1 => (Some(paths[0].clone()), None),
_ => (Some(paths[0].clone()), Some(paths[1].clone())),
}
}
fn extract_filters(&self, query: &str) -> ContractDiffFilters {
let query_lower = query.to_lowercase();
ContractDiffFilters {
breaking_only: query_lower.contains("breaking")
|| query_lower.contains("breaking change"),
endpoint_filter: if query_lower.contains("mobile") {
Some("mobile".to_string())
} else if query_lower.contains("api") {
Some("api".to_string())
} else {
None
},
}
}
}
impl Default for ContractDiffHandler {
fn default() -> Self {
Self::new().expect("Failed to create ContractDiffHandler")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ContractDiffIntent {
AnalyzeRequest {
request_id: Option<String>,
filters: ContractDiffFilters,
},
CompareVersions {
spec1_path: Option<String>,
spec2_path: Option<String>,
filters: ContractDiffFilters,
},
SummarizeDrift {
filters: ContractDiffFilters,
},
FindBreakingChanges {
filters: ContractDiffFilters,
},
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContractDiffFilters {
pub breaking_only: bool,
pub endpoint_filter: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractDiffQueryResult {
pub intent: ContractDiffIntent,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<ContractDiffResult>,
pub summary: String,
pub breaking_changes: Vec<BreakingChange>,
#[serde(skip_serializing_if = "Option::is_none")]
pub link_to_viewer: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BreakingChange {
pub path: String,
pub method: Option<String>,
pub description: String,
pub impact: String,
}