use serde::{Deserialize, Serialize}; /// Storage backend type. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum StorageType { Memory, File, } impl Default for StorageType { fn default() -> Self { StorageType::Memory } } /// Top-level configuration for RustDb server. /// Field names use camelCase to match the TypeScript SmartdbServer options. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RustDbOptions { /// TCP port to listen on (default: 27017) #[serde(default = "default_port")] pub port: u16, /// Host/IP to bind to (default: "127.0.0.1") #[serde(default = "default_host")] pub host: String, /// Unix socket path (overrides TCP if set) #[serde(skip_serializing_if = "Option::is_none")] pub socket_path: Option, /// Storage backend type #[serde(default)] pub storage: StorageType, /// Base path for file storage (required when storage = "file") #[serde(skip_serializing_if = "Option::is_none")] pub storage_path: Option, /// Path for periodic persistence of in-memory data #[serde(skip_serializing_if = "Option::is_none")] pub persist_path: Option, /// Interval in ms for periodic persistence (default: 60000) #[serde(default = "default_persist_interval")] pub persist_interval_ms: u64, /// Authentication configuration. #[serde(default)] pub auth: AuthOptions, /// TLS transport configuration for TCP listeners. #[serde(default)] pub tls: TlsOptions, } /// Authentication configuration for the embedded server. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AuthOptions { /// Whether clients must authenticate before issuing protected commands. #[serde(default)] pub enabled: bool, /// Bootstrap users loaded at startup. Passwords are converted into SCRAM credentials in memory. #[serde(default)] pub users: Vec, /// Optional path for persisted SCRAM user metadata. Stores derived credentials, never plaintext passwords. #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub users_path: Option, /// SCRAM iteration count used for bootstrap credentials. #[serde(default = "default_scram_iterations")] pub scram_iterations: u32, } impl Default for AuthOptions { fn default() -> Self { Self { enabled: false, users: Vec::new(), users_path: None, scram_iterations: default_scram_iterations(), } } } /// TLS transport configuration for the embedded server. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TlsOptions { /// Whether TCP client connections must use TLS. #[serde(default)] pub enabled: bool, /// PEM-encoded server certificate chain. #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub cert_path: Option, /// PEM-encoded server private key. #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub key_path: Option, /// PEM-encoded client CA roots for mTLS verification. #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub ca_path: Option, /// Require clients to present a certificate signed by caPath. #[serde(default)] pub require_client_cert: bool, } impl Default for TlsOptions { fn default() -> Self { Self { enabled: false, cert_path: None, key_path: None, ca_path: None, require_client_cert: false, } } } /// A bootstrap user for SCRAM authentication. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AuthUserOptions { pub username: String, pub password: String, #[serde(default = "default_auth_database")] pub database: String, #[serde(default)] pub roles: Vec, } fn default_port() -> u16 { 27017 } fn default_host() -> String { "127.0.0.1".to_string() } fn default_persist_interval() -> u64 { 60000 } fn default_scram_iterations() -> u32 { 15000 } fn default_auth_database() -> String { "admin".to_string() } impl Default for RustDbOptions { fn default() -> Self { Self { port: default_port(), host: default_host(), socket_path: None, storage: StorageType::default(), storage_path: None, persist_path: None, persist_interval_ms: default_persist_interval(), auth: AuthOptions::default(), tls: TlsOptions::default(), } } } impl RustDbOptions { /// Load options from a JSON config file. pub fn from_file(path: &str) -> Result { let content = std::fs::read_to_string(path) .map_err(|e| ConfigError::IoError(e.to_string()))?; let options: Self = serde_json::from_str(&content) .map_err(|e| ConfigError::ParseError(e.to_string()))?; options.validate()?; Ok(options) } /// Validate the configuration. pub fn validate(&self) -> Result<(), ConfigError> { if self.storage == StorageType::File && self.storage_path.is_none() { return Err(ConfigError::ValidationError( "storagePath is required when storage is 'file'".to_string(), )); } if self.auth.enabled { if self.auth.users.is_empty() && self.auth.users_path.is_none() { return Err(ConfigError::ValidationError( "auth.users or auth.usersPath must be set when auth.enabled is true".to_string(), )); } if self.auth.scram_iterations < 4096 { return Err(ConfigError::ValidationError( "auth.scramIterations must be at least 4096".to_string(), )); } for user in &self.auth.users { if user.username.is_empty() { return Err(ConfigError::ValidationError( "auth.users[].username must not be empty".to_string(), )); } if user.password.is_empty() { return Err(ConfigError::ValidationError( format!("auth user '{}' must have a non-empty password", user.username), )); } if user.database.is_empty() { return Err(ConfigError::ValidationError( format!("auth user '{}' must have a non-empty database", user.username), )); } } } if self.tls.enabled { if self.socket_path.is_some() { return Err(ConfigError::ValidationError( "tls.enabled is only supported for TCP listeners".to_string(), )); } if self.tls.cert_path.as_deref().unwrap_or_default().is_empty() { return Err(ConfigError::ValidationError( "tls.certPath is required when tls.enabled is true".to_string(), )); } if self.tls.key_path.as_deref().unwrap_or_default().is_empty() { return Err(ConfigError::ValidationError( "tls.keyPath is required when tls.enabled is true".to_string(), )); } if self.tls.require_client_cert && self.tls.ca_path.as_deref().unwrap_or_default().is_empty() { return Err(ConfigError::ValidationError( "tls.caPath is required when tls.requireClientCert is true".to_string(), )); } } Ok(()) } /// Get the connection URI for this server configuration. pub fn connection_uri(&self) -> String { if let Some(ref socket_path) = self.socket_path { let encoded = urlencoding(socket_path); format!("mongodb://{}", encoded) } else { let base = format!("mongodb://{}:{}", self.host, self.port); if self.tls.enabled { format!("{}/?tls=true", base) } else { base } } } } /// Simple URL encoding for socket paths (encode / as %2F, etc.) fn urlencoding(s: &str) -> String { s.chars() .map(|c| match c { '/' => "%2F".to_string(), ':' => "%3A".to_string(), ' ' => "%20".to_string(), _ => c.to_string(), }) .collect() } /// Configuration errors. #[derive(Debug, thiserror::Error)] pub enum ConfigError { #[error("IO error: {0}")] IoError(String), #[error("Parse error: {0}")] ParseError(String), #[error("Validation error: {0}")] ValidationError(String), } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_options() { let opts = RustDbOptions::default(); assert_eq!(opts.port, 27017); assert_eq!(opts.host, "127.0.0.1"); assert!(opts.socket_path.is_none()); assert_eq!(opts.storage, StorageType::Memory); } #[test] fn test_deserialize_from_json() { let json = r#"{"port": 27018, "storage": "file", "storagePath": "./data"}"#; let opts: RustDbOptions = serde_json::from_str(json).unwrap(); assert_eq!(opts.port, 27018); assert_eq!(opts.storage, StorageType::File); assert_eq!(opts.storage_path, Some("./data".to_string())); } #[test] fn test_connection_uri_tcp() { let opts = RustDbOptions::default(); assert_eq!(opts.connection_uri(), "mongodb://127.0.0.1:27017"); } #[test] fn test_connection_uri_socket() { let opts = RustDbOptions { socket_path: Some("/tmp/smartdb-test.sock".to_string()), ..Default::default() }; assert_eq!( opts.connection_uri(), "mongodb://%2Ftmp%2Fsmartdb-test.sock" ); } #[test] fn test_validation_file_storage_requires_path() { let opts = RustDbOptions { storage: StorageType::File, storage_path: None, ..Default::default() }; assert!(opts.validate().is_err()); } }