use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ServiceConfig {
pub server: ServerConfig,
pub tools: ToolConfigs,
#[serde(default)]
pub data: DataConfig,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct DataConfig {
pub cdot_path: Option<PathBuf>,
pub liftover: Option<LiftoverConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LiftoverConfig {
pub grch37_to_38: PathBuf,
pub grch38_to_37: PathBuf,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub max_request_size: String,
pub request_timeout_seconds: u64,
pub enable_cors: bool,
pub enable_tracing: bool,
pub max_concurrent_batches: Option<usize>,
pub max_batch_size: Option<usize>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolConfigs {
pub ferro: Option<FerroConfig>,
pub mutalyzer: Option<MutalyzerConfig>,
pub biocommons: Option<BiocommonsConfig>,
pub hgvs_rs: Option<HgvsRsConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FerroConfig {
pub enabled: bool,
pub reference_dir: PathBuf,
pub parallel_workers: Option<usize>,
pub shuffle_direction: Option<String>,
pub error_mode: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MutalyzerMode {
#[default]
Api,
Local,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct MutalyzerConfig {
pub enabled: bool,
#[serde(default)]
pub mode: MutalyzerMode,
pub api_url: String,
pub timeout_seconds: u32,
pub rate_limit_ms: Option<u64>,
pub health_check_interval: Option<u64>,
pub connection_pool: Option<ConnectionPoolConfig>,
pub circuit_breaker: Option<CircuitBreakerConfig>,
pub settings_file: Option<PathBuf>,
#[serde(default)]
pub allow_network: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ConnectionPoolConfig {
pub max_connections: Option<usize>,
pub idle_timeout_seconds: Option<u64>,
pub keep_alive_seconds: Option<u64>,
pub enable_http2: Option<bool>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CircuitBreakerConfig {
pub failure_threshold: Option<u32>,
pub recovery_timeout_seconds: Option<u64>,
pub success_threshold: Option<u32>,
}
fn default_uta_schema() -> String {
"uta_20210129b".to_string()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BiocommonsConfig {
pub enabled: bool,
pub uta_url: String,
#[serde(default = "default_uta_schema")]
pub uta_schema: String,
pub seqrepo_path: PathBuf,
pub docker_container: Option<String>,
pub parallel_workers: Option<usize>,
pub env_vars: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HgvsRsConfig {
pub enabled: bool,
pub uta_url: String,
pub uta_schema: String,
pub seqrepo_path: PathBuf,
pub lrg_mapping_file: Option<PathBuf>,
pub parallel_workers: Option<usize>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 3000,
max_request_size: "10MB".to_string(),
request_timeout_seconds: 60,
enable_cors: true,
enable_tracing: true,
max_concurrent_batches: Some(10),
max_batch_size: Some(1000),
}
}
}
impl Default for ToolConfigs {
fn default() -> Self {
Self {
ferro: Some(FerroConfig::default()),
mutalyzer: Some(MutalyzerConfig::default()),
biocommons: None, hgvs_rs: None, }
}
}
impl Default for FerroConfig {
fn default() -> Self {
Self {
enabled: true,
reference_dir: PathBuf::from("./reference"),
parallel_workers: None,
shuffle_direction: Some("3prime".to_string()),
error_mode: Some("lenient".to_string()),
}
}
}
impl Default for MutalyzerConfig {
fn default() -> Self {
Self {
enabled: true,
mode: MutalyzerMode::default(),
api_url: "http://localhost:8082".to_string(),
timeout_seconds: 30,
rate_limit_ms: Some(50),
health_check_interval: Some(60),
connection_pool: Some(ConnectionPoolConfig::default()),
circuit_breaker: Some(CircuitBreakerConfig::default()),
settings_file: None,
allow_network: false,
}
}
}
impl Default for ConnectionPoolConfig {
fn default() -> Self {
Self {
max_connections: Some(10),
idle_timeout_seconds: Some(30),
keep_alive_seconds: Some(90),
enable_http2: Some(true),
}
}
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: Some(5),
recovery_timeout_seconds: Some(60),
success_threshold: Some(3),
}
}
}
impl ServiceConfig {
pub fn from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let config: ServiceConfig = toml::from_str(&content)?;
Ok(config)
}
pub fn to_file(&self, path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?;
Ok(())
}
pub fn enabled_tools(&self) -> Vec<String> {
let mut tools = Vec::new();
if let Some(ferro) = &self.tools.ferro {
if ferro.enabled {
tools.push("ferro".to_string());
}
}
if let Some(mutalyzer) = &self.tools.mutalyzer {
if mutalyzer.enabled {
tools.push("mutalyzer".to_string());
}
}
if let Some(biocommons) = &self.tools.biocommons {
if biocommons.enabled {
tools.push("biocommons".to_string());
}
}
if let Some(hgvs_rs) = &self.tools.hgvs_rs {
if hgvs_rs.enabled {
tools.push("hgvs-rs".to_string());
}
}
tools
}
pub fn is_tool_enabled(&self, tool: &str) -> bool {
match tool {
"ferro" => self.tools.ferro.as_ref().is_some_and(|c| c.enabled),
"mutalyzer" => self.tools.mutalyzer.as_ref().is_some_and(|c| c.enabled),
"biocommons" => self.tools.biocommons.as_ref().is_some_and(|c| c.enabled),
"hgvs-rs" => self.tools.hgvs_rs.as_ref().is_some_and(|c| c.enabled),
_ => false,
}
}
pub fn validate(&self) -> Result<(), String> {
if self.server.port == 0 {
return Err("Server port must be greater than 0".to_string());
}
if self.enabled_tools().is_empty() {
return Err("At least one tool must be enabled".to_string());
}
if let Some(ferro) = &self.tools.ferro {
if ferro.enabled && !ferro.reference_dir.exists() {
return Err(format!(
"Ferro reference directory does not exist: {}",
ferro.reference_dir.display()
));
}
}
if let Some(biocommons) = &self.tools.biocommons {
if biocommons.enabled && !biocommons.seqrepo_path.exists() {
return Err(format!(
"Biocommons seqrepo directory does not exist: {}",
biocommons.seqrepo_path.display()
));
}
}
if let Some(hgvs_rs) = &self.tools.hgvs_rs {
if hgvs_rs.enabled && !hgvs_rs.seqrepo_path.exists() {
return Err(format!(
"HGVS-RS seqrepo directory does not exist: {}",
hgvs_rs.seqrepo_path.display()
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ServiceConfig::default();
assert_eq!(config.server.host, "0.0.0.0");
assert_eq!(config.server.port, 3000);
assert!(!config.enabled_tools().is_empty());
}
#[test]
fn test_enabled_tools() {
let mut config = ServiceConfig::default();
config.tools.ferro = Some(FerroConfig {
enabled: false,
..FerroConfig::default()
});
config.tools.mutalyzer = Some(MutalyzerConfig {
enabled: false,
..MutalyzerConfig::default()
});
config.tools.biocommons = None;
config.tools.hgvs_rs = None;
assert!(config.enabled_tools().is_empty());
config.tools.ferro = Some(FerroConfig {
enabled: true,
..FerroConfig::default()
});
assert_eq!(config.enabled_tools(), vec!["ferro"]);
}
}