use std::cell::OnceCell;
use std::fmt::Debug;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use hgvs::data::interface::Provider;
use hgvs::mapper::altseq::{
AltSeqBuilder, AltTranscriptData, RefTranscriptData, VariantLocation,
ref_transcript_data_shared,
};
use hgvs::parser::HgvsVariant;
use itertools::Itertools;
use strum::VariantArray;
use super::VcfVariant;
use super::logic::{HgvsVariantExt, apply_n_edits};
use super::terms::AnnField;
use crate::annotate::seqvars::provider::Provider as MehariProvider;
use crate::db::TranscriptDatabase;
use crate::errors::SeqvarsError;
use crate::pbs::txs::{Transcript, TxSeqDatabase};
mod compound;
mod nmd;
mod sequence;
#[cfg(feature = "wasm")]
mod wasm;
pub use compound::CompoundPlugin;
pub use nmd::{NmdPlugin, NmdPrediction};
pub use sequence::SequencePlugin;
#[cfg(feature = "wasm")]
pub use wasm::WasmPlugin;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PluginSpec {
Builtin(BuiltinPlugin),
Wasm {
path: PathBuf,
options: Vec<(String, String)>,
},
}
impl FromStr for PluginSpec {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(',');
let name = parts.next().unwrap_or_default();
let options = parts
.map(|option| {
option
.split_once('=')
.map(|(key, value)| (key.to_string(), value.to_string()))
.ok_or_else(|| {
format!(
"plugin option {option:?} is not key=value; \
repeat the key for several values, e.g. genes=A,genes=B"
)
})
})
.collect::<Result<Vec<_>, _>>()?;
if let Ok(plugin) = name.parse() {
if !options.is_empty() {
return Err(format!("built-in plugin {name} takes no options"));
}
Ok(Self::Builtin(plugin))
} else if name.ends_with(".wasm") {
Ok(Self::Wasm {
path: name.into(),
options,
})
} else {
Err(format!(
"unknown plugin {name:?}, expected one of {} or a path ending in .wasm",
BuiltinPlugin::VARIANTS.iter().join(", ")
))
}
}
}
impl PluginSpec {
pub fn build(&self, run: &RunInfo) -> Result<Arc<dyn TranscriptPlugin>, SeqvarsError> {
match self {
Self::Builtin(plugin) => Ok(plugin.build(run.vep_compatible)),
#[cfg(feature = "wasm")]
Self::Wasm { path, options } => {
Ok(Arc::new(WasmPlugin::load(path, options, run.clone())?))
}
#[cfg(not(feature = "wasm"))]
Self::Wasm { path, .. } => Err(SeqvarsError::Plugin(format!(
"{}: mehari was built without the `wasm` feature",
path.display()
))),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RunInfo {
pub assembly: String,
pub vep_compatible: bool,
}
impl RunInfo {
pub fn new(tx_db: &TxSeqDatabase, vep_compatible: bool) -> Self {
Self {
assembly: tx_db.assembly().to_ascii_lowercase(),
vep_compatible,
}
}
}
#[derive(
Debug, Copy, Clone, PartialEq, Eq, VariantArray, parse_display::FromStr, parse_display::Display,
)]
#[display(style = "kebab-case")]
pub enum BuiltinPlugin {
TxSequenceRef,
TxSequenceAlt,
AaSequenceRef,
AaSequenceAlt,
Nmd,
}
impl BuiltinPlugin {
pub fn build(self, vep_compatible: bool) -> Arc<dyn TranscriptPlugin> {
match self {
Self::TxSequenceRef => Arc::new(SequencePlugin::TxRef),
Self::TxSequenceAlt => Arc::new(SequencePlugin::TxAlt),
Self::AaSequenceRef => Arc::new(SequencePlugin::AaRef),
Self::AaSequenceAlt => Arc::new(SequencePlugin::AaAlt),
Self::Nmd => Arc::new(NmdPlugin { vep_compatible }),
}
}
}
pub trait TranscriptPlugin: Debug + Send + Sync {
fn columns(&self) -> Vec<String>;
fn annotate(
&self,
variants: &[VcfVariant],
txs: &mut Vec<TxAnnotation>,
) -> Result<(), SeqvarsError>;
}
pub struct TxAnnotation {
pub context: TxContext,
pub ann: AnnField,
}
pub struct TxContext {
provider: Option<Arc<MehariProvider>>,
tx_id: Option<String>,
pub var_c: Option<HgvsVariant>,
pub var_n: Option<HgvsVariant>,
region: Option<VariantLocation>,
alt_depends_on_splicing: bool,
ins_at_boundary_is_intronic: bool,
ref_data: OnceCell<Option<Arc<RefTranscriptData>>>,
alt_data: OnceCell<Option<AltTranscriptData>>,
}
impl TxContext {
pub(crate) fn new(
provider: Arc<MehariProvider>,
tx_id: Option<String>,
var_c: Option<HgvsVariant>,
var_n: Option<HgvsVariant>,
) -> Self {
Self {
provider: Some(provider),
tx_id,
var_c,
var_n,
region: None,
alt_depends_on_splicing: false,
ins_at_boundary_is_intronic: true,
ref_data: OnceCell::new(),
alt_data: OnceCell::new(),
}
}
pub(crate) fn empty() -> Self {
Self {
provider: None,
tx_id: None,
var_c: None,
var_n: None,
region: None,
alt_depends_on_splicing: false,
ins_at_boundary_is_intronic: true,
ref_data: OnceCell::new(),
alt_data: OnceCell::new(),
}
}
pub(crate) fn with_alt_inputs(
mut self,
region: Option<VariantLocation>,
alt_depends_on_splicing: bool,
ins_at_boundary_is_intronic: bool,
) -> Self {
self.region = region;
self.alt_depends_on_splicing = alt_depends_on_splicing;
self.ins_at_boundary_is_intronic = ins_at_boundary_is_intronic;
self
}
pub(crate) fn with_ref_data(mut self, ref_data: Arc<RefTranscriptData>) -> Self {
self.ref_data = OnceCell::from(Some(ref_data));
self
}
pub fn tx_span(&self) -> Option<(i32, i32)> {
let HgvsVariant::TxVariant { loc_edit, .. } = self.var_n.as_ref()? else {
return None;
};
let loc = loc_edit.loc.inner();
if loc.start.offset.unwrap_or(0) == 0 && loc.end.offset.unwrap_or(0) == 0 {
return Some((loc.start.base, loc.end.base));
}
if self.alt_depends_on_splicing {
return None;
}
let before = self.var_n.as_ref()?.exon_edge_gap()?;
Some((before, before + 1))
}
pub fn tx(&self) -> Option<&Transcript> {
self.provider.as_ref()?.get_tx(self.tx_id.as_deref()?)
}
pub fn ref_data(&self) -> Option<&RefTranscriptData> {
self.ref_data
.get_or_init(|| {
let tx_id = self.tx_id.as_deref()?;
let provider: Arc<dyn Provider + Send + Sync> = self.provider.clone()?;
ref_transcript_data_shared(&provider, tx_id, None).ok()
})
.as_deref()
}
pub fn alt_data(&self) -> Option<&AltTranscriptData> {
self.alt_data
.get_or_init(|| {
if self.alt_depends_on_splicing {
return None;
}
let ref_data = self.ref_data()?;
match self.region {
Some(
region @ (VariantLocation::FivePrimeUtr | VariantLocation::ThreePrimeUtr),
) => {
let var_n = self.var_n.as_ref()?;
let transcript_sequence =
apply_n_edits(&ref_data.transcript_sequence, &[var_n])?;
let shift = match region {
VariantLocation::FivePrimeUtr => {
transcript_sequence.len() as i32
- ref_data.transcript_sequence.len() as i32
}
_ => 0,
};
Some(AltTranscriptData {
transcript_sequence,
aa_sequence: ref_data.aa_sequence.to_string(),
cds_start: ref_data.cds_start + shift,
cds_stop: ref_data.cds_stop + shift,
protein_accession: ref_data.protein_accession.to_string(),
is_frameshift: false,
variant_start_aa: None,
frameshift_start: None,
is_substitution: false,
is_ambiguous: false,
})
}
_ => {
let var_c = self
.var_c
.as_ref()
.filter(|var| matches!(var, HgvsVariant::CdsVariant { .. }))?;
AltSeqBuilder {
ins_at_boundary_is_intronic: self.ins_at_boundary_is_intronic,
..AltSeqBuilder::new(var_c.clone(), ref_data)
}
.build_altseq()
.ok()?
.into_iter()
.next()
}
}
})
.as_ref()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_plugin_spec() {
assert_eq!("nmd".parse(), Ok(PluginSpec::Builtin(BuiltinPlugin::Nmd)));
assert_eq!(
"plugins/nmd.wasm".parse(),
Ok(PluginSpec::Wasm {
path: "plugins/nmd.wasm".into(),
options: vec![],
})
);
assert_eq!(
"x.wasm,data=/d,panel=a=b".parse(),
Ok(PluginSpec::Wasm {
path: "x.wasm".into(),
options: vec![("data".into(), "/d".into()), ("panel".into(), "a=b".into())],
})
);
assert!("nmdd".parse::<PluginSpec>().is_err());
assert!("nmd,a=b".parse::<PluginSpec>().is_err());
assert!("x.wasm,flag".parse::<PluginSpec>().is_err());
assert_eq!(
"x.wasm,genes=A,genes=B".parse(),
Ok(PluginSpec::Wasm {
path: "x.wasm".into(),
options: vec![("genes".into(), "A".into()), ("genes".into(), "B".into())],
})
);
let error = "x.wasm,genes=A,B".parse::<PluginSpec>().unwrap_err();
assert!(error.contains("repeat the key"), "{error}");
}
#[test]
fn run_info_assembly_is_lowercase() {
let tx_db = TxSeqDatabase {
source_version: vec![crate::pbs::txs::SourceVersion {
assembly: "GRCh38".into(),
..Default::default()
}],
..Default::default()
};
assert_eq!(RunInfo::new(&tx_db, false).assembly, "grch38");
}
}