use crate::pbs;
use crate::pbs::server::{GeneTranscriptsQuery, GeneTranscriptsResponse};
use super::versions::Assembly;
use crate::pbs::txs::GenomeBuild;
use crate::server::run::actix_server::CustomError;
use actix_web::{
get,
web::{self, Data, Json, Path},
};
use hgvs::data::interface::Provider as _;
static PAGE_SIZE_MAX: i32 = 1000;
static PAGE_SIZE_DEFAULT: i32 = 100;
fn genes_tx_impl(
data: Data<super::WebServerData>,
query: GeneTranscriptsQuery,
) -> Result<GeneTranscriptsResponse, CustomError> {
let GeneTranscriptsQuery {
genome_build,
hgnc_id,
page_size,
next_page_token,
..
} = query;
let genome_build = genome_build.unwrap_or_else(|| String::from("grch37"));
let hgnc_id = hgnc_id
.as_ref()
.ok_or_else(|| CustomError::new(anyhow::anyhow!("No HGNC ID provided.")))?;
let page_size = page_size
.unwrap_or(PAGE_SIZE_DEFAULT)
.min(PAGE_SIZE_MAX)
.max(1);
let provider = data
.provider
.get(&genome_build)
.ok_or_else(|| CustomError::new(anyhow::anyhow!("No provider available.")))?;
let tx_acs = provider
.get_tx_for_gene(hgnc_id)
.map_err(|e| CustomError::new(anyhow::anyhow!("No transcripts found: {}", e)))?
.into_iter()
.map(|tx| tx.tx_ac)
.collect::<Vec<_>>();
let first = next_page_token
.as_ref()
.and_then(|next_page_token| tx_acs.iter().position(|tx_ac| tx_ac == next_page_token))
.unwrap_or(0);
let last = (first + page_size as usize).min(tx_acs.len());
Ok(GeneTranscriptsResponse {
transcripts: tx_acs[first..last]
.iter()
.filter_map(|tx_ac| provider.get_tx(tx_ac))
.cloned()
.collect::<Vec<_>>(),
next_page_token: if last < tx_acs.len() {
Some(tx_acs[last].clone())
} else {
None
},
})
}
#[allow(clippy::unused_async)]
#[get("/genes/txs")]
async fn handle(
data: Data<super::WebServerData>,
_path: Path<()>,
query: web::Query<GeneTranscriptsQuery>,
) -> actix_web::Result<Json<GeneTranscriptsResponse>, CustomError> {
Ok(Json(genes_tx_impl(data, query.into_inner())?))
}
#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
pub(crate) struct GenesTranscriptsListQuery {
pub hgnc_id: String,
pub genome_build: Assembly,
pub page_size: Option<i32>,
pub next_page_token: Option<String>,
}
impl From<GenesTranscriptsListQuery> for GeneTranscriptsQuery {
fn from(val: GenesTranscriptsListQuery) -> Self {
GeneTranscriptsQuery {
genome_build: Some(match val.genome_build {
Assembly::Grch38 => String::from("grch38"),
Assembly::Grch37 => String::from("grch37"),
}),
hgnc_id: Some(val.hgnc_id),
#[allow(deprecated)]
genome_build_enum: Some(
match val.genome_build {
Assembly::Grch37 => GenomeBuild::Grch37,
Assembly::Grch38 => GenomeBuild::Grch38,
}
.into(),
),
page_size: val.page_size,
next_page_token: val.next_page_token,
}
}
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub(crate) enum TranscriptBiotype {
Coding,
NonCoding,
}
impl TryFrom<pbs::txs::TranscriptBiotype> for TranscriptBiotype {
type Error = anyhow::Error;
fn try_from(value: pbs::txs::TranscriptBiotype) -> Result<Self, Self::Error> {
match value {
pbs::txs::TranscriptBiotype::Coding => Ok(TranscriptBiotype::Coding),
pbs::txs::TranscriptBiotype::NonCoding => Ok(TranscriptBiotype::NonCoding),
_ => Err(anyhow::anyhow!("Invalid biotype: {:?}", value)),
}
}
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub(crate) enum TranscriptTag {
Basic,
EnsemblCanonical,
ManeSelect,
ManePlusClinical,
RefSeqSelect,
Selenoprotein,
GencodePrimary,
Other,
BasicBackport,
EnsemblCanonicalBackport,
ManeSelectBackport,
ManePlusClinicalBackport,
RefSeqSelectBackport,
SelenoproteinBackport,
GencodePrimaryBackport,
OtherBackport,
}
impl TryFrom<pbs::txs::TranscriptTag> for TranscriptTag {
type Error = anyhow::Error;
fn try_from(value: pbs::txs::TranscriptTag) -> Result<Self, Self::Error> {
match value {
pbs::txs::TranscriptTag::Basic => Ok(TranscriptTag::Basic),
pbs::txs::TranscriptTag::EnsemblCanonical => Ok(TranscriptTag::EnsemblCanonical),
pbs::txs::TranscriptTag::ManeSelect => Ok(TranscriptTag::ManeSelect),
pbs::txs::TranscriptTag::ManePlusClinical => Ok(TranscriptTag::ManePlusClinical),
pbs::txs::TranscriptTag::RefSeqSelect => Ok(TranscriptTag::RefSeqSelect),
pbs::txs::TranscriptTag::Selenoprotein => Ok(TranscriptTag::Selenoprotein),
pbs::txs::TranscriptTag::GencodePrimary => Ok(TranscriptTag::GencodePrimary),
pbs::txs::TranscriptTag::Other => Ok(TranscriptTag::Other),
pbs::txs::TranscriptTag::BasicBackport => Ok(TranscriptTag::BasicBackport),
pbs::txs::TranscriptTag::EnsemblCanonicalBackport => {
Ok(TranscriptTag::EnsemblCanonicalBackport)
}
pbs::txs::TranscriptTag::ManeSelectBackport => Ok(TranscriptTag::ManeSelectBackport),
pbs::txs::TranscriptTag::ManePlusClinicalBackport => {
Ok(TranscriptTag::ManePlusClinicalBackport)
}
pbs::txs::TranscriptTag::RefSeqSelectBackport => {
Ok(TranscriptTag::RefSeqSelectBackport)
}
pbs::txs::TranscriptTag::SelenoproteinBackport => {
Ok(TranscriptTag::SelenoproteinBackport)
}
pbs::txs::TranscriptTag::GencodePrimaryBackport => {
Ok(TranscriptTag::GencodePrimaryBackport)
}
pbs::txs::TranscriptTag::OtherBackport => Ok(TranscriptTag::OtherBackport),
_ => Err(anyhow::anyhow!("Invalid transcript tag: {:?}", value)),
}
}
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub(crate) enum Strand {
Unknown,
Plus,
Minus,
}
impl From<pbs::txs::Strand> for Strand {
fn from(value: pbs::txs::Strand) -> Self {
match value {
pbs::txs::Strand::Unknown => Strand::Unknown,
pbs::txs::Strand::Plus => Strand::Plus,
pbs::txs::Strand::Minus => Strand::Minus,
}
}
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub(crate) struct ExonAlignment {
pub alt_start_i: i32,
pub alt_end_i: i32,
pub ord: i32,
pub alt_cds_start_i: Option<i32>,
pub alt_cds_end_i: Option<i32>,
pub cigar: String,
}
impl From<pbs::txs::ExonAlignment> for ExonAlignment {
fn from(value: pbs::txs::ExonAlignment) -> Self {
ExonAlignment {
alt_start_i: value.alt_start_i,
alt_end_i: value.alt_end_i,
ord: value.ord,
alt_cds_start_i: value.alt_cds_start_i,
alt_cds_end_i: value.alt_cds_end_i,
cigar: value.cigar.clone(),
}
}
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub(crate) struct GenomeAlignment {
pub genome_build: Assembly,
pub contig: String,
pub cds_start: Option<i32>,
pub cds_end: Option<i32>,
pub strand: Strand,
pub exons: Vec<ExonAlignment>,
}
impl TryFrom<pbs::txs::GenomeAlignment> for GenomeAlignment {
type Error = anyhow::Error;
fn try_from(value: pbs::txs::GenomeAlignment) -> Result<Self, Self::Error> {
Ok(GenomeAlignment {
genome_build: super::versions::Assembly::try_from(value.genome_build.as_str())?,
contig: value.contig.clone(),
cds_start: value.cds_start,
cds_end: value.cds_end,
strand: Strand::from(pbs::txs::Strand::try_from(value.strand)?),
exons: value.exons.into_iter().map(Into::into).collect(),
})
}
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub(crate) struct Transcript {
pub id: String,
pub gene_symbol: String,
pub gene_id: String,
pub biotype: TranscriptBiotype,
pub tags: Vec<TranscriptTag>,
pub protein: Option<String>,
pub start_codon: Option<i32>,
pub stop_codon: Option<i32>,
pub genome_alignments: Vec<GenomeAlignment>,
pub filtered: Option<bool>,
pub filter_reason: ::core::option::Option<u32>,
}
impl TryFrom<pbs::txs::Transcript> for Transcript {
type Error = anyhow::Error;
fn try_from(value: pbs::txs::Transcript) -> Result<Self, Self::Error> {
Ok(Transcript {
id: value.id.clone(),
gene_symbol: value.gene_symbol.clone(),
gene_id: value.gene_id.clone(),
biotype: TranscriptBiotype::try_from(pbs::txs::TranscriptBiotype::try_from(
value.biotype,
)?)?,
tags: value
.tags
.into_iter()
.map(|i32_tag| -> Result<_, anyhow::Error> {
TranscriptTag::try_from(pbs::txs::TranscriptTag::try_from(i32_tag)?)
})
.collect::<Result<Vec<_>, _>>()?,
protein: value.protein.clone(),
start_codon: value.start_codon,
stop_codon: value.stop_codon,
genome_alignments: value
.genome_alignments
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
filtered: value.filtered,
filter_reason: value.filter_reason,
})
}
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub(crate) struct GenesTranscriptsListResponse {
pub transcripts: Vec<Transcript>,
pub next_page_token: Option<String>,
}
impl TryFrom<pbs::server::GeneTranscriptsResponse> for GenesTranscriptsListResponse {
type Error = anyhow::Error;
fn try_from(value: pbs::server::GeneTranscriptsResponse) -> Result<Self, Self::Error> {
Ok(GenesTranscriptsListResponse {
transcripts: value
.transcripts
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
next_page_token: value.next_page_token,
})
}
}
#[allow(clippy::unused_async)]
#[utoipa::path(
get,
operation_id = "genesTranscriptsList",
params(GenesTranscriptsListQuery),
responses(
(status = 200, description = "Transcripts for the selected gene.", body = GenesTranscriptsListResponse),
(status = 500, description = "Internal server error.", body = CustomError)
)
)]
#[get("/api/v1/genes/transcripts")]
async fn handle_with_openapi(
data: Data<super::WebServerData>,
_path: Path<()>,
query: web::Query<GenesTranscriptsListQuery>,
) -> actix_web::Result<Json<GenesTranscriptsListResponse>, CustomError> {
let result = genes_tx_impl(data, query.into_inner().into())?;
Ok(Json(result.try_into().map_err(|e| {
CustomError::new(anyhow::anyhow!("Conversion error: {}", e))
})?))
}