use std::collections::BTreeMap;
use actix_web::{
get,
web::{self, Data, Json, Path},
};
use super::{CustomError, versions::VersionsInfoResponse};
use crate::annotate::seqvars::consequence::VcfVariant;
use crate::annotate::seqvars::consequence::terms::FeatureTag;
use crate::annotate::seqvars::consequence::terms::{
AnnField, Consequence, FeatureBiotype, FeatureType, Message, Pos, PutativeImpact, Rank,
};
use crate::annotate::seqvars::provider::Provider as MehariProvider;
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams, utoipa::ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[serde_with::skip_serializing_none]
pub(crate) struct SeqvarsCsqQuery {
pub assembly: String,
pub chromosome: String,
pub position: u32,
pub reference: String,
pub alternative: String,
pub hgnc_id: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub(crate) struct SeqvarsCsqResultEntry {
pub consequences: Vec<Consequence>,
pub putative_impact: PutativeImpact,
pub gene_symbol: String,
pub gene_id: String,
pub hgnc_id: Option<String>,
pub feature_type: FeatureType,
pub feature_id: String,
pub feature_biotype: FeatureBiotype,
pub feature_tags: Vec<FeatureTag>,
pub rank: Option<Rank>,
pub hgvs_g: Option<String>,
pub hgvs_t: Option<String>,
pub hgvs_p: Option<String>,
pub tx_pos: Option<Pos>,
pub cds_pos: Option<Pos>,
pub protein_pos: Option<Pos>,
pub distance: Option<i32>,
pub strand: i32,
pub messages: Option<Vec<Message>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub custom_fields: BTreeMap<String, String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub(crate) struct SeqvarsCsqResponse {
pub version: VersionsInfoResponse,
pub query: SeqvarsCsqQuery,
pub result: Vec<SeqvarsCsqResultEntry>,
}
async fn handle_impl(
data: Data<super::WebServerData>,
_path: Path<()>,
query: web::Query<SeqvarsCsqQuery>,
) -> actix_web::Result<Json<SeqvarsCsqResponse>, super::CustomError> {
let SeqvarsCsqQuery {
assembly: genome_release,
chromosome,
position,
reference,
alternative,
hgnc_id,
} = query.clone().into_inner();
let predictor = data
.seqvars_predictors
.get(&genome_release)
.ok_or_else(|| {
super::CustomError::new(anyhow::anyhow!(
"genome release not supported: {:?}",
query.assembly
))
})?;
let mut result = Vec::new();
let g_var = VcfVariant {
chromosome,
position: position as i32,
reference,
alternative,
};
let ann_fields = predictor
.predict(&g_var)
.map_err(|e| super::CustomError::new(anyhow::anyhow!("prediction failed: {}", e)))?;
if let Some(ann_fields) = ann_fields {
for ann_field in ann_fields {
if let Some(hgnc_id) = &hgnc_id {
if !has_hgnc_id(&predictor.provider, &ann_field.feature_id, hgnc_id) {
continue;
}
}
let gene_hgnc_id = predictor.hgnc_id(&ann_field.feature_id).map(String::from);
let AnnField {
consequences,
putative_impact,
gene_symbol,
gene_id,
feature_type,
feature_id,
feature_biotype,
feature_tags,
rank,
hgvs_g,
hgvs_c: hgvs_t,
hgvs_p,
cdna_pos: tx_pos,
cds_pos,
protein_pos,
distance,
strand,
messages,
custom_fields,
..
} = ann_field;
let entry = SeqvarsCsqResultEntry {
consequences,
putative_impact,
gene_symbol,
gene_id,
hgnc_id: gene_hgnc_id,
feature_type,
feature_id,
feature_biotype: if feature_biotype.contains(&FeatureBiotype::Coding) {
FeatureBiotype::Coding
} else {
FeatureBiotype::Noncoding
},
feature_tags,
rank,
hgvs_g,
hgvs_t,
hgvs_p,
tx_pos,
cds_pos,
protein_pos,
distance,
strand,
messages,
custom_fields: custom_fields
.into_iter()
.filter_map(|(column, value)| Some((column, value?)))
.collect(),
};
result.push(entry);
}
}
result.sort_by_key(|e| e.putative_impact);
let result = SeqvarsCsqResponse {
version: VersionsInfoResponse::from_web_server_data(data.into_inner().as_ref())
.map_err(|e| CustomError::new(anyhow::anyhow!("Problem determining version: {}", e)))?,
query: query.into_inner(),
result,
};
Ok(Json(result))
}
fn has_hgnc_id(provider: &MehariProvider, tx_ac: &str, hgnc_id: &str) -> bool {
provider.get_tx(tx_ac).and_then(|tx| tx.hgnc_id.as_deref()) == Some(hgnc_id)
}
#[allow(clippy::unused_async)]
#[get("/seqvars/csq")]
async fn handle(
data: Data<super::WebServerData>,
_path: Path<()>,
query: web::Query<SeqvarsCsqQuery>,
) -> actix_web::Result<Json<SeqvarsCsqResponse>, super::CustomError> {
handle_impl(data, _path, query).await
}
#[allow(clippy::unused_async)]
#[utoipa::path(
get,
operation_id = "seqvarsCsq",
params(
SeqvarsCsqQuery
),
responses(
(status = 200, description = "Seqvars consequence information.", body = SeqvarsCsqResponse),
(status = 500, description = "Internal server error.", body = CustomError)
)
)]
#[get("/api/v1/seqvars/csq")]
async fn handle_with_openapi(
data: Data<super::WebServerData>,
_path: Path<()>,
query: web::Query<SeqvarsCsqQuery>,
) -> actix_web::Result<Json<SeqvarsCsqResponse>, super::CustomError> {
handle_impl(data, _path, query).await
}
#[cfg(test)]
mod test {
#[rstest::rstest]
#[case::hgnc_id("HGNC:1", true)]
#[case::gene_id("11", false)]
fn has_hgnc_id(#[case] hgnc_id: &str, #[case] expected: bool) {
use crate::annotate::seqvars::provider::{Provider, test_db};
let provider = Provider::new(test_db::one_gene_db(), None::<&str>, true);
assert_eq!(
super::has_hgnc_id(&provider, "NM_000001.1", hgnc_id),
expected
);
}
}