use std::path::PathBuf;
use anyhow::{Result, bail};
use clap::Args;
use libsail::format::Format;
use libsail::seq::fasta::IndexedFasta;
use libsail::seq::p7hmm::IndexedHmm;
use libsail::seq::stockholm::IndexedStockholm;
use crate::cli::FormatArg;
use crate::input::format_of_path;
#[derive(Args)]
pub struct IndexArgs {
pub input: PathBuf,
#[arg(long, value_enum)]
pub format: Option<FormatArg>,
#[arg(short, long)]
pub output: Option<PathBuf>,
}
impl IndexArgs {
pub fn run(self) -> Result<()> {
if self.input.as_os_str() == "-" {
bail!(
"index needs a path, not stdin: an index is byte offsets into a file that stays put"
);
}
let found = format_of_path(&self.input, self.format)?;
let output = self
.output
.clone()
.unwrap_or_else(|| libsail::index::path_for(&self.input));
let count = match found {
Format::Fasta => {
let mut collection = IndexedFasta::open(&self.input)?;
collection.name_index()?;
collection.index().write(&output, &self.input)?;
collection.index().len()
}
Format::Stockholm => {
let mut collection = IndexedStockholm::open(&self.input)?;
collection.name_index()?;
collection.index().write(&output, &self.input)?;
collection.index().len()
}
Format::Hmm => {
let mut collection = IndexedHmm::open(&self.input)?;
collection.name_index()?;
collection.index().write(&output, &self.input)?;
collection.index().len()
}
};
println!("{count} {found} records indexed to {}", output.display());
Ok(())
}
}
#[cfg(test)]
mod tests {
use libsail::index::Index;
use std::path::Path;
use libsail::collection::Indexable;
use super::*;
fn scratch(tag: &str, fixture: &str) -> PathBuf {
let from = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../fixtures")
.join(fixture);
let to = std::env::temp_dir().join(format!("sail-{tag}-{}-{fixture}", std::process::id()));
std::fs::copy(&from, &to).unwrap();
to
}
fn write(bytes: &[u8], tag: &str, extension: &str) -> PathBuf {
let path =
std::env::temp_dir().join(format!("sail-{tag}-{}.{extension}", std::process::id()));
std::fs::write(&path, bytes).unwrap();
path
}
fn index(input: &Path, format: Option<FormatArg>) -> Result<Index> {
IndexArgs {
input: input.to_path_buf(),
format,
output: None,
}
.run()?;
Ok(Index::read(&libsail::index::path_for(input)).unwrap())
}
fn names(index: &Index) -> Vec<Option<String>> {
(0..index.len())
.map(|n| {
index
.name(n)
.unwrap()
.map(|n| String::from_utf8(n.into_owned()).unwrap())
})
.collect()
}
fn clean(input: &Path) {
std::fs::remove_file(libsail::index::path_for(input)).ok();
std::fs::remove_file(input).ok();
}
#[test]
fn indexing_a_fasta_file_names_every_record_from_its_header() {
let input = scratch("fa-names", "proteins.fa");
let index = index(&input, None).unwrap();
assert_eq!(index.format(), Format::Fasta);
assert_eq!(
names(&index),
[
"DLG4_HUMAN",
"SEM5_CAEEL",
"NCF2_HUMAN",
"NHRF1_HUMAN",
"PPP5_HUMAN",
]
.map(|n| Some(n.to_string()))
);
clean(&input);
}
#[test]
fn indexing_an_alignment_names_every_record_from_its_gf_id() {
let input = scratch("sto-names", "families.sto");
let index = index(&input, None).unwrap();
assert_eq!(index.format(), Format::Stockholm);
assert_eq!(
names(&index),
[Some("SH3_1".to_string()), Some("PDZ".to_string())]
);
clean(&input);
}
#[test]
fn an_alignment_with_no_gf_id_is_indexed_with_no_name_at_all() {
let input = write(
b"# STOCKHOLM 1.0\nseq1 ACGT\nseq2 ACGA\n//\n",
"sto-unnamed",
"sto",
);
let index = index(&input, None).unwrap();
assert_eq!(names(&index), [None]);
clean(&input);
}
#[test]
fn indexing_a_profile_database_names_every_model() {
let input = scratch("hmm-names", "models.hmm");
let index = index(&input, None).unwrap();
assert_eq!(index.format(), Format::Hmm);
assert_eq!(
names(&index),
[Some("SH3_1".to_string()), Some("PDZ".to_string())]
);
clean(&input);
}
#[test]
fn the_written_index_reaches_the_records_a_fresh_scan_would() {
let input = scratch("fa-reach", "proteins.fa");
let loaded = IndexedFasta::with_index(&input, index(&input, None).unwrap()).unwrap();
let scanned = IndexedFasta::open(&input).unwrap();
assert_eq!(loaded.len(), scanned.len());
for n in 0..scanned.len() {
assert_eq!(loaded.cloned(n), scanned.cloned(n), "record {n}");
}
clean(&input);
}
#[test]
fn an_index_is_fresh_against_the_file_it_was_just_built_from() {
let input = scratch("fa-fresh", "proteins.fa");
index(&input, None).unwrap();
assert!(Index::read(&libsail::index::path_for(&input)).is_ok());
clean(&input);
}
#[test]
fn a_mismatched_format_assertion_is_refused_before_any_index_is_written() {
let input = scratch("fa-assert", "proteins.fa");
assert!(index(&input, Some(FormatArg::Stockholm)).is_err());
assert!(!libsail::index::path_for(&input).exists());
clean(&input);
}
#[test]
fn a_matching_format_assertion_indexes_as_usual() {
let input = scratch("fa-agree", "proteins.fa");
assert!(index(&input, Some(FormatArg::Fasta)).is_ok());
clean(&input);
}
#[test]
fn stdin_is_refused_rather_than_read_as_a_file_named_dash() {
let args = IndexArgs {
input: PathBuf::from("-"),
format: None,
output: None,
};
assert!(args.run().is_err());
}
#[test]
fn a_file_in_none_of_the_three_formats_is_refused() {
let input = write(b"@read1\nACGT\n+\n!!!!\n", "fastq", "fq");
assert!(index(&input, None).is_err());
clean(&input);
}
}