use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::time::timeout;
use crate::benchmark::types::ParseResult;
use crate::service::types::{
categorize_error, health_check::HealthCheckResult, AgreementSummary, ServiceError, ToolName,
ToolResult,
};
pub mod biocommons;
pub mod ferro;
pub mod hgvs_rs;
pub mod http_client;
pub mod manager;
pub mod mutalyzer;
pub use manager::ToolManager;
#[async_trait::async_trait]
pub trait HgvsToolService: Send + Sync {
async fn parse(&self, hgvs: &str) -> Result<ParseResult, ServiceError>;
async fn normalize(&self, hgvs: &str) -> Result<ParseResult, ServiceError>;
async fn health_check(&self) -> HealthCheckResult;
fn tool_name(&self) -> ToolName;
fn tool_name_str(&self) -> &'static str {
self.tool_name().as_str()
}
}
pub struct MultiToolResult {
pub input: String,
pub results: Vec<ToolResult>,
pub agreement: AgreementSummary,
pub total_time: Duration,
}
pub fn parse_result_to_tool_result(
tool: ToolName,
result: Result<ParseResult, ServiceError>,
elapsed: Duration,
) -> ToolResult {
match result {
Ok(parse_result) => ToolResult {
tool,
success: parse_result.success,
output: parse_result.output,
error: parse_result.error.clone(),
error_category: parse_result
.error
.as_ref()
.map(|e| categorize_error(tool, e)),
elapsed_ms: elapsed.as_millis() as u64,
details: parse_result.details,
},
Err(e) => ToolResult {
tool,
success: false,
output: None,
error: Some(e.to_string()),
error_category: Some(categorize_error(tool, &e.to_string())),
elapsed_ms: elapsed.as_millis() as u64,
details: None,
},
}
}
pub fn analyze_agreement(results: &[ToolResult]) -> AgreementSummary {
let successful_results: Vec<_> = results.iter().filter(|r| r.success).collect();
let failed_results: Vec<_> = results.iter().filter(|r| !r.success).collect();
let mut outputs: HashMap<String, Vec<ToolName>> = HashMap::new();
for result in &successful_results {
if let Some(output) = &result.output {
outputs.entry(output.clone()).or_default().push(result.tool);
}
}
let all_agree = outputs.len() <= 1;
AgreementSummary {
all_agree,
successful_tools: successful_results.len(),
failed_tools: failed_results.len(),
outputs,
}
}
pub async fn run_tools_on_variant<F, Fut>(
tools: &[(String, Arc<dyn HgvsToolService>)],
hgvs: &Arc<str>,
timeout_duration: Duration,
operation: F,
) -> MultiToolResult
where
F: Fn(Arc<dyn HgvsToolService>, Arc<str>) -> Fut + Clone + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<ParseResult, ServiceError>> + Send + 'static,
{
let start_time = Instant::now();
let mut results = Vec::new();
let mut tasks = Vec::new();
for (tool_name, tool_service) in tools {
let tool_name = tool_name.clone();
let tool_service = tool_service.clone();
let hgvs_arc = Arc::clone(hgvs); let operation = operation.clone();
let task = tokio::spawn(async move {
let start = Instant::now();
let result = timeout(timeout_duration, operation(tool_service, hgvs_arc)).await;
let elapsed = start.elapsed();
let final_result = match result {
Ok(tool_result) => tool_result,
Err(_) => Err(ServiceError::Timeout),
};
let tool_enum = ToolName::parse(&tool_name).unwrap_or(ToolName::Ferro);
parse_result_to_tool_result(tool_enum, final_result, elapsed)
});
tasks.push(task);
}
for task in tasks {
match task.await {
Ok(tool_result) => results.push(tool_result),
Err(e) => {
results.push(ToolResult {
tool: ToolName::Ferro, success: false,
output: None,
error: Some(format!("Task error: {}", e)),
error_category: Some(categorize_error(
ToolName::Ferro,
&format!("Task error: {}", e),
)),
elapsed_ms: 0,
details: None,
});
}
}
}
let agreement = analyze_agreement(&results);
let total_time = start_time.elapsed();
MultiToolResult {
input: hgvs.to_string(),
results,
agreement,
total_time,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyze_agreement_all_agree() {
let results = vec![
ToolResult {
tool: ToolName::Ferro,
success: true,
output: Some("NM_000088.3:c.589G>T".to_string()),
error: None,
error_category: None,
elapsed_ms: 10,
details: None,
},
ToolResult {
tool: ToolName::Mutalyzer,
success: true,
output: Some("NM_000088.3:c.589G>T".to_string()),
error: None,
error_category: None,
elapsed_ms: 150,
details: None,
},
];
let agreement = analyze_agreement(&results);
assert!(agreement.all_agree);
assert_eq!(agreement.successful_tools, 2);
assert_eq!(agreement.failed_tools, 0);
assert_eq!(agreement.outputs.len(), 1);
}
#[test]
fn test_analyze_agreement_disagree() {
let results = vec![
ToolResult {
tool: ToolName::Ferro,
success: true,
output: Some("NM_000088.3:c.589G>T".to_string()),
error: None,
error_category: None,
elapsed_ms: 10,
details: None,
},
ToolResult {
tool: ToolName::Mutalyzer,
success: true,
output: Some("NM_000088.3:c.590G>T".to_string()), error: None,
error_category: None,
elapsed_ms: 150,
details: None,
},
];
let agreement = analyze_agreement(&results);
assert!(!agreement.all_agree);
assert_eq!(agreement.successful_tools, 2);
assert_eq!(agreement.failed_tools, 0);
assert_eq!(agreement.outputs.len(), 2);
}
#[test]
fn test_analyze_agreement_with_failures() {
let results = vec![
ToolResult {
tool: ToolName::Ferro,
success: true,
output: Some("NM_000088.3:c.589G>T".to_string()),
error: None,
error_category: None,
elapsed_ms: 10,
details: None,
},
ToolResult {
tool: ToolName::Mutalyzer,
success: false,
output: None,
error: Some("Parse error".to_string()),
error_category: Some("parse_error".to_string()),
elapsed_ms: 150,
details: None,
},
];
let agreement = analyze_agreement(&results);
assert!(agreement.all_agree); assert_eq!(agreement.successful_tools, 1);
assert_eq!(agreement.failed_tools, 1);
assert_eq!(agreement.outputs.len(), 1);
}
}