use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum ClinicalSignificance {
Benign,
LikelyBenign,
UncertainSignificance,
LikelyPathogenic,
Pathogenic,
Conflicting,
DrugResponse,
Association,
RiskFactor,
Protective,
Affects,
#[default]
NotProvided,
Other,
}
impl ClinicalSignificance {
pub fn as_str(&self) -> &'static str {
match self {
Self::Benign => "Benign",
Self::LikelyBenign => "Likely benign",
Self::UncertainSignificance => "Uncertain significance",
Self::LikelyPathogenic => "Likely pathogenic",
Self::Pathogenic => "Pathogenic",
Self::Conflicting => "Conflicting interpretations of pathogenicity",
Self::DrugResponse => "drug response",
Self::Association => "association",
Self::RiskFactor => "risk factor",
Self::Protective => "Protective",
Self::Affects => "Affects",
Self::NotProvided => "not provided",
Self::Other => "other",
}
}
pub fn is_pathogenic(&self) -> bool {
matches!(self, Self::Pathogenic | Self::LikelyPathogenic)
}
pub fn is_benign(&self) -> bool {
matches!(self, Self::Benign | Self::LikelyBenign)
}
pub fn is_uncertain(&self) -> bool {
matches!(self, Self::UncertainSignificance | Self::Conflicting)
}
}
impl std::fmt::Display for ClinicalSignificance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for ClinicalSignificance {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s_lower = s.to_lowercase();
Ok(match s_lower.as_str() {
"benign" => Self::Benign,
"likely benign" | "likely_benign" => Self::LikelyBenign,
"uncertain significance" | "uncertain_significance" | "vus" => {
Self::UncertainSignificance
}
"likely pathogenic" | "likely_pathogenic" => Self::LikelyPathogenic,
"pathogenic" => Self::Pathogenic,
"conflicting interpretations of pathogenicity"
| "conflicting_interpretations_of_pathogenicity"
| "conflicting" => Self::Conflicting,
"drug response" | "drug_response" => Self::DrugResponse,
"association" => Self::Association,
"risk factor" | "risk_factor" => Self::RiskFactor,
"protective" => Self::Protective,
"affects" => Self::Affects,
"not provided" | "not_provided" => Self::NotProvided,
_ => Self::Other,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum ReviewStatus {
#[default]
NoAssertion,
NoAssertionProvided,
ConflictingInterpretations,
SingleSubmitter,
MultipleSubmitters,
ExpertPanel,
PracticeGuideline,
}
impl ReviewStatus {
pub fn stars(&self) -> u8 {
match self {
Self::NoAssertion | Self::NoAssertionProvided => 0,
Self::ConflictingInterpretations | Self::SingleSubmitter => 1,
Self::MultipleSubmitters => 2,
Self::ExpertPanel => 3,
Self::PracticeGuideline => 4,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::NoAssertion => "no assertion criteria provided",
Self::NoAssertionProvided => "no assertion provided",
Self::ConflictingInterpretations => "criteria provided, conflicting interpretations",
Self::SingleSubmitter => "criteria provided, single submitter",
Self::MultipleSubmitters => "criteria provided, multiple submitters, no conflicts",
Self::ExpertPanel => "reviewed by expert panel",
Self::PracticeGuideline => "practice guideline",
}
}
}
impl std::fmt::Display for ReviewStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for ReviewStatus {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s_lower = s.to_lowercase();
Ok(if s_lower.contains("practice guideline") {
Self::PracticeGuideline
} else if s_lower.contains("expert panel") {
Self::ExpertPanel
} else if s_lower.contains("multiple submitters") {
Self::MultipleSubmitters
} else if s_lower.contains("single submitter") {
Self::SingleSubmitter
} else if s_lower.contains("conflicting") {
Self::ConflictingInterpretations
} else if s_lower.contains("no assertion provided") {
Self::NoAssertionProvided
} else {
Self::NoAssertion
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct ConditionInfo {
pub name: String,
pub medgen_id: Option<String>,
pub omim_id: Option<String>,
pub orphanet_id: Option<String>,
}
impl ConditionInfo {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
pub fn with_medgen(mut self, id: impl Into<String>) -> Self {
self.medgen_id = Some(id.into());
self
}
pub fn with_omim(mut self, id: impl Into<String>) -> Self {
self.omim_id = Some(id.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct SubmitterInfo {
pub name: String,
pub org_id: Option<String>,
pub date: Option<String>,
}
impl SubmitterInfo {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ClinVarRecord {
pub variation_id: String,
pub accession: Option<String>,
pub hgvs: String,
pub hgvs_aliases: Vec<String>,
pub gene: Option<String>,
pub significance: ClinicalSignificance,
pub review_status: String,
pub parsed_review_status: Option<ReviewStatus>,
pub conditions: Vec<ConditionInfo>,
pub submitters: Vec<SubmitterInfo>,
pub rsid: Option<String>,
pub last_evaluated: Option<String>,
pub last_updated: Option<String>,
}
impl ClinVarRecord {
pub fn new(variation_id: impl Into<String>, hgvs: impl Into<String>) -> Self {
Self {
variation_id: variation_id.into(),
hgvs: hgvs.into(),
..Default::default()
}
}
pub fn stars(&self) -> u8 {
self.parsed_review_status
.map(|r| r.stars())
.unwrap_or_else(|| {
self.review_status
.parse::<ReviewStatus>()
.unwrap_or_default()
.stars()
})
}
pub fn is_high_confidence_pathogenic(&self) -> bool {
self.significance.is_pathogenic() && self.stars() >= 2
}
pub fn is_high_confidence_benign(&self) -> bool {
self.significance.is_benign() && self.stars() >= 2
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clinical_significance_from_str() {
assert_eq!(
"Pathogenic".parse::<ClinicalSignificance>().unwrap(),
ClinicalSignificance::Pathogenic
);
assert_eq!(
"likely benign".parse::<ClinicalSignificance>().unwrap(),
ClinicalSignificance::LikelyBenign
);
assert_eq!(
"VUS".parse::<ClinicalSignificance>().unwrap(),
ClinicalSignificance::UncertainSignificance
);
assert_eq!(
"unknown".parse::<ClinicalSignificance>().unwrap(),
ClinicalSignificance::Other
);
}
#[test]
fn test_clinical_significance_is_pathogenic() {
assert!(ClinicalSignificance::Pathogenic.is_pathogenic());
assert!(ClinicalSignificance::LikelyPathogenic.is_pathogenic());
assert!(!ClinicalSignificance::Benign.is_pathogenic());
assert!(!ClinicalSignificance::UncertainSignificance.is_pathogenic());
}
#[test]
fn test_clinical_significance_is_benign() {
assert!(ClinicalSignificance::Benign.is_benign());
assert!(ClinicalSignificance::LikelyBenign.is_benign());
assert!(!ClinicalSignificance::Pathogenic.is_benign());
}
#[test]
fn test_clinical_significance_is_uncertain() {
assert!(ClinicalSignificance::UncertainSignificance.is_uncertain());
assert!(ClinicalSignificance::Conflicting.is_uncertain());
assert!(!ClinicalSignificance::Pathogenic.is_uncertain());
}
#[test]
fn test_review_status_from_str() {
assert_eq!(
"criteria provided, single submitter"
.parse::<ReviewStatus>()
.unwrap(),
ReviewStatus::SingleSubmitter
);
assert_eq!(
"reviewed by expert panel".parse::<ReviewStatus>().unwrap(),
ReviewStatus::ExpertPanel
);
assert_eq!(
"practice guideline".parse::<ReviewStatus>().unwrap(),
ReviewStatus::PracticeGuideline
);
}
#[test]
fn test_review_status_stars() {
assert_eq!(ReviewStatus::NoAssertion.stars(), 0);
assert_eq!(ReviewStatus::SingleSubmitter.stars(), 1);
assert_eq!(ReviewStatus::MultipleSubmitters.stars(), 2);
assert_eq!(ReviewStatus::ExpertPanel.stars(), 3);
assert_eq!(ReviewStatus::PracticeGuideline.stars(), 4);
}
#[test]
fn test_clinvar_record_new() {
let record = ClinVarRecord::new("12345", "NM_000088.3:c.10A>G");
assert_eq!(record.variation_id, "12345");
assert_eq!(record.hgvs, "NM_000088.3:c.10A>G");
}
#[test]
fn test_clinvar_record_stars() {
let record = ClinVarRecord {
variation_id: "12345".to_string(),
hgvs: "NM_000088.3:c.10A>G".to_string(),
review_status: "criteria provided, single submitter".to_string(),
..Default::default()
};
assert_eq!(record.stars(), 1);
}
#[test]
fn test_clinvar_record_high_confidence_pathogenic() {
let record = ClinVarRecord {
variation_id: "12345".to_string(),
hgvs: "NM_000088.3:c.10A>G".to_string(),
significance: ClinicalSignificance::Pathogenic,
review_status: "criteria provided, multiple submitters".to_string(),
..Default::default()
};
assert!(record.is_high_confidence_pathogenic());
let low_confidence = ClinVarRecord {
review_status: "no assertion criteria provided".to_string(),
..record.clone()
};
assert!(!low_confidence.is_high_confidence_pathogenic());
}
#[test]
fn test_condition_info_builder() {
let condition = ConditionInfo::new("Hereditary cancer syndrome")
.with_medgen("C1234567")
.with_omim("123456");
assert_eq!(condition.name, "Hereditary cancer syndrome");
assert_eq!(condition.medgen_id, Some("C1234567".to_string()));
assert_eq!(condition.omim_id, Some("123456".to_string()));
}
}