use std::collections::HashSet;
use std::path::PathBuf;
use std::str::FromStr;
use clap::{ArgGroup, Parser, Subcommand, ValueEnum};
use clap_num::number_range;
#[cfg(feature = "python-extension")]
use pyo3::pyclass;
use rust_decimal::RoundingStrategy;
use serde;
use serde::{Deserialize, Serialize};
use crate::plot::axis::Scale;
use crate::plot::data::Reducer;
use crate::plot::ShowLegend;
fn bin_size_parser(s: &str) -> Result<usize, String> {
let mut val = match s.parse::<usize>() {
Ok(v) => v,
Err(e) => panic!("{:?}", e),
};
if val == 0 {
val = usize::MAX
}
Ok(val)
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[command(subcommand_required = true, arg_required_else_help = true)]
pub struct Arguments {
#[clap(subcommand)]
pub cmd: SubCommand,
}
#[derive(Subcommand, Debug)]
pub enum SubCommand {
Depth(DepthOptions),
Filter(FilterOptions),
Index(IndexOptions),
Plot(PlotOptions),
Snail(SnailOptions),
Taxonomy(TaxonomyOptions),
Validate(ValidateOptions),
Create(CreateOptions),
}
#[derive(Parser, Debug)]
#[command(group(
ArgGroup::new("alignment")
.required(false)
.args(["bam", "cram"]),
))]
#[command(arg_required_else_help = true)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub struct DepthOptions {
#[clap(skip)]
pub list: Option<HashSet<Vec<u8>>>,
#[arg(long = "list", short = 'i', value_name = "TXT")]
pub list_file: Option<PathBuf>,
#[arg(long, short = 'b')]
pub bam: Option<PathBuf>,
#[arg(long, short = 'c')]
pub cram: Option<PathBuf>,
#[arg(long, short = 'a')]
pub fasta: Option<PathBuf>,
#[arg(long = "bin-size", short = 's', default_value_t = 0, value_parser = bin_size_parser)]
pub bin_size: usize,
#[arg(long = "bed", short = 'O', value_name = "BED")]
pub bed: Option<PathBuf>,
}
#[derive(Parser, Debug)]
#[command(group(
ArgGroup::new("alignment")
.required(false)
.args(["bam", "cram"]),
))]
#[command(arg_required_else_help = true)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub struct FilterOptions {
#[clap(skip)]
pub list: Option<HashSet<Vec<u8>>>,
#[arg(long = "list", short = 'i', value_name = "TXT")]
pub list_file: Option<PathBuf>,
#[arg(long, short = 'b')]
pub bam: Option<PathBuf>,
#[arg(long, short = 'c', requires = "fasta")]
pub cram: Option<PathBuf>,
#[arg(long, short = 'a')]
pub fasta: Option<PathBuf>,
#[arg(long = "fastq", short = 'f', value_name = "FASTQ")]
pub fastq1: Option<PathBuf>,
#[arg(
long = "fastq2",
short = 'r',
value_name = "FASTQ",
requires = "fastq1"
)]
pub fastq2: Option<PathBuf>,
#[arg(long, short = 'S', value_name = "SUFFIX", default_value_t = String::from("filtered"))]
pub suffix: String,
#[arg(
long = "fasta-out",
short = 'A',
requires = "fasta",
default_value_t = false
)]
pub fasta_out: bool,
#[arg(
long = "fastq-out",
short = 'F',
requires = "fastq1",
default_value_t = false
)]
pub fastq_out: bool,
#[arg(long = "read-list", short = 'O', value_name = "TXT")]
pub read_list: Option<PathBuf>,
}
#[derive(Parser, Debug)]
#[command(arg_required_else_help = true)]
pub struct IndexOptions {
#[arg(long, short = 'b')]
pub blobdir: Option<PathBuf>,
#[arg(long = "window-size", short = 'w', num_args(1..), default_values_t = [1.0], value_parser = window_size_parser, action = clap::ArgAction::Append)]
pub window_size: Vec<f64>,
#[arg(long = "taxon-id", short = 't')]
pub taxon_id: Option<String>,
#[arg(long = "accession", short = 'a')]
pub datasets_accession: Option<String>,
#[arg(long, short = 'u', num_args(1..), action = clap::ArgAction::Append)]
pub busco: Option<Vec<PathBuf>>,
#[arg(long, short = 'g')]
pub schema: bool,
#[arg(long, short = 'O')]
pub out: Option<PathBuf>,
}
#[derive(Parser, Debug, Default)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub struct CreateOptions {
#[arg(short = 'a', long = "fasta", value_name = "FASTA")]
pub fasta: Option<PathBuf>,
#[arg(short = 'b', long = "busco", value_name = "BUSCO")]
pub busco: Option<PathBuf>,
#[arg(short = 'd', long = "out", value_name = "BLOB_DIR")]
pub out: Option<PathBuf>,
}
fn window_size_parser(s: &str) -> Result<f64, String> {
let val = match s.parse::<f64>() {
Ok(v) => v,
Err(e) => panic!("{:?}", e),
};
Ok(val)
}
#[derive(ValueEnum, Clone, Debug, Default)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub enum View {
#[default]
Blob,
Cumulative,
Legend,
Snail,
}
impl FromStr for View {
type Err = ();
fn from_str(input: &str) -> Result<View, Self::Err> {
match input {
"blob" => Ok(View::Blob),
"cumulative" => Ok(View::Cumulative),
"legend" => Ok(View::Legend),
"snail" => Ok(View::Snail),
_ => Ok(View::Blob),
}
}
}
#[derive(ValueEnum, Clone, Debug, Default)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub enum Shape {
#[default]
Circle,
Grid,
}
impl FromStr for Shape {
type Err = ();
fn from_str(input: &str) -> Result<Shape, Self::Err> {
match input {
"circle" => Ok(Shape::Circle),
"grid" => Ok(Shape::Grid),
_ => Ok(Shape::Circle),
}
}
}
#[derive(ValueEnum, Clone, Debug)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub enum Origin {
O,
X,
Y,
}
impl FromStr for Origin {
type Err = ();
fn from_str(input: &str) -> Result<Origin, Self::Err> {
match input {
"o" => Ok(Origin::O),
"x" => Ok(Origin::X),
"y" => Ok(Origin::Y),
_ => Ok(Origin::O),
}
}
}
#[derive(ValueEnum, Clone, Debug)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub enum Palette {
Default,
Inverse,
Viridis,
}
impl FromStr for Palette {
type Err = ();
fn from_str(input: &str) -> Result<Palette, Self::Err> {
match input {
"default" => Ok(Palette::Default),
"inverse" => Ok(Palette::Inverse),
"viridis" => Ok(Palette::Viridis),
_ => Err(()),
}
}
}
#[derive(ValueEnum, Clone, Debug)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub enum ScoreType {
#[value(name = "base")]
Base, #[value(name = "g")]
G, #[value(name = "gs")]
Gs, #[value(name = "ag")]
GAbsolute, #[value(name = "ags")]
GsAbsolute, }
impl FromStr for ScoreType {
type Err = ();
fn from_str(input: &str) -> Result<ScoreType, Self::Err> {
match input {
"base" => Ok(ScoreType::Base),
"g" => Ok(ScoreType::G),
"gs" => Ok(ScoreType::Gs),
"ag" => Ok(ScoreType::GAbsolute),
"ags" => Ok(ScoreType::GsAbsolute),
_ => Err(()),
}
}
}
fn less_than_5(s: &str) -> Result<f64, String> {
Ok(number_range(&format!("{}", s.parse::<f64>().unwrap() * 10.0), 2, 50)? as f64 / 10.0)
}
const BADGE_LINEAR_SCALE_ERROR: &str =
"--badge requires --scale-function linear (badge mode only supports linear scaling)";
fn validate_badge_scale(badge: bool, scale_function: &Scale) -> Result<(), anyhow::Error> {
if badge && scale_function != &Scale::LINEAR {
return Err(anyhow::anyhow!(BADGE_LINEAR_SCALE_ERROR));
}
Ok(())
}
#[derive(Clone, Parser, Debug, Default)]
#[command(arg_required_else_help = true)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub struct PlotOptions {
#[arg(long, short = 'd')]
pub blobdir: PathBuf,
#[arg(long, short = 'v')]
#[clap(value_enum)]
pub view: View,
#[arg(long)]
#[clap(value_enum)]
pub shape: Option<Shape>,
#[arg(long = "window-size", short = 'w')]
pub window_size: Option<String>,
#[arg(long, short = 'o', value_name = "FILE", action = clap::ArgAction::Append)]
pub output: Vec<String>,
#[arg(long = "reference", value_name = "REFERENCE")]
pub reference: Option<PathBuf>,
#[arg(
long,
short = 'f',
value_name = "FILTER",
help = "Filter expression(s), e.g. length:min=1000 or gc:max=0.6"
)]
pub filter: Vec<String>,
#[arg(long, short = 's', default_value_t = 1000)]
pub segments: usize,
#[arg(long = "max-span")]
pub max_span: Option<usize>,
#[arg(long = "max-scaffold")]
pub max_scaffold: Option<usize>,
#[arg(long = "x-field", short = 'x')]
pub x_field: Option<String>,
#[arg(long = "y-field", short = 'y')]
pub y_field: Option<String>,
#[arg(long = "z-field", short = 'z')]
pub z_field: Option<String>,
#[arg(long = "category", short = 'c')]
pub cat_field: Option<String>,
#[arg(long = "synonyms")]
pub synonym_field: Option<String>,
#[arg(long, default_value_t = 30)]
pub resolution: usize,
#[arg(long = "hist-height")]
pub hist_height: Option<usize>,
#[arg(long, value_enum, default_value_t = Reducer::Sum)]
pub reducer_function: Reducer,
#[arg(long, value_enum)]
pub scale_function: Option<Scale>,
#[arg(long, default_value_t = 1.0, value_parser=less_than_5)]
pub scale_factor: f64,
#[arg(long = "x-limit")]
pub x_limit: Option<String>,
#[arg(long = "y-limit")]
pub y_limit: Option<String>,
#[arg(long = "cat-count", default_value_t = 10)]
pub cat_count: usize,
#[arg(long = "legend", value_enum, default_value_t = ShowLegend::Default)]
pub show_legend: ShowLegend,
#[arg(long = "cat-order")]
pub cat_order: Option<String>,
#[arg(long, value_enum)]
pub origin: Option<Origin>,
#[arg(long, value_enum)]
pub palette: Option<Palette>,
#[arg(long)]
pub color: Option<Vec<String>>,
#[arg(long = "significant-digits", default_value_t = 3)]
pub significant_digits: u32,
#[arg(long = "decimal-precision", default_value_t = 2)]
pub decimal_precision: u32,
#[arg(long = "rounding", value_enum)]
pub rounding: Option<RoundingStrategyWrapper>,
#[arg(long = "show-numbers", default_value_t = false)]
pub show_numbers: bool,
#[arg(long = "busco-numbers", default_value_t = false)]
pub busco_numbers: bool,
#[arg(long = "badge", default_value_t = false)]
pub badge: bool,
#[arg(long = "show-score", default_value_t = false)]
pub show_score: bool,
#[arg(long, value_name = "TYPE")]
pub score_type: Option<ScoreType>,
#[clap(skip)]
pub original_fasta: Option<String>,
#[clap(skip)]
pub original_busco: Option<String>,
#[clap(skip)]
pub original_blobdir: Option<String>,
#[clap(skip)]
pub original_reference: Option<String>,
#[clap(skip)]
pub score_only: bool,
#[clap(skip)]
pub score_json: bool,
}
impl PlotOptions {
pub fn resolved_scale_function(&self) -> Scale {
self.scale_function
.clone()
.unwrap_or_else(|| match self.view {
View::Snail => Scale::LINEAR,
_ => Scale::SQRT,
})
}
pub fn validate_score_type(&self) -> Result<(), anyhow::Error> {
validate_badge_scale(self.badge, &self.resolved_scale_function())?;
match &self.score_type {
Some(ScoreType::Base) | None => Ok(()),
Some(ScoreType::G) | Some(ScoreType::GAbsolute) => {
if self.max_span.is_some() || self.reference.is_some() {
Ok(())
} else {
Err(anyhow::anyhow!(
"--score-type g or ag requires either --max-span or --reference"
))
}
}
Some(ScoreType::Gs) | Some(ScoreType::GsAbsolute) => {
let has_max_span = self.max_span.is_some();
let has_max_scaffold = self.max_scaffold.is_some();
let has_ref = self.reference.is_some();
if has_ref || (has_max_span && has_max_scaffold) {
Ok(())
} else {
Err(anyhow::anyhow!(
"--score-type gs or ags requires both --max-span and --max-scaffold, or a --reference"
))
}
}
}
}
}
#[derive(ValueEnum, Clone, Debug, Default)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub enum RoundingStrategyWrapper {
#[default]
#[clap(name = "round")]
MidpointAwayFromZero,
#[clap(name = "down")]
ToZero,
#[clap(name = "up")]
AwayFromZero,
}
impl From<RoundingStrategyWrapper> for RoundingStrategy {
fn from(wrapper: RoundingStrategyWrapper) -> Self {
match wrapper {
RoundingStrategyWrapper::MidpointAwayFromZero => RoundingStrategy::MidpointAwayFromZero,
RoundingStrategyWrapper::ToZero => RoundingStrategy::ToZero,
RoundingStrategyWrapper::AwayFromZero => RoundingStrategy::AwayFromZero,
}
}
}
impl From<RoundingStrategy> for RoundingStrategyWrapper {
fn from(strategy: RoundingStrategy) -> Self {
match strategy {
RoundingStrategy::MidpointAwayFromZero => RoundingStrategyWrapper::MidpointAwayFromZero,
RoundingStrategy::ToZero => RoundingStrategyWrapper::ToZero,
RoundingStrategy::AwayFromZero => RoundingStrategyWrapper::AwayFromZero,
_ => RoundingStrategyWrapper::MidpointAwayFromZero,
}
}
}
impl RoundingStrategyWrapper {
pub fn from_str(input: &str) -> Result<RoundingStrategy, ()> {
match input {
"round" => Ok(RoundingStrategy::MidpointAwayFromZero),
"down" => Ok(RoundingStrategy::ToZero),
"up" => Ok(RoundingStrategy::AwayFromZero),
_ => Err(()),
}
}
pub fn default() -> RoundingStrategy {
RoundingStrategy::MidpointAwayFromZero
}
}
#[derive(Parser, Debug, Default)]
#[command(arg_required_else_help = true)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub struct SnailOptions {
#[arg(long = "fasta", value_name = "FASTA")]
pub fasta: Option<PathBuf>,
#[arg(long = "busco", value_name = "BUSCO")]
pub busco: Option<PathBuf>,
#[arg(long, short = 'd')]
pub blobdir: Option<PathBuf>,
#[arg(long = "reference", value_name = "REFERENCE")]
pub reference: Option<PathBuf>,
#[arg(long, short = 'o', value_name = "FILE", action = clap::ArgAction::Append)]
pub output: Vec<String>,
#[arg(long = "assembly-name", short = 'n')]
pub assembly_name: Option<String>,
#[arg(long = "reference-name", short = 'r')]
pub reference_name: Option<String>,
#[arg(long)]
pub score_only: bool,
#[arg(long)]
pub score_json: bool,
#[arg(
long,
short = 'f',
value_name = "FILTER",
help = "Filter expression(s), e.g. length:min=1000 or gc:max=0.6"
)]
pub filter: Vec<String>,
#[arg(long, short = 's', default_value_t = 1000)]
pub segments: usize,
#[arg(long = "max-span")]
pub max_span: Option<usize>,
#[arg(long = "max-scaffold")]
pub max_scaffold: Option<usize>,
#[arg(long, value_enum, default_value_t = Scale::LINEAR)]
pub scale_function: Scale,
#[arg(long = "significant-digits", default_value_t = 3)]
pub significant_digits: u32,
#[arg(long = "decimal-precision", default_value_t = 2)]
pub decimal_precision: u32,
#[arg(long = "rounding", value_enum)]
pub rounding: Option<RoundingStrategyWrapper>,
#[arg(long = "show-numbers", default_value_t = false)]
pub show_numbers: bool,
#[arg(long = "busco-numbers", default_value_t = false)]
pub busco_numbers: bool,
#[arg(long = "badge", default_value_t = false)]
pub badge: bool,
#[arg(long = "show-score", default_value_t = false)]
pub show_score: bool,
#[arg(long, value_name = "TYPE")]
pub score_type: Option<ScoreType>,
}
impl SnailOptions {
pub fn validate_score_type(&self) -> Result<(), anyhow::Error> {
validate_badge_scale(self.badge, &self.scale_function)?;
match &self.score_type {
Some(ScoreType::Base) | None => Ok(()),
Some(ScoreType::G) | Some(ScoreType::GAbsolute) => {
if self.max_span.is_some() || self.reference.is_some() {
Ok(())
} else {
Err(anyhow::anyhow!(
"--score-type g or ag requires either --max-span or --reference"
))
}
}
Some(ScoreType::Gs) | Some(ScoreType::GsAbsolute) => {
let has_max_span = self.max_span.is_some();
let has_max_scaffold = self.max_scaffold.is_some();
let has_ref = self.reference.is_some();
if has_ref || (has_max_span && has_max_scaffold) {
Ok(())
} else {
Err(anyhow::anyhow!(
"--score-type gs or ags requires both --max-span and --max-scaffold, or a --reference"
))
}
}
}
}
}
#[derive(ValueEnum, Parser, Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum TaxonomyFormat {
NCBI,
GBIF,
ENA,
OTT,
GenomeHubs,
JSONL,
}
impl std::fmt::Display for TaxonomyFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
TaxonomyFormat::NCBI => "ncbi",
TaxonomyFormat::GBIF => "gbif",
TaxonomyFormat::ENA => "ena",
TaxonomyFormat::OTT => "ott",
TaxonomyFormat::GenomeHubs => "genomehubs",
TaxonomyFormat::JSONL => "jsonl",
};
write!(f, "{}", s)
}
}
#[derive(Default, Parser, Serialize, Deserialize, Clone, Debug)]
#[command(arg_required_else_help = true)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub struct TaxonomyOptions {
#[arg(long = "taxdump", short = 't')]
pub path: Option<PathBuf>,
#[arg(long = "taxonomy-format", short = 'f')]
pub taxonomy_format: Option<TaxonomyFormat>,
#[arg(long = "root-id", short = 'r')]
pub root_taxon_id: Option<Vec<String>>,
#[arg(long = "leaf-id", short = 'l')]
pub leaf_taxon_id: Option<Vec<String>>,
#[arg(long = "base-id", short = 'b')]
pub base_taxon_id: Option<String>,
#[arg(long = "taxdump-out", short = 'O')]
pub out: Option<PathBuf>,
#[arg(long = "output-format", short = 'F')]
pub output_format: Option<Vec<TaxonomyFormat>>,
#[arg(long = "config", short = 'c')]
pub config_file: Option<PathBuf>,
#[clap(skip)]
#[serde(default = "default_name_classes")]
pub name_classes: Vec<String>,
#[arg(long = "xref-label", short = 'x')]
pub xref_label: Option<String>,
#[clap(skip)]
pub taxonomies: Option<Vec<TaxonomyOptions>>,
#[clap(skip)]
#[serde(default = "default_create_taxa")]
pub create_taxa: bool,
#[arg(long = "genomehubs_files", short = 'g')]
pub genomehubs_files: Option<Vec<PathBuf>>,
#[arg(long = "api")]
#[serde(default)]
pub api: bool,
#[arg(long, short = 'p', default_value_t = 3000)]
#[serde(default = "default_port")]
pub port: u16,
#[clap(skip)]
#[serde(default)]
pub experimental_fixes: crate::parse::feature_gates::ExperimentalFixes,
}
fn default_name_classes() -> Vec<String> {
vec!["scientific name".to_string()]
}
fn default_create_taxa() -> bool {
false
}
fn default_port() -> u16 {
3000
}
#[derive(Default, Parser, Serialize, Deserialize, Clone, Debug)]
#[command(arg_required_else_help = true)]
#[cfg_attr(feature = "python-extension", pyclass)]
pub struct ValidateOptions {
#[arg(long = "taxdump", short = 't')]
pub taxdump: Option<PathBuf>,
#[arg(long = "taxonomy-format", short = 'f')]
pub taxonomy_format: Option<TaxonomyFormat>,
#[clap(long = "name-classes", short = 'n', default_value = "scientific name")]
#[serde(default = "default_name_classes")]
pub name_classes: Vec<String>,
#[arg(long = "genomehubs_files", short = 'g')]
pub genomehubs_files: Option<Vec<PathBuf>>,
#[arg(long = "schema", short = 'S')]
pub schema: Option<PathBuf>,
#[arg(long = "dry-run", short = 'd')]
pub dry_run: bool,
#[arg(long = "skip-tsv", short = 'k')]
pub skip_tsv: bool,
#[clap(skip)]
#[serde(default)]
pub experimental_fixes: crate::parse::feature_gates::ExperimentalFixes,
}
pub fn parse() -> Arguments {
Arguments::parse()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plot_badge_rejects_non_linear_scale() {
let options = PlotOptions {
badge: true,
scale_function: Some(Scale::SQRT),
..Default::default()
};
let err = options.validate_score_type().unwrap_err();
assert_eq!(err.to_string(), BADGE_LINEAR_SCALE_ERROR);
}
#[test]
fn plot_badge_accepts_snail_default_linear_scale() {
let options = PlotOptions {
badge: true,
view: View::Snail,
scale_function: None,
..Default::default()
};
assert!(options.validate_score_type().is_ok());
}
#[test]
fn snail_badge_rejects_non_linear_scale() {
let options = SnailOptions {
badge: true,
scale_function: Scale::SQRT,
..Default::default()
};
let err = options.validate_score_type().unwrap_err();
assert_eq!(err.to_string(), BADGE_LINEAR_SCALE_ERROR);
}
}