use axum::{extract::State, http::StatusCode, response::Json};
use std::time::Instant;
use crate::hgvs::interval::interval_is_wraparound;
use crate::service::{
server::AppState,
types::{extract_variant_details, ErrorResponse, ServiceError, ValidateResponse},
validation::validate_hgvs as security_validate_hgvs,
};
#[derive(Debug, serde::Deserialize)]
pub struct ValidateRequest {
pub hgvs: String,
}
pub async fn validate_single(
State(_state): State<AppState>,
Json(request): Json<ValidateRequest>,
) -> Result<Json<ValidateResponse>, (StatusCode, Json<ErrorResponse>)> {
let start = Instant::now();
if let Err(validation_error) = security_validate_hgvs(&request.hgvs) {
let error = ServiceError::InvalidHgvs(validation_error.to_string());
return Err((
StatusCode::from_u16(error.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
Json(error.to_response()),
));
}
let hgvs_str = request.hgvs.clone();
let parse_result =
tokio::task::spawn_blocking(move || crate::hgvs::parser::parse_hgvs_lenient(&hgvs_str))
.await
.map_err(|e| {
let error = ServiceError::InternalError(format!("Task error: {}", e));
(StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_response()))
})?;
let elapsed_ms = start.elapsed().as_millis() as u64;
match parse_result {
Ok(result) => {
let components = extract_variant_details(&result.result);
let wraps_origin = variant_wraps_origin(&result.result);
let errors = if result.warnings.is_empty() {
None
} else {
Some(result.warnings.iter().map(|w| w.message.clone()).collect())
};
Ok(Json(ValidateResponse {
input: request.hgvs,
valid: true,
errors,
components,
processing_time_ms: elapsed_ms,
wraps_origin,
}))
}
Err(e) => Ok(Json(ValidateResponse {
input: request.hgvs,
valid: false,
errors: Some(vec![e.to_string()]),
components: None,
processing_time_ms: elapsed_ms,
wraps_origin: false,
})),
}
}
fn variant_wraps_origin(variant: &crate::hgvs::variant::HgvsVariant) -> bool {
use crate::hgvs::variant::HgvsVariant;
match variant {
HgvsVariant::Mt(v) => interval_is_wraparound(&v.loc_edit.location),
HgvsVariant::Circular(v) => interval_is_wraparound(&v.loc_edit.location),
_ => false,
}
}
pub fn validate_hgvs(hgvs: &str) -> ValidateResponse {
match crate::hgvs::parser::parse_hgvs_lenient(hgvs) {
Ok(result) => {
let components = extract_variant_details(&result.result);
let wraps_origin = variant_wraps_origin(&result.result);
let errors = if result.warnings.is_empty() {
None
} else {
Some(result.warnings.iter().map(|w| w.message.clone()).collect())
};
ValidateResponse {
input: hgvs.to_string(),
valid: true,
errors,
components,
processing_time_ms: 0,
wraps_origin,
}
}
Err(e) => ValidateResponse {
input: hgvs.to_string(),
valid: false,
errors: Some(vec![e.to_string()]),
components: None,
processing_time_ms: 0,
wraps_origin: false,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_hgvs_populates_components() {
let response = validate_hgvs("NM_000249.4:c.350C>T");
assert!(response.valid);
let components = response.components.expect("components populated");
assert_eq!(components.coordinate_system, "c");
assert_eq!(components.variant_type, "substitution");
assert_eq!(components.reference, "NM_000249.4");
}
#[test]
fn test_validate_hgvs_allele_has_no_components() {
let response = validate_hgvs("NM_000088.3:c.[10A>G;20C>T]");
assert!(response.valid);
assert!(response.components.is_none());
}
#[test]
fn test_validate_hgvs_invalid_input() {
let response = validate_hgvs("not a variant");
assert!(!response.valid);
assert!(response.components.is_none());
assert!(!response.wraps_origin);
}
#[test]
fn test_variant_wraps_origin_linear_axis_is_false() {
let result = crate::hgvs::parser::parse_hgvs_lenient("NM_000249.4:c.350C>T").unwrap();
assert!(!variant_wraps_origin(&result.result));
}
}