use crate::error::VarEffectError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakendOrientation {
JoinAfterMateRight,
JoinAfterMateLeft,
JoinBeforeMateLeft,
JoinBeforeMateRight,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BreakendMate {
pub chrom: String,
pub pos: u64,
pub orientation: BreakendOrientation,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Breakend {
pub ref_bases: String,
pub mate: Option<BreakendMate>,
}
impl Breakend {
pub fn parse(alt: &str) -> Result<Self, VarEffectError> {
let invalid = || VarEffectError::InvalidBreakend(alt.to_string());
if let Some(t) = alt.strip_suffix('.') {
validate_ref_bases(t, alt)?;
return Ok(Self {
ref_bases: t.to_string(),
mate: None,
});
}
if let Some(t) = alt.strip_prefix('.') {
validate_ref_bases(t, alt)?;
return Ok(Self {
ref_bases: t.to_string(),
mate: None,
});
}
let bytes = alt.as_bytes();
let (first, last) = match (bytes.first(), bytes.last()) {
(Some(&f), Some(&l)) => (f, l),
_ => return Err(invalid()),
};
let (bracket, local_after) = if first == b'[' || first == b']' {
(first, true)
} else if last == b'[' || last == b']' {
(last, false)
} else {
return Err(invalid());
};
let parts: Vec<&str> = alt.split(bracket as char).collect();
if parts.len() != 3 {
return Err(invalid());
}
let (ref_bases, locus) = if local_after {
(parts[2], parts[1])
} else {
(parts[0], parts[1])
};
validate_ref_bases(ref_bases, alt)?;
let (chrom, pos_str) = locus.rsplit_once(':').ok_or_else(invalid)?;
let pos: u64 = pos_str.parse().map_err(|_| invalid())?;
if chrom.is_empty() {
return Err(invalid());
}
let orientation = match (bracket, local_after) {
(b'[', false) => BreakendOrientation::JoinAfterMateRight,
(b']', false) => BreakendOrientation::JoinAfterMateLeft,
(b']', true) => BreakendOrientation::JoinBeforeMateLeft,
(b'[', true) => BreakendOrientation::JoinBeforeMateRight,
_ => return Err(invalid()),
};
Ok(Self {
ref_bases: ref_bases.to_string(),
mate: Some(BreakendMate {
chrom: chrom.to_string(),
pos,
orientation,
}),
})
}
}
fn validate_ref_bases(t: &str, alt: &str) -> Result<(), VarEffectError> {
if t.is_empty()
|| !t.bytes().all(|b| {
matches!(
b,
b'A' | b'C' | b'G' | b'T' | b'N' | b'a' | b'c' | b'g' | b't' | b'n'
)
})
{
return Err(VarEffectError::InvalidBreakend(alt.to_string()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_t_bracket_right() {
let bnd = Breakend::parse("G[17:198982[").unwrap();
assert_eq!(bnd.ref_bases, "G");
let mate = bnd.mate.unwrap();
assert_eq!(mate.chrom, "17");
assert_eq!(mate.pos, 198_982);
assert_eq!(mate.orientation, BreakendOrientation::JoinAfterMateRight);
}
#[test]
fn parses_t_bracket_left() {
let bnd = Breakend::parse("T]chr12:58877476]").unwrap();
assert_eq!(bnd.ref_bases, "T");
let mate = bnd.mate.unwrap();
assert_eq!(mate.chrom, "chr12");
assert_eq!(mate.pos, 58_877_476);
assert_eq!(mate.orientation, BreakendOrientation::JoinAfterMateLeft);
}
#[test]
fn parses_bracket_left_t() {
let bnd = Breakend::parse("]13:123456]A").unwrap();
assert_eq!(bnd.ref_bases, "A");
let mate = bnd.mate.unwrap();
assert_eq!(mate.chrom, "13");
assert_eq!(mate.pos, 123_456);
assert_eq!(mate.orientation, BreakendOrientation::JoinBeforeMateLeft);
}
#[test]
fn parses_bracket_right_t() {
let bnd = Breakend::parse("[2:321682[C").unwrap();
assert_eq!(bnd.ref_bases, "C");
let mate = bnd.mate.unwrap();
assert_eq!(mate.chrom, "2");
assert_eq!(mate.pos, 321_682);
assert_eq!(mate.orientation, BreakendOrientation::JoinBeforeMateRight);
}
#[test]
fn parses_multibase_local_sequence() {
let bnd = Breakend::parse("GACGT[17:198982[").unwrap();
assert_eq!(bnd.ref_bases, "GACGT");
assert_eq!(bnd.mate.unwrap().pos, 198_982);
}
#[test]
fn parses_single_breakend_suffix() {
let bnd = Breakend::parse("G.").unwrap();
assert_eq!(bnd.ref_bases, "G");
assert!(bnd.mate.is_none());
}
#[test]
fn parses_single_breakend_prefix() {
let bnd = Breakend::parse(".G").unwrap();
assert_eq!(bnd.ref_bases, "G");
assert!(bnd.mate.is_none());
}
#[test]
fn rejects_plain_allele() {
assert!(matches!(
Breakend::parse("ACGT"),
Err(VarEffectError::InvalidBreakend(_))
));
}
#[test]
fn rejects_missing_position() {
assert!(matches!(
Breakend::parse("G[17:[",),
Err(VarEffectError::InvalidBreakend(_))
));
}
#[test]
fn rejects_empty_local_base() {
assert!(matches!(
Breakend::parse("[2:321682["),
Err(VarEffectError::InvalidBreakend(_))
));
}
#[test]
fn rejects_non_acgtn_local_base() {
assert!(matches!(
Breakend::parse("X[2:321682["),
Err(VarEffectError::InvalidBreakend(_))
));
}
}