use actix_web::{
get,
web::{self, Data, Json, Path},
};
use crate::common::{cli::GenomeRelease, spdi};
use super::error::CustomError;
use serde_with::{formats::CommaSeparator, StringWithSeparator};
use crate::pbs::clinvar_data::extracted_vars::VariationType as PbVariationType;
use crate::server::run::clinvar_data::ClinvarExtractedVariationType;
const DEFAULT_PAGE_SIZE: u32 = 100;
const DEFAULT_MIN_OVERLAP: f64 = 0.5;
#[serde_with::skip_serializing_none]
#[serde_with::serde_as]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct Request {
pub genome_release: String,
pub chromosome: String,
pub start: u32,
pub stop: u32,
#[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, PbVariationType>>")]
pub variation_types: Option<Vec<PbVariationType>>,
pub min_overlap: Option<f64>,
pub page_no: Option<u32>,
pub page_size: Option<u32>,
}
fn reciprocal_overlap<T>(lhs: &std::ops::Range<T>, rhs: &std::ops::Range<T>) -> f64
where
T: std::cmp::Ord + std::ops::Sub<Output = T> + std::ops::Add<Output = T> + Copy + Into<f64>,
{
if lhs.end <= rhs.start || rhs.end <= lhs.start {
return 0.0;
}
let len_lhs = lhs.end - lhs.start;
let len_rhs = rhs.end - rhs.start;
let len_ovl = std::cmp::min(lhs.end, rhs.end) - std::cmp::max(lhs.start, rhs.start);
let res_lhs = Into::<f64>::into(len_ovl) / Into::<f64>::into(len_lhs);
let res_rhs = Into::<f64>::into(len_ovl) / Into::<f64>::into(len_rhs);
if res_lhs < res_rhs {
res_lhs
} else {
res_rhs
}
}
async fn handle_impl(
data: Data<crate::server::run::WebServerData>,
_path: Path<()>,
query: Request,
) -> actix_web::Result<crate::pbs::clinvar::sv::ResponsePage, CustomError> {
let genome_release: GenomeRelease =
query
.clone()
.genome_release
.parse()
.map_err(|e: strum::ParseError| {
CustomError::new(anyhow::anyhow!("problem getting genome release: {}", e))
})?;
let trees = if let Some(trees) = data.clinvar_svs[genome_release].as_ref() {
trees
} else {
Err(anyhow::anyhow!(
"no clinvar-sv database for genome release {}",
genome_release
))
.map_err(CustomError::new)?
};
let spdi_range = spdi::Range {
sequence: query.chromosome.replace("chr", "").to_string(),
start: query.start as i32,
end: query.stop as i32,
};
let records = trees.query(&spdi_range).map_err(|e| {
CustomError::new(anyhow::anyhow!(
"problem querying clinvar-sv database: {}",
e
))
})?;
let variation_types = query
.variation_types
.as_ref()
.map(|vs| vs.iter().map(|v| *v as i32).collect::<Vec<_>>())
.unwrap_or_default();
let records = {
let mut records = records
.into_iter()
.filter_map(|record| {
let crate::pbs::clinvar_data::clinvar_public::location::SequenceLocation {
start,
stop,
inner_start,
inner_stop,
outer_start,
outer_stop,
..
} = record
.sequence_location
.clone()
.expect("missing sequence_location");
let (start, stop) = if let (Some(start), Some(stop)) = (start, stop) {
(start, stop)
} else if let (Some(inner_start), Some(inner_stop)) = (inner_start, inner_stop) {
(inner_start, inner_stop)
} else if let (Some(outer_start), Some(outer_stop)) = (outer_start, outer_stop) {
(outer_start, outer_stop)
} else {
let accession = record.accession.clone().expect("missing accession");
let vcv = format!("{}.{}", &accession.accession, &accession.version);
tracing::warn!("skipping record because no start/stop: {}", &vcv);
return None;
};
let overlap =
reciprocal_overlap(&((query.start - 1)..query.stop), &((start - 1)..stop));
Some(crate::pbs::clinvar::sv::ResponseRecord {
record: Some(record),
overlap,
})
})
.filter(|record| {
if !variation_types.is_empty() {
return variation_types
.contains(&record.record.as_ref().expect("no record").variation_type);
}
let min_overlap = query.min_overlap.unwrap_or(DEFAULT_MIN_OVERLAP);
if record.overlap < min_overlap {
return false;
}
true
})
.collect::<Vec<_>>();
records.sort_by(|a, b| b.overlap.partial_cmp(&a.overlap).unwrap());
records
};
let per_page = query.page_size.unwrap_or(DEFAULT_PAGE_SIZE);
let total_pages = (records.len() as u32 + 1) / per_page;
let current_page = std::cmp::max(query.page_no.unwrap_or(1), 1);
let begin = ((current_page - 1) * per_page) as usize;
let end = std::cmp::min(begin as u32 + per_page, records.len() as u32) as usize;
let records = records[begin..end].to_vec();
let page_info = crate::pbs::clinvar::sv::PageInfo {
total: records.len() as u32,
per_page,
current_page,
total_pages,
};
Ok(crate::pbs::clinvar::sv::ResponsePage {
records,
page_info: Some(page_info),
})
}
#[get("/clinvar-sv/query")]
async fn handle(
data: Data<crate::server::run::WebServerData>,
path: Path<()>,
query: web::Query<Request>,
) -> actix_web::Result<Json<crate::pbs::clinvar::sv::ResponsePage>, CustomError> {
Ok(Json(handle_impl(data, path, query.into_inner()).await?))
}
#[serde_with::skip_serializing_none]
#[serde_with::serde_as]
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams,
)]
pub(crate) struct StrucvarsClinvarQuery {
pub genome_release: GenomeRelease,
pub chromosome: String,
pub start: u32,
pub stop: u32,
#[serde_as(
as = "Option<StringWithSeparator::<CommaSeparator, ClinvarExtractedVariationType>>"
)]
pub variation_types: Option<Vec<ClinvarExtractedVariationType>>,
pub min_overlap: Option<f64>,
pub page_no: Option<u32>,
pub page_size: Option<u32>,
}
impl From<StrucvarsClinvarQuery> for Request {
fn from(val: StrucvarsClinvarQuery) -> Self {
Request {
genome_release: val.genome_release.to_string(),
chromosome: val.chromosome,
start: val.start,
stop: val.stop,
variation_types: val
.variation_types
.map(|v| v.into_iter().map(Into::into).collect()),
min_overlap: val.min_overlap,
page_no: val.page_no,
page_size: val.page_size,
}
}
}
pub mod response {
use crate::server::run::clinvar_data::ClinvarExtractedVcvRecord;
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema, utoipa::ToResponse,
)]
pub struct StrucvarsClinvarResponseRecord {
pub record: Option<ClinvarExtractedVcvRecord>,
pub overlap: f64,
}
impl TryFrom<crate::pbs::clinvar::sv::ResponseRecord> for StrucvarsClinvarResponseRecord {
type Error = anyhow::Error;
fn try_from(value: crate::pbs::clinvar::sv::ResponseRecord) -> Result<Self, Self::Error> {
Ok(Self {
record: value.record.map(|record| record.try_into()).transpose()?,
overlap: value.overlap,
})
}
}
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema, utoipa::ToResponse,
)]
pub struct StrucvarsClinvarPageInfo {
pub total: u32,
pub per_page: u32,
pub current_page: u32,
pub total_pages: u32,
}
impl From<crate::pbs::clinvar::sv::PageInfo> for StrucvarsClinvarPageInfo {
fn from(value: crate::pbs::clinvar::sv::PageInfo) -> Self {
Self {
total: value.total,
per_page: value.per_page,
current_page: value.current_page,
total_pages: value.total_pages,
}
}
}
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema, utoipa::ToResponse,
)]
pub struct StrucvarsClinvarResponse {
pub records: Vec<StrucvarsClinvarResponseRecord>,
pub page_info: StrucvarsClinvarPageInfo,
}
impl TryFrom<crate::pbs::clinvar::sv::ResponsePage> for StrucvarsClinvarResponse {
type Error = anyhow::Error;
fn try_from(value: crate::pbs::clinvar::sv::ResponsePage) -> Result<Self, Self::Error> {
Ok(Self {
records: value
.records
.into_iter()
.map(|record| record.try_into())
.collect::<Result<_, _>>()?,
page_info: value
.page_info
.map(|page_info| page_info.into())
.ok_or_else(|| anyhow::anyhow!("missing page_info in response"))?,
})
}
}
}
use response::*;
#[utoipa::path(
get,
operation_id = "strucvarsClinvarQuery",
params(StrucvarsClinvarQuery),
responses(
(status = 200, description = "Clinvar strucvars information.", body = StrucvarsClinvarResponse),
(status = 500, description = "Internal server error.", body = CustomError)
)
)]
#[get("/api/v1/strucvars/clinvar/query")]
async fn handle_with_openapi(
data: Data<crate::server::run::WebServerData>,
path: Path<()>,
query: web::Query<StrucvarsClinvarQuery>,
) -> actix_web::Result<Json<StrucvarsClinvarResponse>, CustomError> {
Ok(Json(
handle_impl(data, path, Into::<Request>::into(query.into_inner()))
.await
.map_err(|e| CustomError::new(anyhow::anyhow!("Implementaion error: {:?}", e)))?
.try_into()
.map_err(|e| CustomError::new(anyhow::anyhow!("Response conversion error: {:?}", e)))?,
))
}