use anyhow::{Result, bail};
use crate::consensus_tags::per_base;
use crate::sort::bam_fields;
use fgumi_raw_bam::RawRecordView;
pub fn reverse_per_base_tags_raw(record: &mut [u8]) -> Result<bool> {
if record.len() < bam_fields::MIN_BAM_RECORD_LEN {
bail!(
"BAM record too short ({} bytes, minimum {})",
record.len(),
bam_fields::MIN_BAM_RECORD_LEN
);
}
let flg = RawRecordView::new(record).flags();
if (flg & bam_fields::flags::REVERSE) == 0 {
return Ok(false);
}
let aux_off = bam_fields::aux_data_offset_from_record(record).unwrap_or(record.len());
if aux_off >= record.len() {
return Ok(true);
}
for tag_str in per_base::tags_to_reverse() {
let tag_bytes: [u8; 2] = [tag_str.as_bytes()[0], tag_str.as_bytes()[1]];
let tag_type = bam_fields::find_tag_type(&record[aux_off..], &tag_bytes);
match tag_type {
Some(b'B') => {
bam_fields::reverse_array_tag_in_place(record, aux_off, &tag_bytes);
}
Some(b'Z') => {
bam_fields::reverse_string_tag_in_place(record, aux_off, &tag_bytes);
}
_ => {} }
}
for tag_str in per_base::tags_to_reverse_complement() {
let tag_bytes: [u8; 2] = [tag_str.as_bytes()[0], tag_str.as_bytes()[1]];
bam_fields::reverse_complement_string_tag_in_place(record, aux_off, &tag_bytes);
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use fgumi_raw_bam::{
SamBuilder as RawSamBuilder, flags as raw_flags, raw_record_to_record_buf,
};
use noodles::sam::Header;
use noodles::sam::alignment::record::data::field::Tag;
use noodles::sam::alignment::record_buf::data::field::Value;
use rstest::rstest;
fn to_record_buf(raw: &fgumi_raw_bam::RawRecord) -> noodles::sam::alignment::RecordBuf {
raw_record_to_record_buf(raw, &Header::default())
.expect("raw_record_to_record_buf failed in test")
}
#[rstest]
#[case(Value::from(vec![1i8, 2, 3]), Value::from(vec![3i8, 2, 1]))]
#[case(Value::from(vec![1u8, 2, 3]), Value::from(vec![3u8, 2, 1]))]
#[case(Value::from(vec![1i16, 2, 3]), Value::from(vec![3i16, 2, 1]))]
#[case(Value::from(vec![1u16, 2, 3]), Value::from(vec![3u16, 2, 1]))]
#[case(Value::from(vec![1i32, 2, 3]), Value::from(vec![3i32, 2, 1]))]
#[case(Value::from(vec![1u32, 2, 3]), Value::from(vec![3u32, 2, 1]))]
#[case(Value::from(vec![1.0f32, 2.0, 3.0]), Value::from(vec![3.0f32, 2.0, 1.0]))]
fn test_reverse_buf_value_all_array_types(#[case] input: Value, #[case] expected: Value) {
let reversed = fgumi_sam::reverse_buf_value(&input);
assert_eq!(reversed, expected);
}
#[test]
fn test_reverse_buf_value_non_array() {
let value = Value::from(42i32);
let reversed = fgumi_sam::reverse_buf_value(&value);
assert_eq!(reversed, value); }
#[test]
fn test_reverse_per_base_tags_raw_string_tag_aq() {
use crate::sort::bam_fields;
let mut b = RawSamBuilder::new();
b.sequence(b"ACGT").qualities(&[30; 4]).flags(raw_flags::REVERSE);
b.add_string_tag(b"aq", b"IIHG");
let record_buf = to_record_buf(&b.build());
let header = Header::default();
let raw_rec = fgumi_raw_bam::encode_record_buf_to_raw(&record_buf, &header)
.expect("encoding record should succeed");
let mut raw: Vec<u8> = raw_rec.as_ref().to_vec();
let result =
reverse_per_base_tags_raw(&mut raw).expect("reverse_per_base_tags_raw should succeed");
assert!(result);
let aux = bam_fields::aux_data_slice(&raw);
let s = bam_fields::find_string_tag(aux, b"aq").expect("aq tag should exist");
assert_eq!(s, b"GHII");
}
#[rstest]
#[case(b"ac")]
#[case(b"bc")]
fn test_reverse_per_base_tags_raw_revcomp_string_tags(#[case] tag: &'static [u8; 2]) {
use crate::sort::bam_fields;
let mut b = RawSamBuilder::new();
b.sequence(b"ACGT").qualities(&[30; 4]).flags(raw_flags::REVERSE);
b.add_string_tag(tag, b"ACGA");
let record_buf = to_record_buf(&b.build());
let header = Header::default();
let raw_rec = fgumi_raw_bam::encode_record_buf_to_raw(&record_buf, &header)
.expect("encoding record should succeed");
let mut raw: Vec<u8> = raw_rec.as_ref().to_vec();
let result =
reverse_per_base_tags_raw(&mut raw).expect("reverse_per_base_tags_raw should succeed");
assert!(result);
let aux = bam_fields::aux_data_slice(&raw);
let s = bam_fields::find_string_tag(aux, tag).expect("tag should exist");
assert_eq!(s, b"TCGT");
}
#[test]
fn test_reverse_per_base_tags_raw_short_record() {
let mut short_record = vec![0u8; 16];
let result = reverse_per_base_tags_raw(&mut short_record);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too short"));
}
#[test]
fn test_reverse_per_base_tags_raw_positive_strand() {
let mut b = RawSamBuilder::new();
b.sequence(b"ACGT").qualities(&[30; 4]).flags(0);
let record_buf = to_record_buf(&b.build());
let header = Header::default();
let raw_rec = fgumi_raw_bam::encode_record_buf_to_raw(&record_buf, &header)
.expect("encoding record should succeed");
let mut raw: Vec<u8> = raw_rec.as_ref().to_vec();
let result = reverse_per_base_tags_raw(&mut raw);
assert!(result.is_ok());
assert!(!result.expect("result should be Ok")); }
#[test]
fn test_reverse_per_base_tags_raw_negative_strand() {
use crate::sort::bam_fields;
let mut b = RawSamBuilder::new();
b.sequence(b"ACGT").qualities(&[30; 4]).flags(raw_flags::REVERSE);
let mut record_buf = to_record_buf(&b.build());
let tag = Tag::from([b'c', b'd']);
record_buf.data_mut().insert(tag, Value::from(vec![1u16, 2, 3, 4]));
let header = Header::default();
let raw_rec = fgumi_raw_bam::encode_record_buf_to_raw(&record_buf, &header)
.expect("encoding record should succeed");
let mut raw: Vec<u8> = raw_rec.as_ref().to_vec();
let result = reverse_per_base_tags_raw(&mut raw);
assert!(result.is_ok());
assert!(result.expect("result should be Ok"));
let aux = bam_fields::aux_data_slice(&raw);
let arr = bam_fields::find_array_tag(aux, b"cd").expect("cd tag should exist");
let values = bam_fields::array_tag_to_vec_u16(&arr);
assert_eq!(values, vec![4, 3, 2, 1]);
}
}