Skip to main content

helios_persistence/advisor/
handlers.rs

1//! HTTP API handlers for the configuration advisor.
2//!
3//! This module provides request/response types and handler logic for
4//! the advisor HTTP API endpoints.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::composite::{BackendRole, CompositeConfig};
11use crate::core::{BackendCapability, BackendKind};
12
13use super::analysis::{
14    AnalysisResult, ConfigurationAnalyzer, ConfigurationIssue, GapImpact, IssueSeverity,
15    Recommendation, RecommendationPriority,
16};
17use super::suggestions::{
18    OptimizationSuggestion, SuggestionCategory, SuggestionEngine, SuggestionPriority,
19    WorkloadPattern,
20};
21
22// ============================================================================
23// Request/Response Types
24// ============================================================================
25
26/// Request to analyze a configuration.
27#[derive(Debug, Clone, Deserialize)]
28pub struct AnalyzeRequest {
29    /// Configuration to analyze.
30    pub config: ConfigurationInput,
31}
32
33/// Response from configuration analysis.
34#[derive(Debug, Clone, Serialize)]
35pub struct AnalyzeResponse {
36    /// Whether the analysis was successful.
37    pub success: bool,
38
39    /// Whether the configuration is valid.
40    pub is_valid: bool,
41
42    /// Issues found.
43    pub issues: Vec<IssueOutput>,
44
45    /// Recommendations.
46    pub recommendations: Vec<RecommendationOutput>,
47
48    /// Capability coverage summary.
49    pub capability_coverage: CapabilityCoverageOutput,
50
51    /// Gap analysis summary.
52    pub gap_summary: GapSummaryOutput,
53}
54
55/// Request to validate a configuration.
56#[derive(Debug, Clone, Deserialize)]
57pub struct ValidateRequest {
58    /// Configuration to validate.
59    pub config: ConfigurationInput,
60}
61
62/// Response from configuration validation.
63#[derive(Debug, Clone, Serialize)]
64pub struct ValidateResponse {
65    /// Whether the configuration is valid.
66    pub is_valid: bool,
67
68    /// Validation errors.
69    pub errors: Vec<String>,
70
71    /// Validation warnings.
72    pub warnings: Vec<String>,
73}
74
75/// Request for optimization suggestions.
76#[derive(Debug, Clone, Deserialize)]
77pub struct SuggestRequest {
78    /// Current configuration.
79    pub config: ConfigurationInput,
80
81    /// Workload characteristics.
82    pub workload: WorkloadInput,
83}
84
85/// Response with optimization suggestions.
86#[derive(Debug, Clone, Serialize)]
87pub struct SuggestResponse {
88    /// Suggestions for improvement.
89    pub suggestions: Vec<SuggestionOutput>,
90
91    /// Summary of current configuration.
92    pub current_summary: ConfigSummaryOutput,
93}
94
95/// Request to simulate query routing.
96#[derive(Debug, Clone, Deserialize)]
97pub struct SimulateRequest {
98    /// Configuration to simulate with.
99    pub config: ConfigurationInput,
100
101    /// Query to simulate.
102    pub query: QueryInput,
103}
104
105/// Response from query simulation.
106#[derive(Debug, Clone, Serialize)]
107pub struct SimulateResponse {
108    /// Detected query features.
109    pub features: Vec<String>,
110
111    /// Query complexity score.
112    pub complexity: u8,
113
114    /// Routing decision.
115    pub routing: RoutingOutput,
116
117    /// Estimated cost.
118    pub estimated_cost: f64,
119}
120
121// ============================================================================
122// Input Types (for deserialization)
123// ============================================================================
124
125/// Configuration input for API requests.
126#[derive(Debug, Clone, Deserialize)]
127pub struct ConfigurationInput {
128    /// Backends in the configuration.
129    pub backends: Vec<BackendInput>,
130
131    /// Sync mode (optional).
132    #[serde(default)]
133    pub sync_mode: Option<String>,
134}
135
136/// Backend input for API requests.
137#[derive(Debug, Clone, Deserialize)]
138pub struct BackendInput {
139    /// Backend identifier.
140    pub id: String,
141
142    /// Backend role.
143    pub role: String,
144
145    /// Backend kind.
146    pub kind: String,
147
148    /// Whether the backend is enabled.
149    #[serde(default = "default_true")]
150    pub enabled: bool,
151
152    /// Custom capabilities (optional).
153    #[serde(default)]
154    pub capabilities: Vec<String>,
155
156    /// Failover target (optional).
157    #[serde(default)]
158    pub failover_to: Option<String>,
159}
160
161fn default_true() -> bool {
162    true
163}
164
165/// Workload input for API requests.
166#[derive(Debug, Clone, Deserialize)]
167pub struct WorkloadInput {
168    /// Ratio of read operations.
169    #[serde(default = "default_read_ratio")]
170    pub read_ratio: f64,
171
172    /// Ratio of write operations.
173    #[serde(default = "default_write_ratio")]
174    pub write_ratio: f64,
175
176    /// Ratio of full-text search.
177    #[serde(default)]
178    pub fulltext_search_ratio: f64,
179
180    /// Ratio of chained search.
181    #[serde(default)]
182    pub chained_search_ratio: f64,
183
184    /// Ratio of terminology search.
185    #[serde(default)]
186    pub terminology_search_ratio: f64,
187
188    /// Estimated data size in GB.
189    #[serde(default = "default_data_size")]
190    pub estimated_data_size_gb: f64,
191
192    /// Queries per day.
193    #[serde(default = "default_queries")]
194    pub queries_per_day: u64,
195
196    /// Peak concurrent users.
197    #[serde(default = "default_users")]
198    pub concurrent_users: u64,
199}
200
201fn default_read_ratio() -> f64 {
202    0.8
203}
204fn default_write_ratio() -> f64 {
205    0.2
206}
207fn default_data_size() -> f64 {
208    10.0
209}
210fn default_queries() -> u64 {
211    1000
212}
213fn default_users() -> u64 {
214    10
215}
216
217/// Query input for simulation.
218#[derive(Debug, Clone, Deserialize)]
219pub struct QueryInput {
220    /// Resource type.
221    pub resource_type: String,
222
223    /// Search parameters.
224    #[serde(default)]
225    pub parameters: Vec<ParameterInput>,
226}
227
228/// Parameter input for query simulation.
229#[derive(Debug, Clone, Deserialize)]
230pub struct ParameterInput {
231    /// Parameter name.
232    pub name: String,
233
234    /// Parameter value.
235    pub value: String,
236
237    /// Parameter modifier (optional).
238    #[serde(default)]
239    pub modifier: Option<String>,
240}
241
242// ============================================================================
243// Output Types (for serialization)
244// ============================================================================
245
246/// Issue output for API responses.
247#[derive(Debug, Clone, Serialize)]
248pub struct IssueOutput {
249    /// Issue severity.
250    pub severity: String,
251
252    /// Issue category.
253    pub category: String,
254
255    /// Issue message.
256    pub message: String,
257
258    /// Suggestion for fixing.
259    pub suggestion: Option<String>,
260}
261
262/// Recommendation output for API responses.
263#[derive(Debug, Clone, Serialize)]
264pub struct RecommendationOutput {
265    /// Priority level.
266    pub priority: String,
267
268    /// Recommendation title.
269    pub title: String,
270
271    /// Detailed description.
272    pub description: String,
273
274    /// Expected impact.
275    pub impact: String,
276}
277
278/// Capability coverage output for API responses.
279#[derive(Debug, Clone, Serialize)]
280pub struct CapabilityCoverageOutput {
281    /// Covered capabilities.
282    pub covered: Vec<String>,
283
284    /// Missing capabilities.
285    pub missing: Vec<String>,
286
287    /// Coverage by backend.
288    pub by_backend: HashMap<String, Vec<String>>,
289}
290
291/// Gap summary output for API responses.
292#[derive(Debug, Clone, Serialize)]
293pub struct GapSummaryOutput {
294    /// Completeness score (0-100).
295    pub completeness_percent: u8,
296
297    /// High impact gaps.
298    pub high_impact_gaps: Vec<String>,
299
300    /// Medium impact gaps.
301    pub medium_impact_gaps: Vec<String>,
302}
303
304/// Suggestion output for API responses.
305#[derive(Debug, Clone, Serialize)]
306pub struct SuggestionOutput {
307    /// Priority level.
308    pub priority: String,
309
310    /// Suggestion category.
311    pub category: String,
312
313    /// Suggestion title.
314    pub title: String,
315
316    /// Detailed description.
317    pub description: String,
318
319    /// Estimated improvement.
320    pub estimated_improvement: Option<String>,
321
322    /// Implementation guidance.
323    pub implementation: Option<String>,
324}
325
326/// Configuration summary output.
327#[derive(Debug, Clone, Serialize)]
328pub struct ConfigSummaryOutput {
329    /// Number of backends.
330    pub backend_count: usize,
331
332    /// Enabled backends.
333    pub enabled_backends: Vec<String>,
334
335    /// Primary backend.
336    pub primary: Option<String>,
337
338    /// Secondary backends.
339    pub secondaries: Vec<String>,
340}
341
342/// Routing output for simulation responses.
343#[derive(Debug, Clone, Serialize)]
344pub struct RoutingOutput {
345    /// Primary target backend.
346    pub primary_target: Option<String>,
347
348    /// Auxiliary targets.
349    pub auxiliary_targets: Vec<String>,
350
351    /// Routing error (if any).
352    pub error: Option<String>,
353}
354
355// ============================================================================
356// Backend Info
357// ============================================================================
358
359/// Information about a backend type.
360#[derive(Debug, Clone, Serialize)]
361pub struct BackendInfo {
362    /// Backend kind identifier.
363    pub kind: String,
364
365    /// Human-readable name.
366    pub name: String,
367
368    /// Description.
369    pub description: String,
370
371    /// Default capabilities.
372    pub default_capabilities: Vec<String>,
373
374    /// Recommended roles.
375    pub recommended_roles: Vec<String>,
376
377    /// Strengths.
378    pub strengths: Vec<String>,
379
380    /// Weaknesses.
381    pub weaknesses: Vec<String>,
382}
383
384impl BackendInfo {
385    /// Returns info for all supported backend types.
386    pub fn all() -> Vec<BackendInfo> {
387        vec![
388            BackendInfo {
389                kind: "Sqlite".to_string(),
390                name: "SQLite".to_string(),
391                description:
392                    "Embedded SQL database, ideal for development and single-node deployments"
393                        .to_string(),
394                default_capabilities: vec![
395                    "Create",
396                    "Read",
397                    "Update",
398                    "Delete",
399                    "Search",
400                    "History",
401                    "VersionRead",
402                    "Transaction",
403                ]
404                .into_iter()
405                .map(String::from)
406                .collect(),
407                recommended_roles: vec!["Primary"].into_iter().map(String::from).collect(),
408                strengths: vec![
409                    "Zero configuration",
410                    "Embedded (no network)",
411                    "ACID compliant",
412                    "Great for development",
413                ]
414                .into_iter()
415                .map(String::from)
416                .collect(),
417                weaknesses: vec![
418                    "Single writer",
419                    "Limited full-text search",
420                    "Not suitable for high concurrency",
421                ]
422                .into_iter()
423                .map(String::from)
424                .collect(),
425            },
426            BackendInfo {
427                kind: "Postgres".to_string(),
428                name: "PostgreSQL".to_string(),
429                description: "Robust relational database, ideal for production CRUD operations"
430                    .to_string(),
431                default_capabilities: vec![
432                    "Create",
433                    "Read",
434                    "Update",
435                    "Delete",
436                    "Search",
437                    "History",
438                    "VersionRead",
439                    "Transaction",
440                    "ConditionalCreate",
441                    "ConditionalUpdate",
442                    "ConditionalDelete",
443                ]
444                .into_iter()
445                .map(String::from)
446                .collect(),
447                recommended_roles: vec!["Primary"].into_iter().map(String::from).collect(),
448                strengths: vec![
449                    "ACID compliant",
450                    "Concurrent writes",
451                    "Advanced indexing",
452                    "Production-ready",
453                ]
454                .into_iter()
455                .map(String::from)
456                .collect(),
457                weaknesses: vec![
458                    "Requires separate server",
459                    "More complex setup",
460                    "Limited full-text search",
461                ]
462                .into_iter()
463                .map(String::from)
464                .collect(),
465            },
466            BackendInfo {
467                kind: "Elasticsearch".to_string(),
468                name: "Elasticsearch".to_string(),
469                description: "Distributed search engine, ideal for full-text and analytics queries"
470                    .to_string(),
471                default_capabilities: vec!["Read", "Search", "FullTextSearch"]
472                    .into_iter()
473                    .map(String::from)
474                    .collect(),
475                recommended_roles: vec!["Search"].into_iter().map(String::from).collect(),
476                strengths: vec![
477                    "Excellent full-text search",
478                    "Fast analytics",
479                    "Horizontal scaling",
480                    "Rich query DSL",
481                ]
482                .into_iter()
483                .map(String::from)
484                .collect(),
485                weaknesses: vec![
486                    "Eventual consistency",
487                    "Not suitable as primary store",
488                    "Resource intensive",
489                ]
490                .into_iter()
491                .map(String::from)
492                .collect(),
493            },
494            BackendInfo {
495                kind: "Neo4j".to_string(),
496                name: "Neo4j".to_string(),
497                description: "Graph database, ideal for relationship-heavy queries".to_string(),
498                default_capabilities: vec!["Read", "Search", "ChainedSearch"]
499                    .into_iter()
500                    .map(String::from)
501                    .collect(),
502                recommended_roles: vec!["Graph"].into_iter().map(String::from).collect(),
503                strengths: vec![
504                    "Fast graph traversal",
505                    "Natural relationship modeling",
506                    "Excellent for chained searches",
507                ]
508                .into_iter()
509                .map(String::from)
510                .collect(),
511                weaknesses: vec![
512                    "Not suitable as primary store",
513                    "Learning curve for Cypher",
514                    "Can be expensive",
515                ]
516                .into_iter()
517                .map(String::from)
518                .collect(),
519            },
520            BackendInfo {
521                kind: "S3".to_string(),
522                name: "Amazon S3 / Object Storage".to_string(),
523                description: "Object storage, ideal for archival and large data volumes"
524                    .to_string(),
525                default_capabilities: vec!["Create", "Read", "Delete"]
526                    .into_iter()
527                    .map(String::from)
528                    .collect(),
529                recommended_roles: vec!["Archive"].into_iter().map(String::from).collect(),
530                strengths: vec![
531                    "Very low storage cost",
532                    "Virtually unlimited capacity",
533                    "High durability",
534                    "Good for compliance/archival",
535                ]
536                .into_iter()
537                .map(String::from)
538                .collect(),
539                weaknesses: vec![
540                    "No search capability",
541                    "Higher latency",
542                    "No updates (only replace)",
543                ]
544                .into_iter()
545                .map(String::from)
546                .collect(),
547            },
548        ]
549    }
550
551    /// Returns info for a specific backend type.
552    pub fn get(kind: &str) -> Option<BackendInfo> {
553        Self::all()
554            .into_iter()
555            .find(|b| b.kind.eq_ignore_ascii_case(kind))
556    }
557}
558
559// ============================================================================
560// Handler Functions
561// ============================================================================
562
563/// Handles the analyze endpoint.
564pub fn handle_analyze(request: AnalyzeRequest) -> Result<AnalyzeResponse, String> {
565    let config = convert_config(&request.config)?;
566    let analyzer = ConfigurationAnalyzer::new();
567    let result = analyzer.analyze(&config);
568
569    Ok(AnalyzeResponse {
570        success: true,
571        is_valid: result.is_valid,
572        issues: result.issues.iter().map(convert_issue).collect(),
573        recommendations: result
574            .recommendations
575            .iter()
576            .map(convert_recommendation)
577            .collect(),
578        capability_coverage: convert_capability_coverage(&result),
579        gap_summary: convert_gap_summary(&result),
580    })
581}
582
583/// Handles the validate endpoint.
584pub fn handle_validate(request: ValidateRequest) -> Result<ValidateResponse, String> {
585    let config = convert_config(&request.config)?;
586    let analyzer = ConfigurationAnalyzer::new();
587    let result = analyzer.validate(&config);
588
589    Ok(ValidateResponse {
590        is_valid: result.is_valid,
591        errors: result.errors,
592        warnings: result.warnings,
593    })
594}
595
596/// Handles the suggest endpoint.
597pub fn handle_suggest(request: SuggestRequest) -> Result<SuggestResponse, String> {
598    let config = convert_config(&request.config)?;
599    let workload = convert_workload(&request.workload);
600    let engine = SuggestionEngine::new();
601    let suggestions = engine.suggest(&config, &workload);
602
603    Ok(SuggestResponse {
604        suggestions: suggestions.iter().map(convert_suggestion).collect(),
605        current_summary: create_config_summary(&config),
606    })
607}
608
609/// Handles the simulate endpoint.
610pub fn handle_simulate(request: SimulateRequest) -> Result<SimulateResponse, String> {
611    let config = convert_config(&request.config)?;
612    let query = convert_query(&request.query);
613    let analyzer = ConfigurationAnalyzer::new();
614    let simulation = analyzer.simulate_query(&query, &config);
615
616    Ok(SimulateResponse {
617        features: simulation
618            .query_features
619            .iter()
620            .map(|f| format!("{:?}", f))
621            .collect(),
622        complexity: simulation.complexity_score,
623        routing: RoutingOutput {
624            primary_target: simulation.routing_decision,
625            auxiliary_targets: simulation.auxiliary_targets,
626            error: simulation.routing_error,
627        },
628        estimated_cost: simulation.estimated_cost,
629    })
630}
631
632/// Handles the backends endpoint.
633pub fn handle_backends() -> Vec<BackendInfo> {
634    BackendInfo::all()
635}
636
637/// Handles the backend capabilities endpoint.
638pub fn handle_backend_capabilities(kind: &str) -> Result<BackendInfo, String> {
639    BackendInfo::get(kind).ok_or_else(|| format!("Unknown backend kind: {}", kind))
640}
641
642// ============================================================================
643// Conversion Functions
644// ============================================================================
645
646fn convert_config(input: &ConfigurationInput) -> Result<CompositeConfig, String> {
647    use crate::composite::{BackendEntry, CompositeConfigBuilder, SyncMode};
648
649    let mut builder = CompositeConfigBuilder::new();
650
651    for backend in &input.backends {
652        let kind = parse_backend_kind(&backend.kind)?;
653        let role = parse_backend_role(&backend.role)?;
654
655        let mut entry = BackendEntry::new(&backend.id, role, kind);
656        entry.enabled = backend.enabled;
657        entry.failover_to = backend.failover_to.clone();
658
659        if !backend.capabilities.is_empty() {
660            entry.capabilities = backend
661                .capabilities
662                .iter()
663                .filter_map(|c| parse_capability(c).ok())
664                .collect();
665        }
666
667        builder = builder.with_backend(entry);
668    }
669
670    // Set sync mode
671    if let Some(ref mode) = input.sync_mode {
672        let sync_mode = match mode.to_lowercase().as_str() {
673            "synchronous" | "sync" => SyncMode::Synchronous,
674            "asynchronous" | "async" => SyncMode::Asynchronous,
675            "hybrid" => SyncMode::Hybrid {
676                sync_for_search: true,
677            },
678            _ => SyncMode::Asynchronous,
679        };
680        builder = builder.sync_mode(sync_mode);
681    }
682
683    builder
684        .build()
685        .map_err(|e| format!("Invalid configuration: {:?}", e))
686}
687
688fn convert_workload(input: &WorkloadInput) -> WorkloadPattern {
689    WorkloadPattern {
690        read_ratio: input.read_ratio,
691        write_ratio: input.write_ratio,
692        fulltext_search_ratio: input.fulltext_search_ratio,
693        chained_search_ratio: input.chained_search_ratio,
694        terminology_search_ratio: input.terminology_search_ratio,
695        estimated_data_size_gb: input.estimated_data_size_gb,
696        queries_per_day: input.queries_per_day,
697        concurrent_users: input.concurrent_users,
698        ..Default::default()
699    }
700}
701
702fn convert_query(input: &QueryInput) -> crate::types::SearchQuery {
703    use crate::types::{SearchModifier, SearchParameter, SearchQuery, SearchValue};
704
705    let mut query = SearchQuery::new(&input.resource_type);
706
707    for param in &input.parameters {
708        let modifier = param
709            .modifier
710            .as_ref()
711            .and_then(|m| match m.to_lowercase().as_str() {
712                "exact" => Some(SearchModifier::Exact),
713                "contains" => Some(SearchModifier::Contains),
714                "text" => Some(SearchModifier::Text),
715                "not" => Some(SearchModifier::Not),
716                "missing" => Some(SearchModifier::Missing),
717                "above" => Some(SearchModifier::Above),
718                "below" => Some(SearchModifier::Below),
719                "in" => Some(SearchModifier::In),
720                "not-in" | "notin" => Some(SearchModifier::NotIn),
721                "identifier" => Some(SearchModifier::Identifier),
722                "oftype" | "of-type" => Some(SearchModifier::OfType),
723                "iterate" => Some(SearchModifier::Iterate),
724                _ => None,
725            });
726
727        let search_param = SearchParameter {
728            name: param.name.clone(),
729            values: vec![SearchValue::eq(&param.value)],
730            modifier,
731            ..Default::default()
732        };
733        query.parameters.push(search_param);
734    }
735
736    query
737}
738
739fn convert_issue(issue: &ConfigurationIssue) -> IssueOutput {
740    IssueOutput {
741        severity: match issue.severity {
742            IssueSeverity::Error => "error".to_string(),
743            IssueSeverity::Warning => "warning".to_string(),
744            IssueSeverity::Info => "info".to_string(),
745        },
746        category: format!("{:?}", issue.category),
747        message: issue.message.clone(),
748        suggestion: issue.suggestion.clone(),
749    }
750}
751
752fn convert_recommendation(rec: &Recommendation) -> RecommendationOutput {
753    RecommendationOutput {
754        priority: match rec.priority {
755            RecommendationPriority::Critical => "critical".to_string(),
756            RecommendationPriority::High => "high".to_string(),
757            RecommendationPriority::Medium => "medium".to_string(),
758            RecommendationPriority::Low => "low".to_string(),
759        },
760        title: rec.title.clone(),
761        description: rec.description.clone(),
762        impact: rec.impact.clone(),
763    }
764}
765
766fn convert_suggestion(sug: &OptimizationSuggestion) -> SuggestionOutput {
767    SuggestionOutput {
768        priority: match sug.priority {
769            SuggestionPriority::Critical => "critical".to_string(),
770            SuggestionPriority::High => "high".to_string(),
771            SuggestionPriority::Medium => "medium".to_string(),
772            SuggestionPriority::Low => "low".to_string(),
773        },
774        category: match sug.category {
775            SuggestionCategory::Performance => "performance".to_string(),
776            SuggestionCategory::Scalability => "scalability".to_string(),
777            SuggestionCategory::Cost => "cost".to_string(),
778            SuggestionCategory::Feature => "feature".to_string(),
779            SuggestionCategory::Reliability => "reliability".to_string(),
780        },
781        title: sug.title.clone(),
782        description: sug.description.clone(),
783        estimated_improvement: sug.estimated_improvement.clone(),
784        implementation: sug.implementation.clone(),
785    }
786}
787
788fn convert_capability_coverage(result: &AnalysisResult) -> CapabilityCoverageOutput {
789    CapabilityCoverageOutput {
790        covered: result
791            .capability_coverage
792            .covered
793            .iter()
794            .map(|c| format!("{:?}", c))
795            .collect(),
796        missing: result
797            .capability_coverage
798            .missing
799            .iter()
800            .map(|c| format!("{:?}", c))
801            .collect(),
802        by_backend: result
803            .capability_coverage
804            .capability_backends
805            .iter()
806            .map(|(cap, backends)| (format!("{:?}", cap), backends.clone()))
807            .collect(),
808    }
809}
810
811fn convert_gap_summary(result: &AnalysisResult) -> GapSummaryOutput {
812    let high_impact: Vec<_> = result
813        .gap_analysis
814        .feature_gaps
815        .iter()
816        .filter(|g| g.impact == GapImpact::High)
817        .map(|g| format!("{:?}", g.capability))
818        .collect();
819
820    let medium_impact: Vec<_> = result
821        .gap_analysis
822        .feature_gaps
823        .iter()
824        .filter(|g| g.impact == GapImpact::Medium)
825        .map(|g| format!("{:?}", g.capability))
826        .collect();
827
828    GapSummaryOutput {
829        completeness_percent: (result.gap_analysis.completeness_score * 100.0) as u8,
830        high_impact_gaps: high_impact,
831        medium_impact_gaps: medium_impact,
832    }
833}
834
835fn create_config_summary(config: &CompositeConfig) -> ConfigSummaryOutput {
836    let primary = config
837        .backends
838        .iter()
839        .find(|b| b.role == BackendRole::Primary)
840        .map(|b| b.id.clone());
841
842    let secondaries: Vec<_> = config
843        .backends
844        .iter()
845        .filter(|b| b.role != BackendRole::Primary && b.enabled)
846        .map(|b| b.id.clone())
847        .collect();
848
849    let enabled: Vec<_> = config
850        .backends
851        .iter()
852        .filter(|b| b.enabled)
853        .map(|b| b.id.clone())
854        .collect();
855
856    ConfigSummaryOutput {
857        backend_count: config.backends.len(),
858        enabled_backends: enabled,
859        primary,
860        secondaries,
861    }
862}
863
864fn parse_backend_kind(s: &str) -> Result<BackendKind, String> {
865    match s.to_lowercase().as_str() {
866        "sqlite" => Ok(BackendKind::Sqlite),
867        "postgres" | "postgresql" => Ok(BackendKind::Postgres),
868        "elasticsearch" | "es" => Ok(BackendKind::Elasticsearch),
869        "neo4j" => Ok(BackendKind::Neo4j),
870        "s3" | "objectstore" => Ok(BackendKind::S3),
871        "mongodb" | "mongo" => Ok(BackendKind::MongoDB),
872        "cassandra" => Ok(BackendKind::Cassandra),
873        _ => Err(format!("Unknown backend kind: {}", s)),
874    }
875}
876
877fn parse_backend_role(s: &str) -> Result<BackendRole, String> {
878    match s.to_lowercase().as_str() {
879        "primary" => Ok(BackendRole::Primary),
880        "search" => Ok(BackendRole::Search),
881        "graph" => Ok(BackendRole::Graph),
882        "terminology" => Ok(BackendRole::Terminology),
883        "archive" => Ok(BackendRole::Archive),
884        _ => Err(format!("Unknown backend role: {}", s)),
885    }
886}
887
888fn parse_capability(s: &str) -> Result<BackendCapability, String> {
889    match s.to_lowercase().as_str() {
890        "crud" | "create" | "read" | "update" | "delete" => Ok(BackendCapability::Crud),
891        "basicsearch" | "search" => Ok(BackendCapability::BasicSearch),
892        "versioning" | "versionread" => Ok(BackendCapability::Versioning),
893        "instancehistory" | "history" => Ok(BackendCapability::InstanceHistory),
894        "typehistory" => Ok(BackendCapability::TypeHistory),
895        "systemhistory" => Ok(BackendCapability::SystemHistory),
896        "transactions" | "transaction" => Ok(BackendCapability::Transactions),
897        "chainedsearch" => Ok(BackendCapability::ChainedSearch),
898        "reversechaining" => Ok(BackendCapability::ReverseChaining),
899        "include" => Ok(BackendCapability::Include),
900        "revinclude" => Ok(BackendCapability::Revinclude),
901        "fulltextsearch" => Ok(BackendCapability::FullTextSearch),
902        "terminologysearch" | "terminologyexpansion" => Ok(BackendCapability::TerminologySearch),
903        "datesearch" => Ok(BackendCapability::DateSearch),
904        "quantitysearch" => Ok(BackendCapability::QuantitySearch),
905        "referencesearch" => Ok(BackendCapability::ReferenceSearch),
906        "cursorpagination" => Ok(BackendCapability::CursorPagination),
907        "offsetpagination" => Ok(BackendCapability::OffsetPagination),
908        "sorting" => Ok(BackendCapability::Sorting),
909        "bulkexport" => Ok(BackendCapability::BulkExport),
910        "bulkimport" => Ok(BackendCapability::BulkImport),
911        "optimisticlocking" => Ok(BackendCapability::OptimisticLocking),
912        _ => Err(format!("Unknown capability: {}", s)),
913    }
914}
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919
920    #[test]
921    fn test_handle_backends() {
922        let backends = handle_backends();
923        assert!(!backends.is_empty());
924        assert!(backends.iter().any(|b| b.kind == "Sqlite"));
925    }
926
927    #[test]
928    fn test_handle_backend_capabilities() {
929        let result = handle_backend_capabilities("Sqlite");
930        assert!(result.is_ok());
931        let info = result.unwrap();
932        assert_eq!(info.kind, "Sqlite");
933    }
934
935    #[test]
936    fn test_handle_validate() {
937        let request = ValidateRequest {
938            config: ConfigurationInput {
939                backends: vec![BackendInput {
940                    id: "primary".to_string(),
941                    role: "Primary".to_string(),
942                    kind: "Sqlite".to_string(),
943                    enabled: true,
944                    capabilities: vec![],
945                    failover_to: None,
946                }],
947                sync_mode: None,
948            },
949        };
950
951        let result = handle_validate(request);
952        assert!(result.is_ok());
953        assert!(result.unwrap().is_valid);
954    }
955
956    #[test]
957    fn test_handle_analyze() {
958        let request = AnalyzeRequest {
959            config: ConfigurationInput {
960                backends: vec![BackendInput {
961                    id: "primary".to_string(),
962                    role: "Primary".to_string(),
963                    kind: "Sqlite".to_string(),
964                    enabled: true,
965                    capabilities: vec![],
966                    failover_to: None,
967                }],
968                sync_mode: None,
969            },
970        };
971
972        let result = handle_analyze(request);
973        assert!(result.is_ok());
974        let response = result.unwrap();
975        assert!(response.success);
976    }
977
978    #[test]
979    fn test_handle_suggest() {
980        let request = SuggestRequest {
981            config: ConfigurationInput {
982                backends: vec![BackendInput {
983                    id: "primary".to_string(),
984                    role: "Primary".to_string(),
985                    kind: "Sqlite".to_string(),
986                    enabled: true,
987                    capabilities: vec![],
988                    failover_to: None,
989                }],
990                sync_mode: None,
991            },
992            workload: WorkloadInput {
993                read_ratio: 0.8,
994                write_ratio: 0.2,
995                fulltext_search_ratio: 0.1,
996                chained_search_ratio: 0.05,
997                terminology_search_ratio: 0.02,
998                estimated_data_size_gb: 10.0,
999                queries_per_day: 1000,
1000                concurrent_users: 10,
1001            },
1002        };
1003
1004        let result = handle_suggest(request);
1005        assert!(result.is_ok());
1006    }
1007
1008    #[test]
1009    fn test_handle_simulate() {
1010        let request = SimulateRequest {
1011            config: ConfigurationInput {
1012                backends: vec![BackendInput {
1013                    id: "primary".to_string(),
1014                    role: "Primary".to_string(),
1015                    kind: "Sqlite".to_string(),
1016                    enabled: true,
1017                    capabilities: vec![],
1018                    failover_to: None,
1019                }],
1020                sync_mode: None,
1021            },
1022            query: QueryInput {
1023                resource_type: "Patient".to_string(),
1024                parameters: vec![ParameterInput {
1025                    name: "name".to_string(),
1026                    value: "Smith".to_string(),
1027                    modifier: None,
1028                }],
1029            },
1030        };
1031
1032        let result = handle_simulate(request);
1033        assert!(result.is_ok());
1034        let response = result.unwrap();
1035        assert!(response.estimated_cost > 0.0);
1036    }
1037
1038    #[test]
1039    fn test_parse_backend_kind() {
1040        assert!(parse_backend_kind("Sqlite").is_ok());
1041        assert!(parse_backend_kind("SQLITE").is_ok());
1042        assert!(parse_backend_kind("postgres").is_ok());
1043        assert!(parse_backend_kind("unknown").is_err());
1044    }
1045
1046    #[test]
1047    fn test_parse_backend_role() {
1048        assert!(parse_backend_role("Primary").is_ok());
1049        assert!(parse_backend_role("search").is_ok());
1050        assert!(parse_backend_role("unknown").is_err());
1051    }
1052}