1use std::collections::{HashMap, HashSet};
7
8use crate::composite::{BackendRole, CompositeConfig, QueryAnalyzer, QueryFeature, QueryRouter};
9use crate::core::{BackendCapability, BackendKind};
10use crate::types::SearchQuery;
11
12pub struct ConfigurationAnalyzer {
14 query_analyzer: QueryAnalyzer,
16}
17
18impl ConfigurationAnalyzer {
19 pub fn new() -> Self {
21 Self {
22 query_analyzer: QueryAnalyzer::new(),
23 }
24 }
25
26 pub fn analyze(&self, config: &CompositeConfig) -> AnalysisResult {
28 let capability_coverage = self.analyze_capability_coverage(config);
29 let gap_analysis = self.analyze_gaps(config, &capability_coverage);
30 let redundancy_report = self.analyze_redundancy(config);
31 let issues = self.find_issues(config, &capability_coverage, &redundancy_report);
32 let recommendations = self.generate_recommendations(config, &issues, &gap_analysis);
33
34 AnalysisResult {
35 is_valid: issues.iter().all(|i| i.severity != IssueSeverity::Error),
36 capability_coverage,
37 gap_analysis,
38 redundancy_report,
39 issues,
40 recommendations,
41 }
42 }
43
44 pub fn validate(&self, config: &CompositeConfig) -> ValidationResult {
46 let mut errors = Vec::new();
47 let mut warnings = Vec::new();
48
49 let primaries: Vec<_> = config
51 .backends
52 .iter()
53 .filter(|b| b.role == BackendRole::Primary)
54 .collect();
55
56 if primaries.is_empty() {
57 errors.push("Configuration must have exactly one primary backend".to_string());
58 } else if primaries.len() > 1 {
59 errors.push(format!(
60 "Configuration has {} primary backends (expected 1)",
61 primaries.len()
62 ));
63 }
64
65 let mut seen_ids = HashSet::new();
67 for backend in &config.backends {
68 if !seen_ids.insert(&backend.id) {
69 errors.push(format!("Duplicate backend ID: {}", backend.id));
70 }
71 }
72
73 let backend_ids: HashSet<_> = config.backends.iter().map(|b| &b.id).collect();
75 for backend in &config.backends {
76 if let Some(ref failover) = backend.failover_to {
77 if !backend_ids.contains(failover) {
78 errors.push(format!(
79 "Backend '{}' has invalid failover target: '{}'",
80 backend.id, failover
81 ));
82 }
83 }
84 }
85
86 if let Some(cycle) = self.find_failover_cycle(config) {
88 errors.push(format!("Circular failover chain detected: {}", cycle));
89 }
90
91 if config.backends.iter().filter(|b| b.enabled).count() == 1 {
93 warnings.push("Only one backend enabled - no redundancy".to_string());
94 }
95
96 for backend in &config.backends {
98 if let Some(ref failover) = backend.failover_to {
99 if let Some(target) = config.backends.iter().find(|b| &b.id == failover) {
100 if !target.enabled {
101 warnings.push(format!(
102 "Failover target '{}' for backend '{}' is disabled",
103 failover, backend.id
104 ));
105 }
106 }
107 }
108 }
109
110 ValidationResult {
111 is_valid: errors.is_empty(),
112 errors,
113 warnings,
114 }
115 }
116
117 pub fn simulate_query(&self, query: &SearchQuery, config: &CompositeConfig) -> QuerySimulation {
119 let analysis = self.query_analyzer.analyze(query);
120 let router = QueryRouter::new(config.clone());
121 let routing = router.route(query);
122
123 let estimated_cost = match &routing {
124 Ok(decision) => {
125 let base_cost = match decision.primary_target.as_str() {
127 t if config
128 .backends
129 .iter()
130 .find(|b| b.id == t)
131 .map(|b| b.kind == BackendKind::Sqlite)
132 .unwrap_or(false) =>
133 {
134 1.0
135 }
136 t if config
137 .backends
138 .iter()
139 .find(|b| b.id == t)
140 .map(|b| b.kind == BackendKind::Elasticsearch)
141 .unwrap_or(false) =>
142 {
143 2.0
144 }
145 _ => 1.5,
146 };
147 base_cost * (1.0 + analysis.complexity_score as f64 * 0.1)
148 }
149 Err(_) => 10.0, };
151
152 QuerySimulation {
153 query_features: analysis.features.iter().cloned().collect(),
154 complexity_score: analysis.complexity_score,
155 routing_decision: routing.as_ref().map(|d| d.primary_target.clone()).ok(),
156 auxiliary_targets: routing
157 .as_ref()
158 .map(|d| d.auxiliary_targets.values().cloned().collect())
159 .unwrap_or_default(),
160 estimated_cost,
161 routing_error: routing.as_ref().err().map(|e| format!("{:?}", e)),
162 }
163 }
164
165 fn analyze_capability_coverage(&self, config: &CompositeConfig) -> CapabilityCoverage {
167 let mut covered_capabilities = HashSet::new();
168 let mut capability_backends: HashMap<BackendCapability, Vec<String>> = HashMap::new();
169
170 for backend in &config.backends {
171 if !backend.enabled {
172 continue;
173 }
174
175 let capabilities = backend.effective_capabilities();
176 for cap in capabilities {
177 covered_capabilities.insert(cap);
178 capability_backends
179 .entry(cap)
180 .or_default()
181 .push(backend.id.clone());
182 }
183 }
184
185 let all_capabilities: HashSet<_> = [
187 BackendCapability::Crud,
188 BackendCapability::Versioning,
189 BackendCapability::BasicSearch,
190 BackendCapability::InstanceHistory,
191 BackendCapability::TypeHistory,
192 BackendCapability::Transactions,
193 BackendCapability::ChainedSearch,
194 BackendCapability::FullTextSearch,
195 BackendCapability::TerminologySearch,
196 BackendCapability::Include,
197 BackendCapability::Revinclude,
198 ]
199 .into_iter()
200 .collect();
201
202 let missing_capabilities: HashSet<_> = all_capabilities
203 .difference(&covered_capabilities)
204 .cloned()
205 .collect();
206
207 CapabilityCoverage {
208 covered: covered_capabilities,
209 missing: missing_capabilities,
210 capability_backends,
211 coverage_percentage: 0.0, }
213 }
214
215 fn analyze_gaps(
217 &self,
218 _config: &CompositeConfig,
219 coverage: &CapabilityCoverage,
220 ) -> GapAnalysis {
221 let mut feature_gaps = Vec::new();
222
223 let critical = [BackendCapability::Crud, BackendCapability::BasicSearch];
225
226 for cap in critical {
227 if coverage.missing.contains(&cap) {
228 feature_gaps.push(FeatureGap {
229 capability: cap,
230 impact: GapImpact::High,
231 suggestion: format!("Add a backend that supports {:?}", cap),
232 });
233 }
234 }
235
236 let advanced = [
238 BackendCapability::ChainedSearch,
239 BackendCapability::FullTextSearch,
240 BackendCapability::TerminologySearch,
241 ];
242
243 for cap in advanced {
244 if coverage.missing.contains(&cap) {
245 feature_gaps.push(FeatureGap {
246 capability: cap,
247 impact: GapImpact::Medium,
248 suggestion: format!("Consider adding a specialized backend for {:?}", cap),
249 });
250 }
251 }
252
253 let total_features = critical.len() + advanced.len();
255 let covered_features = total_features - feature_gaps.len();
256 let completeness = covered_features as f64 / total_features as f64;
257
258 GapAnalysis {
259 feature_gaps,
260 completeness_score: completeness,
261 recommendations: Vec::new(), }
263 }
264
265 fn analyze_redundancy(&self, config: &CompositeConfig) -> RedundancyReport {
267 let mut overlapping_capabilities: HashMap<BackendCapability, Vec<String>> = HashMap::new();
268 let mut redundant_backends = Vec::new();
269
270 for backend in &config.backends {
272 if !backend.enabled {
273 continue;
274 }
275
276 let capabilities = backend.effective_capabilities();
277 for cap in capabilities {
278 overlapping_capabilities
279 .entry(cap)
280 .or_default()
281 .push(backend.id.clone());
282 }
283 }
284
285 overlapping_capabilities.retain(|_, v| v.len() > 1);
287
288 for backend in &config.backends {
290 if !backend.enabled || backend.role == BackendRole::Primary {
291 continue;
292 }
293
294 let capabilities = backend.effective_capabilities();
295 let all_covered = capabilities.iter().all(|cap| {
296 overlapping_capabilities
297 .get(cap)
298 .map(|backends| backends.iter().any(|b| b != &backend.id))
299 .unwrap_or(false)
300 });
301
302 if all_covered && !capabilities.is_empty() {
303 redundant_backends.push(RedundantBackend {
304 backend_id: backend.id.clone(),
305 covered_by: overlapping_capabilities
306 .values()
307 .flatten()
308 .filter(|b| *b != &backend.id)
309 .cloned()
310 .collect::<HashSet<_>>()
311 .into_iter()
312 .collect(),
313 reason: "All capabilities provided by other backends".to_string(),
314 });
315 }
316 }
317
318 let total_capability_assignments: usize =
320 overlapping_capabilities.values().map(|v| v.len()).sum();
321 let unique_capabilities = overlapping_capabilities.len();
322 let redundancy_score = if unique_capabilities > 0 {
323 (total_capability_assignments - unique_capabilities) as f64
324 / total_capability_assignments as f64
325 } else {
326 0.0
327 };
328
329 RedundancyReport {
330 overlapping_capabilities,
331 redundant_backends,
332 redundancy_score,
333 }
334 }
335
336 fn find_issues(
338 &self,
339 config: &CompositeConfig,
340 coverage: &CapabilityCoverage,
341 redundancy: &RedundancyReport,
342 ) -> Vec<ConfigurationIssue> {
343 let mut issues = Vec::new();
344
345 let primary_count = config
347 .backends
348 .iter()
349 .filter(|b| b.role == BackendRole::Primary)
350 .count();
351
352 if primary_count == 0 {
353 issues.push(ConfigurationIssue {
354 severity: IssueSeverity::Error,
355 category: IssueCategory::MissingRequirement,
356 message: "No primary backend configured".to_string(),
357 suggestion: Some("Add a backend with role Primary".to_string()),
358 });
359 } else if primary_count > 1 {
360 issues.push(ConfigurationIssue {
361 severity: IssueSeverity::Error,
362 category: IssueCategory::Configuration,
363 message: "Multiple primary backends configured".to_string(),
364 suggestion: Some("Only one backend should have role Primary".to_string()),
365 });
366 }
367
368 for cap in &coverage.missing {
370 let severity = match cap {
371 BackendCapability::Crud => IssueSeverity::Error,
372 BackendCapability::BasicSearch => IssueSeverity::Warning,
373 _ => IssueSeverity::Info,
374 };
375
376 issues.push(ConfigurationIssue {
377 severity,
378 category: IssueCategory::MissingCapability,
379 message: format!("Missing capability: {:?}", cap),
380 suggestion: Some(format!("Add a backend that supports {:?}", cap)),
381 });
382 }
383
384 if redundancy.redundancy_score > 0.5 {
386 issues.push(ConfigurationIssue {
387 severity: IssueSeverity::Warning,
388 category: IssueCategory::Redundancy,
389 message: format!(
390 "High redundancy detected ({:.0}%)",
391 redundancy.redundancy_score * 100.0
392 ),
393 suggestion: Some("Consider consolidating backends".to_string()),
394 });
395 }
396
397 issues
398 }
399
400 fn generate_recommendations(
402 &self,
403 _config: &CompositeConfig,
404 issues: &[ConfigurationIssue],
405 gap_analysis: &GapAnalysis,
406 ) -> Vec<Recommendation> {
407 let mut recommendations = Vec::new();
408
409 for issue in issues {
411 if let Some(ref suggestion) = issue.suggestion {
412 let priority = match issue.severity {
413 IssueSeverity::Error => RecommendationPriority::Critical,
414 IssueSeverity::Warning => RecommendationPriority::High,
415 IssueSeverity::Info => RecommendationPriority::Medium,
416 };
417
418 recommendations.push(Recommendation {
419 priority,
420 title: format!("Fix: {}", issue.message),
421 description: suggestion.clone(),
422 impact: format!("Resolves {:?} issue", issue.category),
423 });
424 }
425 }
426
427 for gap in &gap_analysis.feature_gaps {
429 recommendations.push(Recommendation {
430 priority: match gap.impact {
431 GapImpact::High => RecommendationPriority::High,
432 GapImpact::Medium => RecommendationPriority::Medium,
433 GapImpact::Low => RecommendationPriority::Low,
434 },
435 title: format!("Add support for {:?}", gap.capability),
436 description: gap.suggestion.clone(),
437 impact: format!("Enables {:?} operations", gap.capability),
438 });
439 }
440
441 recommendations
442 }
443
444 fn find_failover_cycle(&self, config: &CompositeConfig) -> Option<String> {
446 for backend in &config.backends {
447 let mut visited = HashSet::new();
448 let mut current = backend.id.clone();
449
450 while let Some(next) = config
451 .backends
452 .iter()
453 .find(|b| b.id == current)
454 .and_then(|b| b.failover_to.clone())
455 {
456 if !visited.insert(current.clone()) {
457 return Some(format!("{} -> {}", current, next));
458 }
459 current = next;
460 }
461 }
462 None
463 }
464}
465
466impl Default for ConfigurationAnalyzer {
467 fn default() -> Self {
468 Self::new()
469 }
470}
471
472#[derive(Debug, Clone)]
474pub struct AnalysisResult {
475 pub is_valid: bool,
477
478 pub capability_coverage: CapabilityCoverage,
480
481 pub gap_analysis: GapAnalysis,
483
484 pub redundancy_report: RedundancyReport,
486
487 pub issues: Vec<ConfigurationIssue>,
489
490 pub recommendations: Vec<Recommendation>,
492}
493
494#[derive(Debug, Clone)]
496pub struct ValidationResult {
497 pub is_valid: bool,
499
500 pub errors: Vec<String>,
502
503 pub warnings: Vec<String>,
505}
506
507#[derive(Debug, Clone)]
509pub struct CapabilityCoverage {
510 pub covered: HashSet<BackendCapability>,
512
513 pub missing: HashSet<BackendCapability>,
515
516 pub capability_backends: HashMap<BackendCapability, Vec<String>>,
518
519 pub coverage_percentage: f64,
521}
522
523#[derive(Debug, Clone)]
525pub struct GapAnalysis {
526 pub feature_gaps: Vec<FeatureGap>,
528
529 pub completeness_score: f64,
531
532 pub recommendations: Vec<String>,
534}
535
536#[derive(Debug, Clone)]
538pub struct FeatureGap {
539 pub capability: BackendCapability,
541
542 pub impact: GapImpact,
544
545 pub suggestion: String,
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
551pub enum GapImpact {
552 High,
554 Medium,
556 Low,
558}
559
560#[derive(Debug, Clone)]
562pub struct RedundancyReport {
563 pub overlapping_capabilities: HashMap<BackendCapability, Vec<String>>,
565
566 pub redundant_backends: Vec<RedundantBackend>,
568
569 pub redundancy_score: f64,
571}
572
573#[derive(Debug, Clone)]
575pub struct RedundantBackend {
576 pub backend_id: String,
578
579 pub covered_by: Vec<String>,
581
582 pub reason: String,
584}
585
586#[derive(Debug, Clone)]
588pub struct ConfigurationIssue {
589 pub severity: IssueSeverity,
591
592 pub category: IssueCategory,
594
595 pub message: String,
597
598 pub suggestion: Option<String>,
600}
601
602#[derive(Debug, Clone, Copy, PartialEq, Eq)]
604pub enum IssueSeverity {
605 Error,
607 Warning,
609 Info,
611}
612
613#[derive(Debug, Clone, Copy, PartialEq, Eq)]
615pub enum IssueCategory {
616 MissingRequirement,
618 MissingCapability,
620 Configuration,
622 Redundancy,
624}
625
626#[derive(Debug, Clone)]
628pub struct Recommendation {
629 pub priority: RecommendationPriority,
631
632 pub title: String,
634
635 pub description: String,
637
638 pub impact: String,
640}
641
642#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
644pub enum RecommendationPriority {
645 Critical,
647 High,
649 Medium,
651 Low,
653}
654
655#[derive(Debug, Clone)]
657pub struct QuerySimulation {
658 pub query_features: Vec<QueryFeature>,
660
661 pub complexity_score: u8,
663
664 pub routing_decision: Option<String>,
666
667 pub auxiliary_targets: Vec<String>,
669
670 pub estimated_cost: f64,
672
673 pub routing_error: Option<String>,
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use crate::composite::{BackendEntry, CompositeConfigBuilder};
681
682 #[test]
683 fn test_analyzer_creation() {
684 let analyzer = ConfigurationAnalyzer::new();
685 let _analysis = analyzer
687 .query_analyzer
688 .analyze(&SearchQuery::new("Patient"));
689 }
690
691 #[test]
692 fn test_validation_no_primary() {
693 let analyzer = ConfigurationAnalyzer::new();
694 let config = CompositeConfig {
695 backends: vec![BackendEntry::new(
696 "secondary",
697 BackendRole::Search,
698 BackendKind::Elasticsearch,
699 )],
700 ..Default::default()
701 };
702
703 let result = analyzer.validate(&config);
704 assert!(!result.is_valid);
705 assert!(result.errors.iter().any(|e| e.contains("primary")));
706 }
707
708 #[test]
709 fn test_validation_valid_config() {
710 let analyzer = ConfigurationAnalyzer::new();
711 let config = CompositeConfigBuilder::new()
712 .primary("sqlite", BackendKind::Sqlite)
713 .build()
714 .unwrap();
715
716 let result = analyzer.validate(&config);
717 assert!(result.is_valid);
718 }
719
720 #[test]
721 fn test_analysis_capability_coverage() {
722 let analyzer = ConfigurationAnalyzer::new();
723 let config = CompositeConfigBuilder::new()
724 .primary("sqlite", BackendKind::Sqlite)
725 .search_backend("es", BackendKind::Elasticsearch)
726 .build()
727 .unwrap();
728
729 let result = analyzer.analyze(&config);
730 assert!(!result.capability_coverage.covered.is_empty());
731 }
732
733 #[test]
734 fn test_query_simulation() {
735 let analyzer = ConfigurationAnalyzer::new();
736 let config = CompositeConfigBuilder::new()
737 .primary("sqlite", BackendKind::Sqlite)
738 .build()
739 .unwrap();
740
741 let query = SearchQuery::new("Patient");
742 let simulation = analyzer.simulate_query(&query, &config);
743
744 assert!(simulation.estimated_cost > 0.0);
745 }
746}