mehari 0.47.0

Variant effect prediction all in Rust
//! Implementation of endpoint `/api/v1/seqvars/csq`.
//!
//! Also includes the implementation of the `/seqvars/csq` endpoint (deprecated).

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;

/// Query parameters of the `/api/v1/seqvars/csq` endpoint.
#[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 {
    /// The assembly.
    pub assembly: String,
    /// SPDI sequence.
    pub chromosome: String,
    /// SPDI position.
    pub position: u32,
    /// SPDI deletion.
    pub reference: String,
    /// SPDI insertion.
    pub alternative: String,
    /// Optionally, the HGNC ID of the gene to limit to.
    pub hgnc_id: Option<String>,
}

/// One entry in `SeqvarsCsqResponse`.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub(crate) struct SeqvarsCsqResultEntry {
    /// The consequences of the allele.
    pub consequences: Vec<Consequence>,
    /// The putative impact.
    pub putative_impact: PutativeImpact,
    /// The gene symbol.
    pub gene_symbol: String,
    /// The gene ID of the annotation, e.g., `"672"` for BRCA1 in RefSeq.
    pub gene_id: String,
    /// The HGNC ID of the gene, e.g., `"HGNC:1100"` for BRCA1; unset for a gene without one.
    pub hgnc_id: Option<String>,
    /// The feature type.
    pub feature_type: FeatureType,
    /// The feature identifier.
    pub feature_id: String,
    /// The feature biotype.
    pub feature_biotype: FeatureBiotype,
    /// The feature tags.
    pub feature_tags: Vec<FeatureTag>,
    /// The exon / intron rank.
    pub rank: Option<Rank>,
    /// HGVS g. notation.
    pub hgvs_g: Option<String>,
    /// HGVS c. notation.
    pub hgvs_t: Option<String>,
    /// HGVS p. notation.
    pub hgvs_p: Option<String>,
    /// cDNA position.
    pub tx_pos: Option<Pos>,
    /// CDS position.
    pub cds_pos: Option<Pos>,
    /// Protein position.
    pub protein_pos: Option<Pos>,
    /// Distance to feature.
    pub distance: Option<i32>,
    /// Strand of the alignment
    pub strand: i32,
    /// Optional list of warnings and error messages.
    pub messages: Option<Vec<Message>>,
    /// Values of the custom `ANN` columns, e.g. from plugins, by column, as written into `ANN`.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub custom_fields: BTreeMap<String, String>,
}

/// Response of the `/api/v1/seqvars/csq` endpoint.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub(crate) struct SeqvarsCsqResponse {
    /// Version information.
    pub version: VersionsInfoResponse,
    /// The original query records.
    pub query: SeqvarsCsqQuery,
    /// The resulting records for the scored genes.
    pub result: Vec<SeqvarsCsqResultEntry>,
}

/// Implementation of endpoints.
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 {
                // Skip if HGNC gene ID is given but does not match.
                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))
}

/// Whether the gene of the transcript `tx_ac` has the HGNC ID `hgnc_id`.
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)
}

/// Query for consequence of a variant.
#[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
}

/// Query for consequence of a variant.
#[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 {
    /// The filter by HGNC ID uses `hgnc_id`, not `gene_id`.
    #[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
        );
    }
}