mockforge-core 0.3.116

Shared logic for MockForge - routing, validation, latency, proxy
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
//! Contract Diff Handler for processing natural language queries
//!
//! This module provides functionality to process natural language queries about
//! contract diffs, enabling users to ask questions like "show me breaking changes"
//! or "compare the last 3 versions" via the AI Studio chat interface.

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};

/// Contract diff handler for NL queries
pub struct ContractDiffHandler {
    /// LLM client for parsing queries
    #[allow(dead_code)]
    llm_client: LlmClient,
    /// Contract diff analyzer
    analyzer: ContractDiffAnalyzer,
    /// Configuration
    #[allow(dead_code)]
    config: IntelligentBehaviorConfig,
}

impl ContractDiffHandler {
    /// Create a new contract diff handler
    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,
        })
    }

    /// Create with custom configuration
    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,
        })
    }

    /// Analyze a contract diff from a natural language query
    ///
    /// Parses the query to extract:
    /// - Which spec/request to analyze
    /// - What type of analysis to perform
    /// - Any filters (breaking changes only, mobile endpoints, etc.)
    pub async fn analyze_from_query(
        &self,
        query: &str,
        spec: Option<&OpenApiSpec>,
        captured_request: Option<CapturedRequest>,
    ) -> Result<ContractDiffQueryResult> {
        // Parse the query to understand intent
        let intent = self.parse_query_intent(query).await?;

        match intent {
            ContractDiffIntent::AnalyzeRequest { request_id, filters } => {
                // Analyze a specific captured request
                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 } => {
                // Compare two contract versions
                // This would require loading both specs
                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 } => {
                // Summarize contract drift
                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 } => {
                // Find breaking changes
                if let Some(_spec) = spec {
                    // This is a simplified version - in practice, you'd compare against a previous version
                    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,
                })
            }
        }
    }

    /// Compare two contract versions using ContractValidator
    pub async fn compare_versions(
        &self,
        spec1: &OpenApiSpec,
        spec2: &OpenApiSpec,
    ) -> Result<ContractDiffResult> {
        let validator = ContractValidator::new();
        let validation_result = validator.compare_specs(spec1, spec2);

        // Convert ValidationResult into ContractDiffResult
        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,
            },
        })
    }

    /// Summarize contract drift
    ///
    /// Generates a human-readable summary of contract drift based on recent analyses.
    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
        );

        // Apply filters
        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)
    }

    /// Find breaking changes in contract diff results
    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()
    }

    /// Extract breaking changes from result
    fn extract_breaking_changes(&self, result: &ContractDiffResult) -> Vec<BreakingChange> {
        self.find_breaking_changes(result)
    }

    /// Generate a summary from contract diff 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)
    }

    /// Parse query intent from natural language
    async fn parse_query_intent(&self, query: &str) -> Result<ContractDiffIntent> {
        let query_lower = query.to_lowercase();

        // Simple keyword-based intent detection (can be enhanced with LLM)
        if query_lower.contains("analyze") || query_lower.contains("check") {
            // Extract request ID if mentioned
            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)
    }

    /// Extract request ID from query (simple pattern matching)
    fn extract_request_id(&self, query: &str) -> Option<String> {
        // Look for patterns like "request id: abc123" or "request abc123"
        for word in query.split_whitespace() {
            if word.len() > 10 {
                // Likely a UUID or request ID
                return Some(word.to_string());
            }
        }
        None
    }

    /// Extract spec paths from query
    fn extract_spec_paths(&self, query: &str) -> (Option<String>, Option<String>) {
        // Simple extraction - look for file paths or URLs
        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())),
        }
    }

    /// Extract filters from query
    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")
    }
}

/// Intent detected from natural language query
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ContractDiffIntent {
    /// Analyze a specific request
    AnalyzeRequest {
        /// Optional request ID
        request_id: Option<String>,
        /// Filters to apply
        filters: ContractDiffFilters,
    },
    /// Compare two contract versions
    CompareVersions {
        /// Path to first spec
        spec1_path: Option<String>,
        /// Path to second spec
        spec2_path: Option<String>,
        /// Filters to apply
        filters: ContractDiffFilters,
    },
    /// Summarize contract drift
    SummarizeDrift {
        /// Filters to apply
        filters: ContractDiffFilters,
    },
    /// Find breaking changes
    FindBreakingChanges {
        /// Filters to apply
        filters: ContractDiffFilters,
    },
    /// Unknown intent
    Unknown,
}

/// Filters for contract diff queries
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContractDiffFilters {
    /// Show only breaking changes
    pub breaking_only: bool,
    /// Filter by endpoint pattern
    pub endpoint_filter: Option<String>,
}

/// Result of a contract diff query
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractDiffQueryResult {
    /// Detected intent
    pub intent: ContractDiffIntent,
    /// Contract diff result (if analysis was performed)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<ContractDiffResult>,
    /// Human-readable summary
    pub summary: String,
    /// Breaking changes found
    pub breaking_changes: Vec<BreakingChange>,
    /// Link to Contract Diff Viewer page
    #[serde(skip_serializing_if = "Option::is_none")]
    pub link_to_viewer: Option<String>,
}

/// Breaking change information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BreakingChange {
    /// Path/endpoint affected
    pub path: String,
    /// HTTP method (if applicable)
    pub method: Option<String>,
    /// Description of the breaking change
    pub description: String,
    /// Impact assessment
    pub impact: String,
}