1use crate::ai_contract_diff::{
8 CapturedRequest, ContractDiffAnalyzer, ContractDiffConfig, ContractDiffResult, DiffMetadata,
9 Mismatch, MismatchSeverity, MismatchType,
10};
11use crate::contract_validation::ContractValidator;
12use crate::intelligent_behavior::{config::IntelligentBehaviorConfig, llm_client::LlmClient};
13use chrono::Utc;
14use mockforge_foundation::Result;
15use mockforge_openapi::OpenApiSpec;
16use serde::{Deserialize, Serialize};
17
18pub struct ContractDiffHandler {
20 #[allow(dead_code)]
22 llm_client: LlmClient,
23 analyzer: ContractDiffAnalyzer,
25 #[allow(dead_code)]
27 config: IntelligentBehaviorConfig,
28}
29
30impl ContractDiffHandler {
31 pub fn new() -> Result<Self> {
33 let config = IntelligentBehaviorConfig::default();
34 let llm_client = LlmClient::new(config.behavior_model.clone());
35 let diff_config = ContractDiffConfig {
36 enabled: true,
37 llm_provider: config.behavior_model.llm_provider.clone(),
38 llm_model: config.behavior_model.model.clone(),
39 confidence_threshold: 0.5,
40 ..Default::default()
41 };
42 let analyzer = ContractDiffAnalyzer::new(diff_config)?;
43
44 Ok(Self {
45 llm_client,
46 analyzer,
47 config,
48 })
49 }
50
51 pub fn with_config(config: IntelligentBehaviorConfig) -> Result<Self> {
53 let llm_client = LlmClient::new(config.behavior_model.clone());
54 let diff_config = ContractDiffConfig {
55 enabled: true,
56 llm_provider: config.behavior_model.llm_provider.clone(),
57 llm_model: config.behavior_model.model.clone(),
58 confidence_threshold: 0.5,
59 ..Default::default()
60 };
61 let analyzer = ContractDiffAnalyzer::new(diff_config)?;
62
63 Ok(Self {
64 llm_client,
65 analyzer,
66 config,
67 })
68 }
69
70 pub async fn analyze_from_query(
77 &self,
78 query: &str,
79 spec: Option<&OpenApiSpec>,
80 captured_request: Option<CapturedRequest>,
81 ) -> Result<ContractDiffQueryResult> {
82 let intent = self.parse_query_intent(query).await?;
84
85 match intent {
86 ContractDiffIntent::AnalyzeRequest { request_id, filters } => {
87 if let Some(request) = captured_request {
89 if let Some(spec) = spec {
90 let result = self.analyzer.analyze(&request, spec).await?;
91 let breaking_changes = self.extract_breaking_changes(&result);
92 let summary = self.generate_summary(&result, &filters).await?;
93 Ok(ContractDiffQueryResult {
94 intent: ContractDiffIntent::AnalyzeRequest {
95 request_id: None,
96 filters: filters.clone(),
97 },
98 result: Some(result),
99 summary,
100 breaking_changes,
101 link_to_viewer: Some(format!("/contract-diff?request_id={}", request_id.unwrap_or_default())),
102 })
103 } else {
104 Err(mockforge_foundation::Error::internal("OpenAPI spec is required for analysis"))
105 }
106 } else {
107 Err(mockforge_foundation::Error::internal("Captured request is required for analysis"))
108 }
109 }
110 ContractDiffIntent::CompareVersions { spec1_path, spec2_path, filters } => {
111 Ok(ContractDiffQueryResult {
114 intent: ContractDiffIntent::CompareVersions {
115 spec1_path: spec1_path.clone(),
116 spec2_path: spec2_path.clone(),
117 filters: filters.clone(),
118 },
119 result: None,
120 summary: format!(
121 "To compare versions, please provide both OpenAPI specifications. Spec 1: {}, Spec 2: {}",
122 spec1_path.unwrap_or_else(|| "not specified".to_string()),
123 spec2_path.unwrap_or_else(|| "not specified".to_string())
124 ),
125 breaking_changes: Vec::new(),
126 link_to_viewer: Some("/contract-diff/compare".to_string()),
127 })
128 }
129 ContractDiffIntent::SummarizeDrift { filters } => {
130 Ok(ContractDiffQueryResult {
132 intent: ContractDiffIntent::SummarizeDrift { filters: filters.clone() },
133 result: None,
134 summary: "Drift summary would be generated from recent contract diff analyses. Use the Contract Diff page to view detailed drift history.".to_string(),
135 breaking_changes: Vec::new(),
136 link_to_viewer: Some("/contract-diff".to_string()),
137 })
138 }
139 ContractDiffIntent::FindBreakingChanges { filters } => {
140 if let Some(_spec) = spec {
142 Ok(ContractDiffQueryResult {
144 intent: ContractDiffIntent::FindBreakingChanges { filters: filters.clone() },
145 result: None,
146 summary: "Breaking changes analysis requires comparing against a previous contract version. Use the Contract Diff page to compare versions.".to_string(),
147 breaking_changes: Vec::new(),
148 link_to_viewer: Some("/contract-diff".to_string()),
149 })
150 } else {
151 Err(mockforge_foundation::Error::internal("OpenAPI spec is required for breaking changes analysis"))
152 }
153 }
154 ContractDiffIntent::Unknown => {
155 Ok(ContractDiffQueryResult {
156 intent: ContractDiffIntent::Unknown,
157 result: None,
158 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(),
159 breaking_changes: Vec::new(),
160 link_to_viewer: None,
161 })
162 }
163 }
164 }
165
166 pub async fn compare_versions(
168 &self,
169 spec1: &OpenApiSpec,
170 spec2: &OpenApiSpec,
171 ) -> Result<ContractDiffResult> {
172 let validator = ContractValidator::new();
173 let validation_result = validator.compare_specs(spec1, spec2);
174
175 let mismatches: Vec<Mismatch> = validation_result
177 .errors
178 .iter()
179 .map(|err| Mismatch {
180 mismatch_type: if err.is_breaking_change {
181 MismatchType::SchemaMismatch
182 } else {
183 MismatchType::ConstraintViolation
184 },
185 path: err.path.clone(),
186 method: None,
187 expected: err.expected.clone(),
188 actual: err.actual.clone(),
189 description: err.message.clone(),
190 severity: if err.is_breaking_change {
191 MismatchSeverity::Critical
192 } else {
193 MismatchSeverity::High
194 },
195 confidence: 1.0,
196 context: std::collections::HashMap::new(),
197 })
198 .chain(validation_result.breaking_changes.iter().map(|bc| Mismatch {
199 mismatch_type: MismatchType::SchemaMismatch,
200 path: bc.path.clone(),
201 method: None,
202 expected: None,
203 actual: None,
204 description: bc.description.clone(),
205 severity: MismatchSeverity::Critical,
206 confidence: 1.0,
207 context: std::collections::HashMap::new(),
208 }))
209 .collect();
210
211 Ok(ContractDiffResult {
212 matches: validation_result.passed,
213 confidence: 1.0,
214 mismatches,
215 recommendations: Vec::new(),
216 corrections: Vec::new(),
217 metadata: DiffMetadata {
218 analyzed_at: Utc::now(),
219 request_source: "version_comparison".to_string(),
220 contract_version: None,
221 contract_format: "openapi-3.0".to_string(),
222 endpoint_path: "/".to_string(),
223 http_method: "ALL".to_string(),
224 request_count: 0,
225 llm_provider: None,
226 llm_model: None,
227 },
228 })
229 }
230
231 pub async fn summarize_drift(
235 &self,
236 results: &[ContractDiffResult],
237 filters: &ContractDiffFilters,
238 ) -> Result<String> {
239 if results.is_empty() {
240 return Ok("No contract drift detected in recent analyses.".to_string());
241 }
242
243 let total_mismatches: usize = results.iter().map(|r| r.mismatches.len()).sum();
244 let breaking_count = results
245 .iter()
246 .flat_map(|r| &r.mismatches)
247 .filter(|m| m.severity == MismatchSeverity::Critical)
248 .count();
249
250 let mut summary = format!(
251 "Contract drift summary:\n- Total analyses: {}\n- Total mismatches: {}\n- Breaking changes: {}",
252 results.len(),
253 total_mismatches,
254 breaking_count
255 );
256
257 if let Some(ref endpoint_filter) = filters.endpoint_filter {
259 summary.push_str(&format!("\n- Filtered by endpoint: {}", endpoint_filter));
260 }
261
262 if filters.breaking_only {
263 summary.push_str("\n- Showing breaking changes only");
264 }
265
266 Ok(summary)
267 }
268
269 pub fn find_breaking_changes(&self, result: &ContractDiffResult) -> Vec<BreakingChange> {
271 result
272 .mismatches
273 .iter()
274 .filter(|m| m.severity == MismatchSeverity::Critical)
275 .map(|m| BreakingChange {
276 path: m.path.clone(),
277 method: m.method.clone(),
278 description: m.description.clone(),
279 impact: "High - This change will break existing clients".to_string(),
280 })
281 .collect()
282 }
283
284 fn extract_breaking_changes(&self, result: &ContractDiffResult) -> Vec<BreakingChange> {
286 self.find_breaking_changes(result)
287 }
288
289 async fn generate_summary(
291 &self,
292 result: &ContractDiffResult,
293 filters: &ContractDiffFilters,
294 ) -> Result<String> {
295 if result.matches {
296 return Ok("Contract validation passed - no mismatches detected.".to_string());
297 }
298
299 let mut summary =
300 format!("Found {} mismatch(es) between request and contract.", result.mismatches.len());
301
302 if filters.breaking_only {
303 let breaking = result
304 .mismatches
305 .iter()
306 .filter(|m| m.severity == MismatchSeverity::Critical)
307 .count();
308 summary = format!("Found {} breaking change(s).", breaking);
309 }
310
311 if !result.recommendations.is_empty() {
312 summary.push_str(&format!(
313 "\n\n{} AI-powered recommendation(s) available.",
314 result.recommendations.len()
315 ));
316 }
317
318 if !result.corrections.is_empty() {
319 summary.push_str(&format!(
320 "\n\n{} correction proposal(s) available.",
321 result.corrections.len()
322 ));
323 }
324
325 Ok(summary)
326 }
327
328 async fn parse_query_intent(&self, query: &str) -> Result<ContractDiffIntent> {
330 let query_lower = query.to_lowercase();
331
332 if query_lower.contains("analyze") || query_lower.contains("check") {
334 let request_id = self.extract_request_id(query);
336 let filters = self.extract_filters(query);
337 return Ok(ContractDiffIntent::AnalyzeRequest {
338 request_id,
339 filters,
340 });
341 }
342
343 if query_lower.contains("compare") || query_lower.contains("diff") {
344 let (spec1, spec2) = self.extract_spec_paths(query);
345 let filters = self.extract_filters(query);
346 return Ok(ContractDiffIntent::CompareVersions {
347 spec1_path: spec1,
348 spec2_path: spec2,
349 filters,
350 });
351 }
352
353 if query_lower.contains("summarize")
354 || query_lower.contains("summary")
355 || query_lower.contains("drift")
356 {
357 let filters = self.extract_filters(query);
358 return Ok(ContractDiffIntent::SummarizeDrift { filters });
359 }
360
361 if query_lower.contains("breaking") || query_lower.contains("breaking change") {
362 let filters = self.extract_filters(query);
363 return Ok(ContractDiffIntent::FindBreakingChanges { filters });
364 }
365
366 Ok(ContractDiffIntent::Unknown)
367 }
368
369 fn extract_request_id(&self, query: &str) -> Option<String> {
371 for word in query.split_whitespace() {
373 if word.len() > 10 {
374 return Some(word.to_string());
376 }
377 }
378 None
379 }
380
381 fn extract_spec_paths(&self, query: &str) -> (Option<String>, Option<String>) {
383 let words: Vec<&str> = query.split_whitespace().collect();
385 let mut paths = Vec::new();
386
387 for word in words {
388 if word.ends_with(".yaml")
389 || word.ends_with(".yml")
390 || word.ends_with(".json")
391 || word.starts_with("http")
392 {
393 paths.push(word.to_string());
394 }
395 }
396
397 match paths.len() {
398 0 => (None, None),
399 1 => (Some(paths[0].clone()), None),
400 _ => (Some(paths[0].clone()), Some(paths[1].clone())),
401 }
402 }
403
404 fn extract_filters(&self, query: &str) -> ContractDiffFilters {
406 let query_lower = query.to_lowercase();
407 ContractDiffFilters {
408 breaking_only: query_lower.contains("breaking")
409 || query_lower.contains("breaking change"),
410 endpoint_filter: if query_lower.contains("mobile") {
411 Some("mobile".to_string())
412 } else if query_lower.contains("api") {
413 Some("api".to_string())
414 } else {
415 None
416 },
417 }
418 }
419}
420
421impl Default for ContractDiffHandler {
422 fn default() -> Self {
423 Self::new().expect("Failed to create ContractDiffHandler")
424 }
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub enum ContractDiffIntent {
430 AnalyzeRequest {
432 request_id: Option<String>,
434 filters: ContractDiffFilters,
436 },
437 CompareVersions {
439 spec1_path: Option<String>,
441 spec2_path: Option<String>,
443 filters: ContractDiffFilters,
445 },
446 SummarizeDrift {
448 filters: ContractDiffFilters,
450 },
451 FindBreakingChanges {
453 filters: ContractDiffFilters,
455 },
456 Unknown,
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize, Default)]
462pub struct ContractDiffFilters {
463 pub breaking_only: bool,
465 pub endpoint_filter: Option<String>,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct ContractDiffQueryResult {
472 pub intent: ContractDiffIntent,
474 #[serde(skip_serializing_if = "Option::is_none")]
476 pub result: Option<ContractDiffResult>,
477 pub summary: String,
479 pub breaking_changes: Vec<BreakingChange>,
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub link_to_viewer: Option<String>,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct BreakingChange {
489 pub path: String,
491 pub method: Option<String>,
493 pub description: String,
495 pub impact: String,
497}