From ebb6c81912096831c4c2d853fddb3cb24845f2f7 Mon Sep 17 00:00:00 2001 From: Michael Macias Date: Sat, 5 Oct 2024 15:56:43 -0500 Subject: [PATCH] cram/record/feature: Use named fields --- .../substitution_matrix/builder.rs | 63 +-- noodles-cram/src/data_container/slice.rs | 16 +- noodles-cram/src/io/reader/record.rs | 41 +- noodles-cram/src/io/writer/record.rs | 30 +- noodles-cram/src/record.rs | 40 +- noodles-cram/src/record/feature.rs | 267 ++++++++---- noodles-cram/src/record/features.rs | 381 +++++++++++++----- noodles-cram/src/record/features/cigar.rs | 52 ++- .../src/record/features/with_positions.rs | 34 +- noodles-cram/src/record/resolve.rs | 164 +++++--- 10 files changed, 746 insertions(+), 342 deletions(-) diff --git a/noodles-cram/src/data_container/compression_header/preservation_map/substitution_matrix/builder.rs b/noodles-cram/src/data_container/compression_header/preservation_map/substitution_matrix/builder.rs index 3e7ef9fd0..d38805fb0 100644 --- a/noodles-cram/src/data_container/compression_header/preservation_map/substitution_matrix/builder.rs +++ b/noodles-cram/src/data_container/compression_header/preservation_map/substitution_matrix/builder.rs @@ -12,14 +12,15 @@ pub struct Builder { impl Builder { pub fn update(&mut self, record: &Record) { for feature in record.features().iter() { - match feature { - Feature::Substitution(_, substitution::Value::Bases(reference_base, read_base)) => { - self.histogram.hit(*reference_base, *read_base); + if let Feature::Substitution { value, .. } = feature { + match value { + substitution::Value::Bases(reference_base, read_base) => { + self.histogram.hit(*reference_base, *read_base); + } + substitution::Value::Code(_) => { + panic!("substitution matrix cannot be built from substitution codes"); + } } - Feature::Substitution(_, substitution::Value::Code(_)) => { - panic!("substitution matrix cannot be built from substitution codes"); - } - _ => {} } } } @@ -51,30 +52,30 @@ mod tests { .set_read_length(bases.len()) .set_bases(bases) .set_features(Features::from(vec![ - Feature::Substitution( - Position::try_from(1)?, - substitution::Value::Bases(Base::A, Base::T), - ), - Feature::Substitution( - Position::try_from(3)?, - substitution::Value::Bases(Base::A, Base::T), - ), - Feature::Substitution( - Position::try_from(6)?, - substitution::Value::Bases(Base::A, Base::C), - ), - Feature::Substitution( - Position::try_from(7)?, - substitution::Value::Bases(Base::A, Base::G), - ), - Feature::Substitution( - Position::try_from(9)?, - substitution::Value::Bases(Base::A, Base::G), - ), - Feature::Substitution( - Position::try_from(10)?, - substitution::Value::Bases(Base::A, Base::T), - ), + Feature::Substitution { + position: Position::try_from(1)?, + value: substitution::Value::Bases(Base::A, Base::T), + }, + Feature::Substitution { + position: Position::try_from(3)?, + value: substitution::Value::Bases(Base::A, Base::T), + }, + Feature::Substitution { + position: Position::try_from(6)?, + value: substitution::Value::Bases(Base::A, Base::C), + }, + Feature::Substitution { + position: Position::try_from(7)?, + value: substitution::Value::Bases(Base::A, Base::G), + }, + Feature::Substitution { + position: Position::try_from(9)?, + value: substitution::Value::Bases(Base::A, Base::G), + }, + Feature::Substitution { + position: Position::try_from(10)?, + value: substitution::Value::Bases(Base::A, Base::T), + }, ])) .build(); diff --git a/noodles-cram/src/data_container/slice.rs b/noodles-cram/src/data_container/slice.rs index ccd85fcf2..efb05665c 100644 --- a/noodles-cram/src/data_container/slice.rs +++ b/noodles-cram/src/data_container/slice.rs @@ -649,10 +649,10 @@ mod tests { .set_reference_sequence_id(0) .set_read_length(2) .set_alignment_start(Position::MIN) - .set_features(Features::from(vec![Feature::Bases( - Position::MIN, - vec![b'A', b'C'], - )])) + .set_features(Features::from(vec![Feature::Bases { + position: Position::MIN, + bases: vec![b'A', b'C'], + }])) .build()]; resolve_bases( @@ -681,10 +681,10 @@ mod tests { .set_id(1) .set_bam_flags(sam::alignment::record::Flags::empty()) .set_read_length(2) - .set_features(Features::from(vec![Feature::Scores( - Position::try_from(1)?, - vec![8, 13], - )])) + .set_features(Features::from(vec![Feature::Scores { + position: Position::try_from(1)?, + quality_scores: vec![8, 13], + }])) .build(), Record::builder().set_id(2).build(), Record::builder() diff --git a/noodles-cram/src/io/reader/record.rs b/noodles-cram/src/io/reader/record.rs index 6fc214dbd..ee1442d31 100644 --- a/noodles-cram/src/io/reader/record.rs +++ b/noodles-cram/src/io/reader/record.rs @@ -465,52 +465,65 @@ where match code { Code::Bases => { let bases = self.read_stretches_of_bases()?; - Ok(Feature::Bases(position, bases)) + Ok(Feature::Bases { position, bases }) } Code::Scores => { let quality_scores = self.read_stretches_of_quality_scores()?; - Ok(Feature::Scores(position, quality_scores)) + + Ok(Feature::Scores { + position, + quality_scores, + }) } Code::ReadBase => { let base = self.read_base()?; let quality_score = self.read_quality_score()?; - Ok(Feature::ReadBase(position, base, quality_score)) + + Ok(Feature::ReadBase { + position, + base, + quality_score, + }) } Code::Substitution => { - let code = self.read_base_substitution_code()?; - Ok(Feature::Substitution(position, code)) + let value = self.read_base_substitution_code()?; + Ok(Feature::Substitution { position, value }) } Code::Insertion => { let bases = self.read_insertion_bases()?; - Ok(Feature::Insertion(position, bases)) + Ok(Feature::Insertion { position, bases }) } Code::Deletion => { let len = self.read_deletion_length()?; - Ok(Feature::Deletion(position, len)) + Ok(Feature::Deletion { position, len }) } Code::InsertBase => { let base = self.read_base()?; - Ok(Feature::InsertBase(position, base)) + Ok(Feature::InsertBase { position, base }) } Code::QualityScore => { - let score = self.read_quality_score()?; - Ok(Feature::QualityScore(position, score)) + let quality_score = self.read_quality_score()?; + + Ok(Feature::QualityScore { + position, + quality_score, + }) } Code::ReferenceSkip => { let len = self.read_reference_skip_length()?; - Ok(Feature::ReferenceSkip(position, len)) + Ok(Feature::ReferenceSkip { position, len }) } Code::SoftClip => { let bases = self.read_soft_clip_bases()?; - Ok(Feature::SoftClip(position, bases)) + Ok(Feature::SoftClip { position, bases }) } Code::Padding => { let len = self.read_padding_length()?; - Ok(Feature::Padding(position, len)) + Ok(Feature::Padding { position, len }) } Code::HardClip => { let len = self.read_hard_clip_length()?; - Ok(Feature::HardClip(position, len)) + Ok(Feature::HardClip { position, len }) } } } diff --git a/noodles-cram/src/io/writer/record.rs b/noodles-cram/src/io/writer/record.rs index eb6b2bcb0..a3440b814 100644 --- a/noodles-cram/src/io/writer/record.rs +++ b/noodles-cram/src/io/writer/record.rs @@ -501,41 +501,45 @@ where self.write_feature_position(position)?; match feature { - Feature::Bases(_, bases) => { + Feature::Bases { bases, .. } => { self.write_stretches_of_bases(bases)?; } - Feature::Scores(_, quality_scores) => { + Feature::Scores { quality_scores, .. } => { self.write_stretches_of_quality_scores(quality_scores)?; } - Feature::ReadBase(_, base, quality_score) => { + Feature::ReadBase { + base, + quality_score, + .. + } => { self.write_base(*base)?; self.write_quality_score(*quality_score)?; } - Feature::Substitution(_, value) => { + Feature::Substitution { value, .. } => { self.write_base_substitution_code(*value)?; } - Feature::Insertion(_, bases) => { + Feature::Insertion { bases, .. } => { self.write_insertion(bases)?; } - Feature::Deletion(_, len) => { + Feature::Deletion { len, .. } => { self.write_deletion_length(*len)?; } - Feature::InsertBase(_, base) => { + Feature::InsertBase { base, .. } => { self.write_base(*base)?; } - Feature::QualityScore(_, score) => { - self.write_quality_score(*score)?; + Feature::QualityScore { quality_score, .. } => { + self.write_quality_score(*quality_score)?; } - Feature::ReferenceSkip(_, len) => { + Feature::ReferenceSkip { len, .. } => { self.write_reference_skip_length(*len)?; } - Feature::SoftClip(_, bases) => { + Feature::SoftClip { bases, .. } => { self.write_soft_clip(bases)?; } - Feature::Padding(_, len) => { + Feature::Padding { len, .. } => { self.write_padding(*len)?; } - Feature::HardClip(_, len) => { + Feature::HardClip { len, .. } => { self.write_hard_clip(*len)?; } } diff --git a/noodles-cram/src/record.rs b/noodles-cram/src/record.rs index 4e51ec0ec..665523b2c 100644 --- a/noodles-cram/src/record.rs +++ b/noodles-cram/src/record.rs @@ -338,11 +338,11 @@ pub(crate) fn calculate_alignment_span(read_length: usize, features: &Features) features .iter() .fold(read_length, |alignment_span, feature| match feature { - Feature::Insertion(_, bases) => alignment_span - bases.len(), - Feature::InsertBase(_, _) => alignment_span - 1, - Feature::Deletion(_, len) => alignment_span + len, - Feature::ReferenceSkip(_, len) => alignment_span + len, - Feature::SoftClip(_, bases) => alignment_span - bases.len(), + Feature::Insertion { bases, .. } => alignment_span - bases.len(), + Feature::InsertBase { .. } => alignment_span - 1, + Feature::Deletion { len, .. } => alignment_span + len, + Feature::ReferenceSkip { len, .. } => alignment_span + len, + Feature::SoftClip { bases, .. } => alignment_span - bases.len(), _ => alignment_span, }) } @@ -370,15 +370,33 @@ mod tests { let features = Features::default(); assert_eq!(calculate_alignment_span(4, &features), 4); - let features = Features::from(vec![Feature::HardClip(Position::try_from(1)?, 4)]); + let features = Features::from(vec![Feature::HardClip { + position: Position::try_from(1)?, + len: 4, + }]); assert_eq!(calculate_alignment_span(4, &features), 4); let features = Features::from(vec![ - Feature::Insertion(Position::try_from(1)?, vec![b'A', b'C']), - Feature::InsertBase(Position::try_from(4)?, b'G'), - Feature::Deletion(Position::try_from(6)?, 3), - Feature::ReferenceSkip(Position::try_from(10)?, 5), - Feature::SoftClip(Position::try_from(16)?, vec![b'A', b'C', b'G', b'T']), + Feature::Insertion { + position: Position::try_from(1)?, + bases: vec![b'A', b'C'], + }, + Feature::InsertBase { + position: Position::try_from(4)?, + base: b'G', + }, + Feature::Deletion { + position: Position::try_from(6)?, + len: 3, + }, + Feature::ReferenceSkip { + position: Position::try_from(10)?, + len: 5, + }, + Feature::SoftClip { + position: Position::try_from(16)?, + bases: vec![b'A', b'C', b'G', b'T'], + }, ]); assert_eq!(calculate_alignment_span(20, &features), 21); diff --git a/noodles-cram/src/record/feature.rs b/noodles-cram/src/record/feature.rs index 9289c4cf2..f7a6889c4 100644 --- a/noodles-cram/src/record/feature.rs +++ b/noodles-cram/src/record/feature.rs @@ -8,32 +8,46 @@ pub use self::code::Code; use noodles_core::Position; /// A CRAM record feature. +#[allow(missing_docs)] #[derive(Clone, Debug, Eq, PartialEq)] pub enum Feature { - /// A stretch of bases (position, bases). - Bases(Position, Vec), - /// A stretch of quality scores (position, quality scores). - Scores(Position, Vec), - /// A base-quality score pair (position, base, quality score). - ReadBase(Position, u8, u8), - /// A base substitution (position, code (read) / base (write)). - Substitution(Position, substitution::Value), - /// Inserted bases (position, bases). - Insertion(Position, Vec), - /// A number of deleted bases (position, length). - Deletion(Position, usize), - /// A single inserted base (position, base). - InsertBase(Position, u8), - /// A single quality score (position, score). - QualityScore(Position, u8), - /// A number of skipped bases (position, length). - ReferenceSkip(Position, usize), - /// Soft clipped bases (position, bases). - SoftClip(Position, Vec), - /// A number of padded bases (position, length). - Padding(Position, usize), - /// A number of hard clipped bases (position, length). - HardClip(Position, usize), + /// A stretch of bases. + Bases { position: Position, bases: Vec }, + /// A stretch of quality scores. + Scores { + position: Position, + quality_scores: Vec, + }, + /// A base-quality score pair. + ReadBase { + position: Position, + base: u8, + quality_score: u8, + }, + /// A base substitution. + Substitution { + position: Position, + value: substitution::Value, + }, + /// Inserted bases. + Insertion { position: Position, bases: Vec }, + /// A number of deleted bases. + Deletion { position: Position, len: usize }, + /// A single inserted base. + InsertBase { position: Position, base: u8 }, + /// A single quality score. + QualityScore { + position: Position, + quality_score: u8, + }, + /// A number of skipped bases. + ReferenceSkip { position: Position, len: usize }, + /// Soft clipped bases. + SoftClip { position: Position, bases: Vec }, + /// A number of padded bases. + Padding { position: Position, len: usize }, + /// A number of hard clipped bases. + HardClip { position: Position, len: usize }, } impl Feature { @@ -46,25 +60,25 @@ impl Feature { /// use noodles_cram::record::{feature::Code, Feature}; /// /// let position = Position::try_from(8)?; - /// let feature = Feature::Padding(position, 13); + /// let feature = Feature::Padding{ position, len: 13 }; /// /// assert_eq!(feature.code(), Code::Padding); /// # Ok::<_, noodles_core::position::TryFromIntError>(()) /// ``` pub fn code(&self) -> Code { match self { - Self::Bases(..) => Code::Bases, - Self::Scores(..) => Code::Scores, - Self::ReadBase(..) => Code::ReadBase, - Self::Substitution(..) => Code::Substitution, - Self::Insertion(..) => Code::Insertion, - Self::Deletion(..) => Code::Deletion, - Self::InsertBase(..) => Code::InsertBase, - Self::QualityScore(..) => Code::QualityScore, - Self::ReferenceSkip(..) => Code::ReferenceSkip, - Self::SoftClip(..) => Code::SoftClip, - Self::Padding(..) => Code::Padding, - Self::HardClip(..) => Code::HardClip, + Self::Bases { .. } => Code::Bases, + Self::Scores { .. } => Code::Scores, + Self::ReadBase { .. } => Code::ReadBase, + Self::Substitution { .. } => Code::Substitution, + Self::Insertion { .. } => Code::Insertion, + Self::Deletion { .. } => Code::Deletion, + Self::InsertBase { .. } => Code::InsertBase, + Self::QualityScore { .. } => Code::QualityScore, + Self::ReferenceSkip { .. } => Code::ReferenceSkip, + Self::SoftClip { .. } => Code::SoftClip, + Self::Padding { .. } => Code::Padding, + Self::HardClip { .. } => Code::HardClip, } } @@ -77,25 +91,25 @@ impl Feature { /// use noodles_cram::record::Feature; /// /// let position = Position::try_from(8)?; - /// let feature = Feature::Padding(position, 13); + /// let feature = Feature::Padding{ position, len: 13 }; /// /// assert_eq!(feature.position(), position); /// # Ok::<_, noodles_core::position::TryFromIntError>(()) /// ``` pub fn position(&self) -> Position { match self { - Self::Bases(pos, _) => *pos, - Self::Scores(pos, _) => *pos, - Self::ReadBase(pos, _, _) => *pos, - Self::Substitution(pos, _) => *pos, - Self::Insertion(pos, _) => *pos, - Self::Deletion(pos, _) => *pos, - Self::InsertBase(pos, _) => *pos, - Self::QualityScore(pos, _) => *pos, - Self::ReferenceSkip(pos, _) => *pos, - Self::SoftClip(pos, _) => *pos, - Self::Padding(pos, _) => *pos, - Self::HardClip(pos, _) => *pos, + Self::Bases { position, .. } => *position, + Self::Scores { position, .. } => *position, + Self::ReadBase { position, .. } => *position, + Self::Substitution { position, .. } => *position, + Self::Insertion { position, .. } => *position, + Self::Deletion { position, .. } => *position, + Self::InsertBase { position, .. } => *position, + Self::QualityScore { position, .. } => *position, + Self::ReferenceSkip { position, .. } => *position, + Self::SoftClip { position, .. } => *position, + Self::Padding { position, .. } => *position, + Self::HardClip { position, .. } => *position, } } } @@ -108,56 +122,161 @@ mod tests { fn test_code() { let position = Position::MIN; - assert_eq!(Feature::Bases(position, Vec::new()).code(), Code::Bases); - assert_eq!(Feature::Scores(position, Vec::new()).code(), Code::Scores); - assert_eq!(Feature::ReadBase(position, b'N', 0).code(), Code::ReadBase); assert_eq!( - Feature::Substitution(position, substitution::Value::Code(0)).code(), + Feature::Bases { + position, + bases: Vec::new() + } + .code(), + Code::Bases + ); + assert_eq!( + Feature::Scores { + position, + quality_scores: Vec::new() + } + .code(), + Code::Scores + ); + assert_eq!( + Feature::ReadBase { + position, + base: b'N', + quality_score: 0 + } + .code(), + Code::ReadBase + ); + assert_eq!( + Feature::Substitution { + position, + value: substitution::Value::Code(0) + } + .code(), Code::Substitution ); assert_eq!( - Feature::Insertion(position, Vec::new()).code(), + Feature::Insertion { + position, + bases: Vec::new() + } + .code(), Code::Insertion ); - assert_eq!(Feature::Deletion(position, 0).code(), Code::Deletion); - assert_eq!(Feature::InsertBase(position, b'N').code(), Code::InsertBase); assert_eq!( - Feature::QualityScore(position, 0).code(), + Feature::Deletion { position, len: 0 }.code(), + Code::Deletion + ); + assert_eq!( + Feature::InsertBase { + position, + base: b'N' + } + .code(), + Code::InsertBase + ); + assert_eq!( + Feature::QualityScore { + position, + quality_score: 0 + } + .code(), Code::QualityScore ); assert_eq!( - Feature::ReferenceSkip(position, 0).code(), + Feature::ReferenceSkip { position, len: 0 }.code(), Code::ReferenceSkip ); assert_eq!( - Feature::SoftClip(position, Vec::new()).code(), + Feature::SoftClip { + position, + bases: Vec::new() + } + .code(), Code::SoftClip ); - assert_eq!(Feature::Padding(position, 0).code(), Code::Padding); - assert_eq!(Feature::HardClip(position, 0).code(), Code::HardClip); + assert_eq!(Feature::Padding { position, len: 0 }.code(), Code::Padding); + assert_eq!( + Feature::HardClip { position, len: 0 }.code(), + Code::HardClip + ); } #[test] fn test_position() { let position = Position::MIN; - assert_eq!(Feature::Bases(position, Vec::new()).position(), position); - assert_eq!(Feature::Scores(position, Vec::new()).position(), position); - assert_eq!(Feature::ReadBase(position, b'N', 0).position(), position); assert_eq!( - Feature::Substitution(position, substitution::Value::Code(0)).position(), + Feature::Bases { + position, + bases: Vec::new() + } + .position(), + position + ); + assert_eq!( + Feature::Scores { + position, + quality_scores: Vec::new() + } + .position(), + position + ); + assert_eq!( + Feature::ReadBase { + position, + base: b'N', + quality_score: 0 + } + .position(), + position + ); + assert_eq!( + Feature::Substitution { + position, + value: substitution::Value::Code(0) + } + .position(), + position + ); + assert_eq!( + Feature::Insertion { + position, + bases: Vec::new() + } + .position(), + position + ); + assert_eq!(Feature::Deletion { position, len: 0 }.position(), position); + assert_eq!( + Feature::InsertBase { + position, + base: b'N' + } + .position(), + position + ); + assert_eq!( + Feature::QualityScore { + position, + quality_score: 0 + } + .position(), + position + ); + assert_eq!( + Feature::ReferenceSkip { position, len: 0 }.position(), position ); assert_eq!( - Feature::Insertion(position, Vec::new()).position(), + Feature::SoftClip { + position, + bases: Vec::new() + } + .position(), position ); - assert_eq!(Feature::Deletion(position, 0).position(), position); - assert_eq!(Feature::InsertBase(position, b'N').position(), position); - assert_eq!(Feature::QualityScore(position, 0).position(), position); - assert_eq!(Feature::ReferenceSkip(position, 0).position(), position); - assert_eq!(Feature::SoftClip(position, Vec::new()).position(), position); - assert_eq!(Feature::Padding(position, 0).position(), position); - assert_eq!(Feature::HardClip(position, 0).position(), position); + assert_eq!(Feature::Padding { position, len: 0 }.position(), position); + assert_eq!(Feature::HardClip { position, len: 0 }.position(), position); } } diff --git a/noodles-cram/src/record/features.rs b/noodles-cram/src/record/features.rs index f1b349bbc..3d0aa7f37 100644 --- a/noodles-cram/src/record/features.rs +++ b/noodles-cram/src/record/features.rs @@ -61,14 +61,14 @@ impl Features { } let (kind, len) = match feature { - Feature::Substitution(..) => (Kind::Match, 1), - Feature::Insertion(_, bases) => (Kind::Insertion, bases.len()), - Feature::Deletion(_, len) => (Kind::Deletion, *len), - Feature::InsertBase(..) => (Kind::Insertion, 1), - Feature::ReferenceSkip(_, len) => (Kind::Skip, *len), - Feature::SoftClip(_, bases) => (Kind::SoftClip, bases.len()), - Feature::Padding(_, len) => (Kind::Pad, *len), - Feature::HardClip(_, len) => (Kind::HardClip, *len), + Feature::Substitution { .. } => (Kind::Match, 1), + Feature::Insertion { bases, .. } => (Kind::Insertion, bases.len()), + Feature::Deletion { len, .. } => (Kind::Deletion, *len), + Feature::InsertBase { .. } => (Kind::Insertion, 1), + Feature::ReferenceSkip { len, .. } => (Kind::Skip, *len), + Feature::SoftClip { bases, .. } => (Kind::SoftClip, bases.len()), + Feature::Padding { len, .. } => (Kind::Pad, *len), + Feature::HardClip { len, .. } => (Kind::HardClip, *len), _ => continue, }; @@ -134,79 +134,119 @@ fn cigar_to_features( use sam::alignment::record::cigar::op::Kind; let mut features = Features::default(); - let mut read_position = Position::MIN; + let mut position = Position::MIN; for op in cigar.as_ref().iter() { match op.kind() { Kind::Match | Kind::SequenceMatch | Kind::SequenceMismatch => { if op.len() == 1 { - let base = sequence[read_position]; - let score = quality_scores[read_position]; - features.push(Feature::ReadBase(read_position, base, score)); + let base = sequence[position]; + let quality_score = quality_scores[position]; + + features.push(Feature::ReadBase { + position, + base, + quality_score, + }); } else { - let end = read_position + let end = position .checked_add(op.len()) .expect("attempt to add with overflow"); - let bases = sequence[read_position..end].to_vec(); - features.push(Feature::Bases(read_position, bases)); + let bases = sequence[position..end].to_vec(); + features.push(Feature::Bases { position, bases }); if !flags.are_quality_scores_stored_as_array() { - let scores = quality_scores[read_position..end].to_vec(); - features.push(Feature::Scores(read_position, scores)); + let quality_scores = quality_scores[position..end].to_vec(); + + features.push(Feature::Scores { + position, + quality_scores, + }); } } } Kind::Insertion => { if op.len() == 1 { - let base = sequence[read_position]; - features.push(Feature::InsertBase(read_position, base)); + let base = sequence[position]; + features.push(Feature::InsertBase { position, base }); if !flags.are_quality_scores_stored_as_array() { - let score = quality_scores[read_position]; - features.push(Feature::QualityScore(read_position, score)); + let quality_score = quality_scores[position]; + + features.push(Feature::QualityScore { + position, + quality_score, + }); } } else { - let end = read_position + let end = position .checked_add(op.len()) .expect("attempt to add with overflow"); - let bases = sequence[read_position..end].to_vec(); - features.push(Feature::Insertion(read_position, bases)); + let bases = sequence[position..end].to_vec(); + features.push(Feature::Insertion { position, bases }); if !flags.are_quality_scores_stored_as_array() { - let scores = quality_scores[read_position..end].to_vec(); - features.push(Feature::Scores(read_position, scores)); + let quality_scores = quality_scores[position..end].to_vec(); + + features.push(Feature::Scores { + position, + quality_scores, + }); } } } - Kind::Deletion => features.push(Feature::Deletion(read_position, op.len())), - Kind::Skip => features.push(Feature::ReferenceSkip(read_position, op.len())), + Kind::Deletion => features.push(Feature::Deletion { + position, + len: op.len(), + }), + Kind::Skip => features.push(Feature::ReferenceSkip { + position, + len: op.len(), + }), Kind::SoftClip => { - let end = read_position + let end = position .checked_add(op.len()) .expect("attempt to add with overflow"); - let bases = &sequence[read_position..end]; + let bases = &sequence[position..end]; - features.push(Feature::SoftClip(read_position, bases.to_vec())); + features.push(Feature::SoftClip { + position, + bases: bases.to_vec(), + }); if !flags.are_quality_scores_stored_as_array() { if bases.len() == 1 { - let score = quality_scores[read_position]; - features.push(Feature::QualityScore(read_position, score)); + let quality_score = quality_scores[position]; + + features.push(Feature::QualityScore { + position, + quality_score, + }); } else { - let scores = quality_scores[read_position..end].to_vec(); - features.push(Feature::Scores(read_position, scores)); + let quality_scores = quality_scores[position..end].to_vec(); + + features.push(Feature::Scores { + position, + quality_scores, + }); } } } - Kind::HardClip => features.push(Feature::HardClip(read_position, op.len())), - Kind::Pad => features.push(Feature::Padding(read_position, op.len())), + Kind::HardClip => features.push(Feature::HardClip { + position, + len: op.len(), + }), + Kind::Pad => features.push(Feature::Padding { + position, + len: op.len(), + }), }; if op.kind().consumes_read() { - read_position = read_position + position = position .checked_add(op.len()) .expect("attempt to add with overflow"); } @@ -229,7 +269,11 @@ mod tests { let sequence = Sequence::from(b"A"); let quality_scores = QualityScores::from(vec![45]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); - let expected = Features::from(vec![Feature::ReadBase(Position::try_from(1)?, b'A', 45)]); + let expected = Features::from(vec![Feature::ReadBase { + position: Position::try_from(1)?, + base: b'A', + quality_score: 45, + }]); assert_eq!(actual, expected); let cigar = [Op::new(Kind::Match, 2)].into_iter().collect(); @@ -237,8 +281,14 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::Bases(Position::try_from(1)?, vec![b'A', b'C']), - Feature::Scores(Position::try_from(1)?, vec![45, 35]), + Feature::Bases { + position: Position::try_from(1)?, + bases: vec![b'A', b'C'], + }, + Feature::Scores { + position: Position::try_from(1)?, + quality_scores: vec![45, 35], + }, ]); assert_eq!(actual, expected); @@ -249,9 +299,19 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::InsertBase(Position::try_from(1)?, b'A'), - Feature::QualityScore(Position::try_from(1)?, 45), - Feature::ReadBase(Position::try_from(2)?, b'C', 35), + Feature::InsertBase { + position: Position::try_from(1)?, + base: b'A', + }, + Feature::QualityScore { + position: Position::try_from(1)?, + quality_score: 45, + }, + Feature::ReadBase { + position: Position::try_from(2)?, + base: b'C', + quality_score: 35, + }, ]); assert_eq!(actual, expected); @@ -262,9 +322,19 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35, 43]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::Insertion(Position::try_from(1)?, vec![b'A', b'C']), - Feature::Scores(Position::try_from(1)?, vec![45, 35]), - Feature::ReadBase(Position::try_from(3)?, b'G', 43), + Feature::Insertion { + position: Position::try_from(1)?, + bases: vec![b'A', b'C'], + }, + Feature::Scores { + position: Position::try_from(1)?, + quality_scores: vec![45, 35], + }, + Feature::ReadBase { + position: Position::try_from(3)?, + base: b'G', + quality_score: 43, + }, ]); assert_eq!(actual, expected); @@ -275,9 +345,18 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::Deletion(Position::try_from(1)?, 1), - Feature::Bases(Position::try_from(1)?, vec![b'A', b'C']), - Feature::Scores(Position::try_from(1)?, vec![45, 35]), + Feature::Deletion { + position: Position::try_from(1)?, + len: 1, + }, + Feature::Bases { + position: Position::try_from(1)?, + bases: vec![b'A', b'C'], + }, + Feature::Scores { + position: Position::try_from(1)?, + quality_scores: vec![45, 35], + }, ]); assert_eq!(actual, expected); @@ -288,8 +367,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::ReferenceSkip(Position::try_from(1)?, 1), - Feature::ReadBase(Position::try_from(1)?, b'A', 45), + Feature::ReferenceSkip { + position: Position::try_from(1)?, + len: 1, + }, + Feature::ReadBase { + position: Position::try_from(1)?, + base: b'A', + quality_score: 45, + }, ]); assert_eq!(actual, expected); @@ -300,9 +386,19 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::SoftClip(Position::try_from(1)?, vec![b'A']), - Feature::QualityScore(Position::try_from(1)?, 45), - Feature::ReadBase(Position::try_from(2)?, b'C', 35), + Feature::SoftClip { + position: Position::try_from(1)?, + bases: vec![b'A'], + }, + Feature::QualityScore { + position: Position::try_from(1)?, + quality_score: 45, + }, + Feature::ReadBase { + position: Position::try_from(2)?, + base: b'C', + quality_score: 35, + }, ]); assert_eq!(actual, expected); @@ -313,9 +409,19 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35, 43]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::SoftClip(Position::try_from(1)?, vec![b'A', b'C']), - Feature::Scores(Position::try_from(1)?, vec![45, 35]), - Feature::ReadBase(Position::try_from(3)?, b'G', 43), + Feature::SoftClip { + position: Position::try_from(1)?, + bases: vec![b'A', b'C'], + }, + Feature::Scores { + position: Position::try_from(1)?, + quality_scores: vec![45, 35], + }, + Feature::ReadBase { + position: Position::try_from(3)?, + base: b'G', + quality_score: 43, + }, ]); assert_eq!(actual, expected); @@ -326,8 +432,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::HardClip(Position::try_from(1)?, 1), - Feature::ReadBase(Position::try_from(1)?, b'A', 45), + Feature::HardClip { + position: Position::try_from(1)?, + len: 1, + }, + Feature::ReadBase { + position: Position::try_from(1)?, + base: b'A', + quality_score: 45, + }, ]); assert_eq!(actual, expected); @@ -338,8 +451,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::Padding(Position::try_from(1)?, 1), - Feature::ReadBase(Position::try_from(1)?, b'A', 45), + Feature::Padding { + position: Position::try_from(1)?, + len: 1, + }, + Feature::ReadBase { + position: Position::try_from(1)?, + base: b'A', + quality_score: 45, + }, ]); assert_eq!(actual, expected); @@ -355,17 +475,21 @@ mod tests { let sequence = Sequence::from(b"A"); let quality_scores = QualityScores::from(vec![45]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); - let expected = Features::from(vec![Feature::ReadBase(Position::try_from(1)?, b'A', 45)]); + let expected = Features::from(vec![Feature::ReadBase { + position: Position::try_from(1)?, + base: b'A', + quality_score: 45, + }]); assert_eq!(actual, expected); let cigar = [Op::new(Kind::Match, 2)].into_iter().collect(); let sequence = Sequence::from(b"AC"); let quality_scores = QualityScores::from(vec![45, 35]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); - let expected = Features::from(vec![Feature::Bases( - Position::try_from(1)?, - vec![b'A', b'C'], - )]); + let expected = Features::from(vec![Feature::Bases { + position: Position::try_from(1)?, + bases: vec![b'A', b'C'], + }]); assert_eq!(actual, expected); let cigar = [Op::new(Kind::Insertion, 1), Op::new(Kind::Match, 1)] @@ -375,8 +499,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::InsertBase(Position::try_from(1)?, b'A'), - Feature::ReadBase(Position::try_from(2)?, b'C', 35), + Feature::InsertBase { + position: Position::try_from(1)?, + base: b'A', + }, + Feature::ReadBase { + position: Position::try_from(2)?, + base: b'C', + quality_score: 35, + }, ]); assert_eq!(actual, expected); @@ -387,8 +518,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35, 43]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::Insertion(Position::try_from(1)?, vec![b'A', b'C']), - Feature::ReadBase(Position::try_from(3)?, b'G', 43), + Feature::Insertion { + position: Position::try_from(1)?, + bases: vec![b'A', b'C'], + }, + Feature::ReadBase { + position: Position::try_from(3)?, + base: b'G', + quality_score: 43, + }, ]); assert_eq!(actual, expected); @@ -399,8 +537,14 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::Deletion(Position::try_from(1)?, 1), - Feature::Bases(Position::try_from(1)?, vec![b'A', b'C']), + Feature::Deletion { + position: Position::try_from(1)?, + len: 1, + }, + Feature::Bases { + position: Position::try_from(1)?, + bases: vec![b'A', b'C'], + }, ]); assert_eq!(actual, expected); @@ -411,8 +555,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::ReferenceSkip(Position::try_from(1)?, 1), - Feature::ReadBase(Position::try_from(1)?, b'A', 45), + Feature::ReferenceSkip { + position: Position::try_from(1)?, + len: 1, + }, + Feature::ReadBase { + position: Position::try_from(1)?, + base: b'A', + quality_score: 45, + }, ]); assert_eq!(actual, expected); @@ -423,8 +574,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::SoftClip(Position::try_from(1)?, vec![b'A']), - Feature::ReadBase(Position::try_from(2)?, b'C', 35), + Feature::SoftClip { + position: Position::try_from(1)?, + bases: vec![b'A'], + }, + Feature::ReadBase { + position: Position::try_from(2)?, + base: b'C', + quality_score: 35, + }, ]); assert_eq!(actual, expected); @@ -435,8 +593,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45, 35, 43]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::SoftClip(Position::try_from(1)?, vec![b'A', b'C']), - Feature::ReadBase(Position::try_from(3)?, b'G', 43), + Feature::SoftClip { + position: Position::try_from(1)?, + bases: vec![b'A', b'C'], + }, + Feature::ReadBase { + position: Position::try_from(3)?, + base: b'G', + quality_score: 43, + }, ]); assert_eq!(actual, expected); @@ -447,8 +612,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::HardClip(Position::try_from(1)?, 1), - Feature::ReadBase(Position::try_from(1)?, b'A', 45), + Feature::HardClip { + position: Position::try_from(1)?, + len: 1, + }, + Feature::ReadBase { + position: Position::try_from(1)?, + base: b'A', + quality_score: 45, + }, ]); assert_eq!(actual, expected); @@ -459,8 +631,15 @@ mod tests { let quality_scores = QualityScores::from(vec![45]); let actual = cigar_to_features(flags, &cigar, &sequence, &quality_scores); let expected = Features::from(vec![ - Feature::Padding(Position::try_from(1)?, 1), - Feature::ReadBase(Position::try_from(1)?, b'A', 45), + Feature::Padding { + position: Position::try_from(1)?, + len: 1, + }, + Feature::ReadBase { + position: Position::try_from(1)?, + base: b'A', + quality_score: 45, + }, ]); assert_eq!(actual, expected); @@ -477,10 +656,10 @@ mod tests { [Op::new(Kind::Match, 4)].into_iter().collect() ); - let features = Features::from(vec![Feature::SoftClip( - Position::try_from(1)?, - vec![b'A', b'T'], - )]); + let features = Features::from(vec![Feature::SoftClip { + position: Position::try_from(1)?, + bases: vec![b'A', b'T'], + }]); assert_eq!( features.try_into_cigar(4)?, [Op::new(Kind::SoftClip, 2), Op::new(Kind::Match, 2)] @@ -488,7 +667,10 @@ mod tests { .collect() ); - let features = Features::from(vec![Feature::SoftClip(Position::try_from(4)?, vec![b'G'])]); + let features = Features::from(vec![Feature::SoftClip { + position: Position::try_from(4)?, + bases: vec![b'G'], + }]); assert_eq!( features.try_into_cigar(4)?, [Op::new(Kind::Match, 3), Op::new(Kind::SoftClip, 1)] @@ -496,7 +678,10 @@ mod tests { .collect() ); - let features = Features::from(vec![Feature::HardClip(Position::try_from(1)?, 2)]); + let features = Features::from(vec![Feature::HardClip { + position: Position::try_from(1)?, + len: 2, + }]); assert_eq!( features.try_into_cigar(4)?, [Op::new(Kind::HardClip, 2), Op::new(Kind::Match, 4)] @@ -505,8 +690,14 @@ mod tests { ); let features = Features::from(vec![ - Feature::SoftClip(Position::try_from(1)?, vec![b'A']), - Feature::Substitution(Position::try_from(3)?, substitution::Value::Code(0)), + Feature::SoftClip { + position: Position::try_from(1)?, + bases: vec![b'A'], + }, + Feature::Substitution { + position: Position::try_from(3)?, + value: substitution::Value::Code(0), + }, ]); assert_eq!( features.try_into_cigar(4)?, @@ -515,10 +706,10 @@ mod tests { .collect() ); - let features = Features::from(vec![Feature::Substitution( - Position::try_from(2)?, - substitution::Value::Code(0), - )]); + let features = Features::from(vec![Feature::Substitution { + position: Position::try_from(2)?, + value: substitution::Value::Code(0), + }]); assert_eq!( features.try_into_cigar(4)?, [Op::new(Kind::Match, 4)].into_iter().collect() diff --git a/noodles-cram/src/record/features/cigar.rs b/noodles-cram/src/record/features/cigar.rs index 3700baf2e..7598dded1 100644 --- a/noodles-cram/src/record/features/cigar.rs +++ b/noodles-cram/src/record/features/cigar.rs @@ -56,14 +56,14 @@ impl<'a> Iterator for Cigar<'a> { } let (kind, len) = match feature { - Feature::Substitution(..) => (Kind::Match, 1), - Feature::Insertion(_, bases) => (Kind::Insertion, bases.len()), - Feature::Deletion(_, len) => (Kind::Deletion, *len), - Feature::InsertBase(..) => (Kind::Insertion, 1), - Feature::ReferenceSkip(_, len) => (Kind::Skip, *len), - Feature::SoftClip(_, bases) => (Kind::SoftClip, bases.len()), - Feature::Padding(_, len) => (Kind::Pad, *len), - Feature::HardClip(_, len) => (Kind::HardClip, *len), + Feature::Substitution { .. } => (Kind::Match, 1), + Feature::Insertion { bases, .. } => (Kind::Insertion, bases.len()), + Feature::Deletion { len, .. } => (Kind::Deletion, *len), + Feature::InsertBase { .. } => (Kind::Insertion, 1), + Feature::ReferenceSkip { len, .. } => (Kind::Skip, *len), + Feature::SoftClip { bases, .. } => (Kind::SoftClip, bases.len()), + Feature::Padding { len, .. } => (Kind::Pad, *len), + Feature::HardClip { len, .. } => (Kind::HardClip, *len), _ => todo!(), }; @@ -94,24 +94,30 @@ mod tests { let features = Features::default(); t(&features, 4, &[Op::new(Kind::Match, 4)]); - let features = Features::from(vec![Feature::SoftClip( - Position::try_from(1)?, - vec![b'A', b'T'], - )]); + let features = Features::from(vec![Feature::SoftClip { + position: Position::try_from(1)?, + bases: vec![b'A', b'T'], + }]); t( &features, 4, &[Op::new(Kind::SoftClip, 2), Op::new(Kind::Match, 2)], ); - let features = Features::from(vec![Feature::SoftClip(Position::try_from(4)?, vec![b'G'])]); + let features = Features::from(vec![Feature::SoftClip { + position: Position::try_from(4)?, + bases: vec![b'G'], + }]); t( &features, 4, &[Op::new(Kind::Match, 3), Op::new(Kind::SoftClip, 1)], ); - let features = Features::from(vec![Feature::HardClip(Position::try_from(1)?, 2)]); + let features = Features::from(vec![Feature::HardClip { + position: Position::try_from(1)?, + len: 2, + }]); t( &features, 4, @@ -119,8 +125,14 @@ mod tests { ); let features = Features::from(vec![ - Feature::SoftClip(Position::try_from(1)?, vec![b'A']), - Feature::Substitution(Position::try_from(3)?, substitution::Value::Code(0)), + Feature::SoftClip { + position: Position::try_from(1)?, + bases: vec![b'A'], + }, + Feature::Substitution { + position: Position::try_from(3)?, + value: substitution::Value::Code(0), + }, ]); t( &features, @@ -133,10 +145,10 @@ mod tests { ], ); - let features = Features::from(vec![Feature::Substitution( - Position::try_from(2)?, - substitution::Value::Code(0), - )]); + let features = Features::from(vec![Feature::Substitution { + position: Position::try_from(2)?, + value: substitution::Value::Code(0), + }]); t( &features, 4, diff --git a/noodles-cram/src/record/features/with_positions.rs b/noodles-cram/src/record/features/with_positions.rs index 1185915a5..a10afd025 100644 --- a/noodles-cram/src/record/features/with_positions.rs +++ b/noodles-cram/src/record/features/with_positions.rs @@ -42,18 +42,18 @@ where let feature = self.iter.next()?; let (reference_position_delta, read_position_delta) = match feature { - Feature::Bases(_, bases) => (bases.len(), bases.len()), - Feature::Scores(..) => continue, - Feature::ReadBase(..) => (1, 1), - Feature::Substitution(..) => (1, 1), - Feature::Insertion(_, bases) => (0, bases.len()), - Feature::Deletion(_, len) => (*len, 0), - Feature::InsertBase(..) => (0, 1), - Feature::QualityScore(..) => continue, - Feature::ReferenceSkip(_, len) => (*len, 0), - Feature::SoftClip(_, bases) => (0, bases.len()), - Feature::Padding(..) => (0, 0), - Feature::HardClip(..) => (0, 0), + Feature::Bases { bases, .. } => (bases.len(), bases.len()), + Feature::Scores { .. } => continue, + Feature::ReadBase { .. } => (1, 1), + Feature::Substitution { .. } => (1, 1), + Feature::Insertion { bases, .. } => (0, bases.len()), + Feature::Deletion { len, .. } => (*len, 0), + Feature::InsertBase { .. } => (0, 1), + Feature::QualityScore { .. } => continue, + Feature::ReferenceSkip { len, .. } => (*len, 0), + Feature::SoftClip { bases, .. } => (0, bases.len()), + Feature::Padding { .. } => (0, 0), + Feature::HardClip { .. } => (0, 0), }; let feature_position = usize::from(feature.position()); @@ -95,8 +95,14 @@ mod tests { use crate::record::Features; let features = Features::from(vec![ - Feature::Bases(Position::MIN, vec![b'A', b'C']), - Feature::Scores(Position::MIN, vec![0, 0]), + Feature::Bases { + position: Position::MIN, + bases: vec![b'A', b'C'], + }, + Feature::Scores { + position: Position::MIN, + quality_scores: vec![0, 0], + }, ]); let mut iter = WithPositions::new(features.iter(), Position::MIN); diff --git a/noodles-cram/src/record/resolve.rs b/noodles-cram/src/record/resolve.rs index e63913134..6576889e7 100644 --- a/noodles-cram/src/record/resolve.rs +++ b/noodles-cram/src/record/resolve.rs @@ -39,36 +39,38 @@ pub(crate) fn resolve_bases( } match feature { - Feature::Bases(_, bases) => copy_from_bases(&mut buf[read_position..], bases), - Feature::Scores(..) => {} - Feature::ReadBase(_, base, _) => buf[read_position] = *base, - Feature::Substitution(_, substitution::Value::Code(code)) => { - if let Some(reference_sequence) = reference_sequence { - let base = reference_sequence[reference_position]; - let reference_base = SubstitutionBase::try_from(base).unwrap_or_default(); - let read_base = substitution_matrix.get(reference_base, *code); - buf[read_position] = u8::from(read_base); - } else { + Feature::Bases { bases, .. } => copy_from_bases(&mut buf[read_position..], bases), + Feature::Scores { .. } => {} + Feature::ReadBase { base, .. } => buf[read_position] = *base, + Feature::Substitution { value, .. } => match value { + substitution::Value::Code(code) => { + if let Some(reference_sequence) = reference_sequence { + let base = reference_sequence[reference_position]; + let reference_base = SubstitutionBase::try_from(base).unwrap_or_default(); + let read_base = substitution_matrix.get(reference_base, *code); + buf[read_position] = u8::from(read_base); + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "cannot resolve base substitution without reference sequence", + )); + } + } + substitution::Value::Bases(..) => { return Err(io::Error::new( io::ErrorKind::InvalidData, - "cannot resolve base substitution without reference sequence", + "cannot resolve base substitution with bases", )); } - } - Feature::Substitution(_, substitution::Value::Bases(..)) => { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "cannot resolve base substitution with bases", - )) - } - Feature::Insertion(_, bases) => copy_from_bases(&mut buf[read_position..], bases), - Feature::Deletion(..) => {} - Feature::InsertBase(_, base) => buf[read_position] = *base, - Feature::QualityScore(..) => {} - Feature::ReferenceSkip(..) => {} - Feature::SoftClip(_, bases) => copy_from_bases(&mut buf[read_position..], bases), - Feature::Padding(..) => {} - Feature::HardClip(..) => {} + }, + Feature::Insertion { bases, .. } => copy_from_bases(&mut buf[read_position..], bases), + Feature::Deletion { .. } => {} + Feature::InsertBase { base, .. } => buf[read_position] = *base, + Feature::QualityScore { .. } => {} + Feature::ReferenceSkip { .. } => {} + Feature::SoftClip { bases, .. } => copy_from_bases(&mut buf[read_position..], bases), + Feature::Padding { .. } => {} + Feature::HardClip { .. } => {} } let (next_reference_position, next_read_position) = it.positions(); @@ -114,15 +116,24 @@ pub fn resolve_quality_scores( let read_position = feature.position(); match feature { - Feature::Scores(_, scores) => { + Feature::Scores { + quality_scores: scores, + .. + } => { let end = read_position .checked_add(scores.len()) .expect("attempt to add with overflow"); quality_scores[read_position..end].copy_from_slice(scores); } - Feature::ReadBase(_, _, score) => quality_scores[read_position] = *score, - Feature::QualityScore(_, score) => quality_scores[read_position] = *score, + Feature::ReadBase { + quality_score: score, + .. + } => quality_scores[read_position] = *score, + Feature::QualityScore { + quality_score: score, + .. + } => quality_scores[read_position] = *score, _ => continue, } } @@ -159,62 +170,81 @@ mod tests { t(&Features::default(), &Sequence::from(b"ACGT"))?; t( - &Features::from(vec![Feature::Bases( - Position::try_from(1)?, - vec![b'T', b'G'], - )]), + &Features::from(vec![Feature::Bases { + position: Position::try_from(1)?, + bases: vec![b'T', b'G'], + }]), &Sequence::from(b"TGGT"), )?; t( - &Features::from(vec![Feature::ReadBase(Position::try_from(2)?, b'Y', 0)]), + &Features::from(vec![Feature::ReadBase { + position: Position::try_from(2)?, + base: b'Y', + quality_score: 0, + }]), &Sequence::from(b"AYGT"), )?; t( - &Features::from(vec![Feature::Substitution( - Position::try_from(2)?, - substitution::Value::Code(1), - )]), + &Features::from(vec![Feature::Substitution { + position: Position::try_from(2)?, + value: substitution::Value::Code(1), + }]), &Sequence::from(b"AGGT"), )?; t( - &Features::from(vec![Feature::Insertion( - Position::try_from(2)?, - vec![b'G', b'G'], - )]), + &Features::from(vec![Feature::Insertion { + position: Position::try_from(2)?, + bases: vec![b'G', b'G'], + }]), &Sequence::from(b"AGGC"), )?; t( - &Features::from(vec![Feature::Deletion(Position::try_from(2)?, 2)]), + &Features::from(vec![Feature::Deletion { + position: Position::try_from(2)?, + len: 2, + }]), &Sequence::from(b"ATAC"), )?; t( - &Features::from(vec![Feature::InsertBase(Position::try_from(2)?, b'G')]), + &Features::from(vec![Feature::InsertBase { + position: Position::try_from(2)?, + base: b'G', + }]), &Sequence::from(b"AGCG"), )?; t( - &Features::from(vec![Feature::ReferenceSkip(Position::try_from(2)?, 2)]), + &Features::from(vec![Feature::ReferenceSkip { + position: Position::try_from(2)?, + len: 2, + }]), &Sequence::from(b"ATAC"), )?; t( - &Features::from(vec![Feature::SoftClip( - Position::try_from(3)?, - vec![b'G', b'G'], - )]), + &Features::from(vec![Feature::SoftClip { + position: Position::try_from(3)?, + bases: vec![b'G', b'G'], + }]), &Sequence::from(b"ACGG"), )?; t( - &Features::from(vec![Feature::Padding(Position::try_from(1)?, 2)]), + &Features::from(vec![Feature::Padding { + position: Position::try_from(1)?, + len: 2, + }]), &Sequence::from(b"ACGT"), )?; t( - &Features::from(vec![Feature::HardClip(Position::try_from(1)?, 2)]), + &Features::from(vec![Feature::HardClip { + position: Position::try_from(1)?, + len: 2, + }]), &Sequence::from(b"ACGT"), )?; - let features = Features::from(vec![Feature::Substitution( - Position::try_from(2)?, - substitution::Value::Bases(SubstitutionBase::A, SubstitutionBase::C), - )]); + let features = Features::from(vec![Feature::Substitution { + position: Position::try_from(2)?, + value: substitution::Value::Bases(SubstitutionBase::A, SubstitutionBase::C), + }]); let mut actual = Sequence::default(); assert!(matches!( resolve_bases( @@ -234,10 +264,10 @@ mod tests { #[test] fn test_resolve_bases_without_a_reference_sequence() -> Result<(), Box> { let substitution_matrix = SubstitutionMatrix::default(); - let features = Features::from(vec![Feature::Bases( - Position::try_from(1)?, - vec![b'N', b'N', b'N', b'N'], - )]); + let features = Features::from(vec![Feature::Bases { + position: Position::try_from(1)?, + bases: vec![b'N', b'N', b'N', b'N'], + }]); let alignment_start = Position::try_from(1)?; let mut actual = Sequence::default(); @@ -259,9 +289,19 @@ mod tests { #[test] fn test_resolve_quality_scores() -> Result<(), Box> { let features = [ - Feature::ReadBase(Position::try_from(1)?, b'A', 5), - Feature::QualityScore(Position::try_from(3)?, 8), - Feature::Scores(Position::try_from(5)?, vec![13, 21]), + Feature::ReadBase { + position: Position::try_from(1)?, + base: b'A', + quality_score: 5, + }, + Feature::QualityScore { + position: Position::try_from(3)?, + quality_score: 8, + }, + Feature::Scores { + position: Position::try_from(5)?, + quality_scores: vec![13, 21], + }, ]; let mut quality_scores = QualityScores::default();