gwseq-io 0.2.1

Rust library for processing bigWig, bigBed, BAM, CRAM and HiC files
Documentation
//! One reader over both alignment formats.
//!
//! BAM and CRAM answer the same questions with the same types — the same
//! [`EntriesRequest`], the same [`BamRecord`], the same four read paths — because
//! the CRAM reader rebuilds BAM records rather than inventing a second entry
//! type. What is left over is the *dispatch*, and this is it.
//!
//! # Why an enum rather than a trait
//!
//! The instinct is a trait, and it does not survive contact with the walks.
//! [`bam::LocusWalk`] and [`cram::LocusWalk`] carry different plans — the BAM
//! one a set of decompressed-chunk cursors, the CRAM one nothing of the sort —
//! and the borrowing iterators over them carry lifetimes. An object-safe trait
//! would mean boxing the plan and giving up the plain `Iterator` a Rust caller
//! gets today, to abstract over a set that has two members and will not grow a
//! third. A `match` costs one predictable branch per *window*, not per record.
//!
//! This is what the Python layer holds, which is why `gwseq_io.BamReader` reads
//! CRAM too and reports which it has through its `type` — exactly as
//! `BbiReader` already serves bigWig and bigBed.

use crate::bam::{self, BamReader, BamRecord, EntriesRequest, SamHeader};
use crate::cram::{self, CramReader};
use crate::error::{Error, Result};
use crate::genomic::ChrMap;

/// A BAM or a CRAM, opened for reading.
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Alignments {
    Bam(BamReader),
    Cram(CramReader),
}

impl Alignments {
    /// `"bam"` or `"cram"`.
    pub fn kind(&self) -> &'static str {
        match self {
            Self::Bam(_) => "bam",
            Self::Cram(_) => "cram",
        }
    }

    pub fn header(&self) -> &SamHeader {
        match self {
            Self::Bam(reader) => reader.header(),
            Self::Cram(reader) => reader.header(),
        }
    }

    pub fn chr_sizes(&self) -> &ChrMap {
        match self {
            Self::Bam(reader) => reader.chr_sizes(),
            Self::Cram(reader) => reader.chr_sizes(),
        }
    }

    pub fn is_indexed(&self) -> bool {
        match self {
            Self::Bam(reader) => reader.is_indexed(),
            Self::Cram(reader) => reader.is_indexed(),
        }
    }

    pub fn index_error(&self) -> &str {
        match self {
            Self::Bam(reader) => reader.index_error(),
            Self::Cram(reader) => reader.index_error(),
        }
    }

    pub fn is_closed(&self) -> bool {
        match self {
            Self::Bam(reader) => reader.is_closed(),
            Self::Cram(reader) => reader.is_closed(),
        }
    }

    pub fn path(&self) -> &str {
        match self {
            Self::Bam(reader) => reader.path(),
            Self::Cram(reader) => reader.path(),
        }
    }

    pub fn parallel(&self) -> usize {
        match self {
            Self::Bam(reader) => reader.parallel(),
            Self::Cram(reader) => reader.parallel(),
        }
    }

    pub fn close(&mut self) {
        match self {
            Self::Bam(reader) => reader.close(),
            Self::Cram(reader) => reader.close(),
        }
    }

    /// The CRAM version, or `None` for a BAM.
    pub fn version(&self) -> Option<(u8, u8)> {
        match self {
            Self::Bam(_) => None,
            Self::Cram(reader) => Some(reader.version()),
        }
    }

    /// The reference FASTA in use. Always `None` for a BAM, which needs none.
    pub fn reference_path(&self) -> Option<&str> {
        match self {
            Self::Bam(_) => None,
            Self::Cram(reader) => reader.reference_path(),
        }
    }

    /// Why there is no reference, when there is none and one was wanted.
    pub fn reference_error(&self) -> &str {
        match self {
            Self::Bam(_) => "",
            Self::Cram(reader) => reader.reference_error(),
        }
    }

    pub fn read_entries(&self, req: &EntriesRequest) -> Result<Vec<Vec<BamRecord>>> {
        match self {
            Self::Bam(reader) => reader.read_entries(req),
            Self::Cram(reader) => reader.read_entries(req),
        }
    }

    pub fn read_all_entries(&self, req: &EntriesRequest) -> Result<Vec<BamRecord>> {
        match self {
            Self::Bam(reader) => reader.read_all_entries(req),
            Self::Cram(reader) => reader.read_all_entries(req),
        }
    }
}

/// A walk over either format's loci or windows.
#[derive(Debug)]
pub enum AlignmentWalk {
    Bam(bam::LocusWalk),
    Cram(cram::LocusWalk),
}

impl AlignmentWalk {
    /// A per-locus walk.
    pub fn plan(reader: &Alignments, req: &EntriesRequest) -> Result<Self> {
        Ok(match reader {
            Alignments::Bam(reader) => Self::Bam(bam::LocusWalk::plan(reader, req)?),
            Alignments::Cram(reader) => Self::Cram(cram::LocusWalk::plan(reader, req)?),
        })
    }

    /// A walk over windows tiling whole references.
    pub fn plan_windows(reader: &Alignments, req: &EntriesRequest, span: i64) -> Result<Self> {
        Ok(match reader {
            Alignments::Bam(reader) => Self::Bam(bam::LocusWalk::plan_windows(reader, req, span)?),
            Alignments::Cram(reader) => {
                Self::Cram(cram::LocusWalk::plan_windows(reader, req, span)?)
            }
        })
    }

    /// The same walk, back at its first locus, sharing the plan.
    pub fn restarted(&self) -> Self {
        match self {
            Self::Bam(walk) => Self::Bam(walk.restarted()),
            Self::Cram(walk) => Self::Cram(walk.restarted()),
        }
    }

    pub fn len(&self) -> usize {
        match self {
            Self::Bam(walk) => walk.len(),
            Self::Cram(walk) => walk.len(),
        }
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    pub fn order(&self) -> &[usize] {
        match self {
            Self::Bam(walk) => walk.order(),
            Self::Cram(walk) => walk.order(),
        }
    }

    /// The next window, from the reader this walk was planned against.
    ///
    /// Handing it a reader of the other format is a caller's mistake rather
    /// than a corrupt file, so it is [`Error::InvalidArgument`] — and it cannot
    /// happen through the Python layer, which holds the pair together.
    pub fn next_window(&mut self, reader: &Alignments) -> Option<Result<Vec<BamRecord>>> {
        match (self, reader) {
            (Self::Bam(walk), Alignments::Bam(reader)) => walk.next_window(reader),
            (Self::Cram(walk), Alignments::Cram(reader)) => walk.next_window(reader),
            _ => Some(Err(Error::invalid(
                "this walk was planned against a reader of the other format",
            ))),
        }
    }
}