1use 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#[derive(Debug, Clone, Deserialize)]
28pub struct AnalyzeRequest {
29 pub config: ConfigurationInput,
31}
32
33#[derive(Debug, Clone, Serialize)]
35pub struct AnalyzeResponse {
36 pub success: bool,
38
39 pub is_valid: bool,
41
42 pub issues: Vec<IssueOutput>,
44
45 pub recommendations: Vec<RecommendationOutput>,
47
48 pub capability_coverage: CapabilityCoverageOutput,
50
51 pub gap_summary: GapSummaryOutput,
53}
54
55#[derive(Debug, Clone, Deserialize)]
57pub struct ValidateRequest {
58 pub config: ConfigurationInput,
60}
61
62#[derive(Debug, Clone, Serialize)]
64pub struct ValidateResponse {
65 pub is_valid: bool,
67
68 pub errors: Vec<String>,
70
71 pub warnings: Vec<String>,
73}
74
75#[derive(Debug, Clone, Deserialize)]
77pub struct SuggestRequest {
78 pub config: ConfigurationInput,
80
81 pub workload: WorkloadInput,
83}
84
85#[derive(Debug, Clone, Serialize)]
87pub struct SuggestResponse {
88 pub suggestions: Vec<SuggestionOutput>,
90
91 pub current_summary: ConfigSummaryOutput,
93}
94
95#[derive(Debug, Clone, Deserialize)]
97pub struct SimulateRequest {
98 pub config: ConfigurationInput,
100
101 pub query: QueryInput,
103}
104
105#[derive(Debug, Clone, Serialize)]
107pub struct SimulateResponse {
108 pub features: Vec<String>,
110
111 pub complexity: u8,
113
114 pub routing: RoutingOutput,
116
117 pub estimated_cost: f64,
119}
120
121#[derive(Debug, Clone, Deserialize)]
127pub struct ConfigurationInput {
128 pub backends: Vec<BackendInput>,
130
131 #[serde(default)]
133 pub sync_mode: Option<String>,
134}
135
136#[derive(Debug, Clone, Deserialize)]
138pub struct BackendInput {
139 pub id: String,
141
142 pub role: String,
144
145 pub kind: String,
147
148 #[serde(default = "default_true")]
150 pub enabled: bool,
151
152 #[serde(default)]
154 pub capabilities: Vec<String>,
155
156 #[serde(default)]
158 pub failover_to: Option<String>,
159}
160
161fn default_true() -> bool {
162 true
163}
164
165#[derive(Debug, Clone, Deserialize)]
167pub struct WorkloadInput {
168 #[serde(default = "default_read_ratio")]
170 pub read_ratio: f64,
171
172 #[serde(default = "default_write_ratio")]
174 pub write_ratio: f64,
175
176 #[serde(default)]
178 pub fulltext_search_ratio: f64,
179
180 #[serde(default)]
182 pub chained_search_ratio: f64,
183
184 #[serde(default)]
186 pub terminology_search_ratio: f64,
187
188 #[serde(default = "default_data_size")]
190 pub estimated_data_size_gb: f64,
191
192 #[serde(default = "default_queries")]
194 pub queries_per_day: u64,
195
196 #[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#[derive(Debug, Clone, Deserialize)]
219pub struct QueryInput {
220 pub resource_type: String,
222
223 #[serde(default)]
225 pub parameters: Vec<ParameterInput>,
226}
227
228#[derive(Debug, Clone, Deserialize)]
230pub struct ParameterInput {
231 pub name: String,
233
234 pub value: String,
236
237 #[serde(default)]
239 pub modifier: Option<String>,
240}
241
242#[derive(Debug, Clone, Serialize)]
248pub struct IssueOutput {
249 pub severity: String,
251
252 pub category: String,
254
255 pub message: String,
257
258 pub suggestion: Option<String>,
260}
261
262#[derive(Debug, Clone, Serialize)]
264pub struct RecommendationOutput {
265 pub priority: String,
267
268 pub title: String,
270
271 pub description: String,
273
274 pub impact: String,
276}
277
278#[derive(Debug, Clone, Serialize)]
280pub struct CapabilityCoverageOutput {
281 pub covered: Vec<String>,
283
284 pub missing: Vec<String>,
286
287 pub by_backend: HashMap<String, Vec<String>>,
289}
290
291#[derive(Debug, Clone, Serialize)]
293pub struct GapSummaryOutput {
294 pub completeness_percent: u8,
296
297 pub high_impact_gaps: Vec<String>,
299
300 pub medium_impact_gaps: Vec<String>,
302}
303
304#[derive(Debug, Clone, Serialize)]
306pub struct SuggestionOutput {
307 pub priority: String,
309
310 pub category: String,
312
313 pub title: String,
315
316 pub description: String,
318
319 pub estimated_improvement: Option<String>,
321
322 pub implementation: Option<String>,
324}
325
326#[derive(Debug, Clone, Serialize)]
328pub struct ConfigSummaryOutput {
329 pub backend_count: usize,
331
332 pub enabled_backends: Vec<String>,
334
335 pub primary: Option<String>,
337
338 pub secondaries: Vec<String>,
340}
341
342#[derive(Debug, Clone, Serialize)]
344pub struct RoutingOutput {
345 pub primary_target: Option<String>,
347
348 pub auxiliary_targets: Vec<String>,
350
351 pub error: Option<String>,
353}
354
355#[derive(Debug, Clone, Serialize)]
361pub struct BackendInfo {
362 pub kind: String,
364
365 pub name: String,
367
368 pub description: String,
370
371 pub default_capabilities: Vec<String>,
373
374 pub recommended_roles: Vec<String>,
376
377 pub strengths: Vec<String>,
379
380 pub weaknesses: Vec<String>,
382}
383
384impl BackendInfo {
385 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 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
559pub 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
583pub 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
596pub 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
609pub 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
632pub fn handle_backends() -> Vec<BackendInfo> {
634 BackendInfo::all()
635}
636
637pub fn handle_backend_capabilities(kind: &str) -> Result<BackendInfo, String> {
639 BackendInfo::get(kind).ok_or_else(|| format!("Unknown backend kind: {}", kind))
640}
641
642fn 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 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(¶m.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}