use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use crate::commands::spec::{
BinaryInputs, IntervalColumns, IntervalCommandSpec, NearestCommandSpec, OverlapCommandSpec,
StrandConstraint, DEFAULT_CHROM_COL, DEFAULT_DISTANCE_COL, DEFAULT_END_COL,
DEFAULT_NEAREST_SUFFIX, DEFAULT_START_COL, DEFAULT_STRAND_COL,
};
use crate::commands::{CommandError, Result};
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
#[command(name = "regular", about = "Primary modern command set for polaranges.")]
pub struct RegularCli {
#[command(subcommand)]
pub command: RegularCommand,
}
#[derive(Subcommand, Clone, Debug, PartialEq, Eq)]
pub enum RegularCommand {
Overlap(OverlapArgs),
Nearest(NearestArgs),
}
#[derive(Args, Clone, Debug, PartialEq, Eq)]
pub struct OverlapArgs {
#[command(flatten)]
pub common: BinaryIntervalArgs,
#[command(flatten)]
pub execution: ExecutionArgs,
}
#[derive(Args, Clone, Debug, PartialEq, Eq)]
pub struct NearestArgs {
#[command(flatten)]
pub common: BinaryIntervalArgs,
#[command(flatten)]
pub execution: ExecutionArgs,
#[arg(long)]
pub no_sort_output: bool,
}
#[derive(Args, Clone, Debug, PartialEq, Eq)]
pub struct BinaryIntervalArgs {
#[arg(long = "a", value_name = "PATH")]
pub left_path: PathBuf,
#[arg(long = "b", value_name = "PATH")]
pub right_path: PathBuf,
#[command(flatten)]
pub columns: ColumnArgs,
#[command(flatten)]
pub strand: StrandArgs,
}
#[derive(Args, Clone, Debug, PartialEq, Eq)]
pub struct ColumnArgs {
#[arg(long, default_value = DEFAULT_CHROM_COL)]
pub chrom_col: String,
#[arg(long, default_value = DEFAULT_START_COL)]
pub start_col: String,
#[arg(long, default_value = DEFAULT_END_COL)]
pub end_col: String,
#[arg(long)]
pub strand_col: Option<String>,
}
#[derive(Args, Clone, Debug, PartialEq, Eq, Default)]
pub struct StrandArgs {
#[arg(long, conflicts_with = "opposite_strand")]
pub same_strand: bool,
#[arg(long, conflicts_with = "same_strand")]
pub opposite_strand: bool,
}
#[derive(Args, Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExecutionArgs {
#[arg(long)]
pub benchmark: bool,
#[arg(long, default_value_t = 1, value_name = "N")]
pub reps: usize,
#[arg(long)]
pub no_output: bool,
}
impl Default for ExecutionArgs {
fn default() -> Self {
Self {
benchmark: false,
reps: 1,
no_output: false,
}
}
}
pub fn translate_regular_cli(cli: RegularCli) -> Result<IntervalCommandSpec> {
translate_regular_command(cli.command)
}
pub fn translate_regular_command(command: RegularCommand) -> Result<IntervalCommandSpec> {
match command {
RegularCommand::Overlap(args) => Ok(IntervalCommandSpec::Overlap(OverlapCommandSpec {
inputs: translate_inputs(&args.common),
columns: translate_columns(&args.common.columns),
strand_constraint: translate_strand_constraint(&args.common.strand)?,
})),
RegularCommand::Nearest(args) => Ok(IntervalCommandSpec::Nearest(NearestCommandSpec {
inputs: translate_inputs(&args.common),
columns: translate_columns(&args.common.columns),
strand_constraint: translate_strand_constraint(&args.common.strand)?,
suffix: DEFAULT_NEAREST_SUFFIX.to_owned(),
distance_column: Some(DEFAULT_DISTANCE_COL.to_owned()),
preserve_input_order: !args.no_sort_output,
})),
}
}
fn translate_inputs(args: &BinaryIntervalArgs) -> BinaryInputs {
BinaryInputs {
left_path: args.left_path.clone(),
right_path: args.right_path.clone(),
}
}
fn translate_columns(args: &ColumnArgs) -> IntervalColumns {
IntervalColumns {
chrom_col: args.chrom_col.as_str().into(),
start_col: args.start_col.as_str().into(),
end_col: args.end_col.as_str().into(),
strand_col: Some(
args.strand_col
.as_deref()
.unwrap_or(DEFAULT_STRAND_COL)
.into(),
),
}
}
fn translate_strand_constraint(args: &StrandArgs) -> Result<StrandConstraint> {
match (args.same_strand, args.opposite_strand) {
(true, true) => Err(CommandError::InvalidStrandConstraint),
(true, false) => Ok(StrandConstraint::Same),
(false, true) => Ok(StrandConstraint::Opposite),
(false, false) => Ok(StrandConstraint::Ignore),
}
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::*;
#[test]
fn regular_cli_parses_overlap_command() {
let cli = RegularCli::try_parse_from([
"regular",
"overlap",
"--a",
"left.bed",
"--b",
"right.bed",
"--same-strand",
])
.unwrap();
match cli.command {
RegularCommand::Overlap(args) => {
assert_eq!(args.common.left_path, PathBuf::from("left.bed"));
assert_eq!(args.common.right_path, PathBuf::from("right.bed"));
assert!(args.common.strand.same_strand);
assert_eq!(args.common.columns.chrom_col, DEFAULT_CHROM_COL);
}
other => panic!("expected overlap command, got {other:?}"),
}
}
#[test]
fn regular_cli_parses_nearest_command() {
let cli = RegularCli::try_parse_from([
"regular",
"nearest",
"--a",
"left.tsv",
"--b",
"right.tsv",
"--chrom-col",
"Chrom",
"--start-col",
"Lower",
"--end-col",
"Upper",
])
.unwrap();
match cli.command {
RegularCommand::Nearest(args) => {
assert_eq!(args.common.left_path, PathBuf::from("left.tsv"));
assert_eq!(args.common.right_path, PathBuf::from("right.tsv"));
assert_eq!(args.common.columns.chrom_col, "Chrom");
assert_eq!(args.common.columns.start_col, "Lower");
assert_eq!(args.common.columns.end_col, "Upper");
}
other => panic!("expected nearest command, got {other:?}"),
}
}
#[test]
fn regular_cli_parses_nearest_no_sort_output() {
let cli = RegularCli::try_parse_from([
"regular",
"nearest",
"--a",
"left.tsv",
"--b",
"right.tsv",
"--no-sort-output",
])
.unwrap();
match cli.command {
RegularCommand::Nearest(args) => assert!(args.no_sort_output),
other => panic!("expected nearest command, got {other:?}"),
}
}
#[test]
fn nearest_no_sort_output_disables_input_order_preservation() {
let cli = RegularCli::try_parse_from([
"regular",
"nearest",
"--a",
"left.tsv",
"--b",
"right.tsv",
"--no-sort-output",
])
.unwrap();
let spec = translate_regular_cli(cli).unwrap();
match spec {
IntervalCommandSpec::Nearest(spec) => assert!(!spec.preserve_input_order),
other => panic!("expected nearest spec, got {other:?}"),
}
}
#[test]
fn regular_cli_parses_benchmark_execution_flags() {
let cli = RegularCli::try_parse_from([
"regular",
"nearest",
"--a",
"left.tsv",
"--b",
"right.tsv",
"--benchmark",
"--reps",
"3",
"--no-output",
])
.unwrap();
match cli.command {
RegularCommand::Nearest(args) => {
assert!(args.execution.benchmark);
assert_eq!(args.execution.reps, 3);
assert!(args.execution.no_output);
}
other => panic!("expected nearest command, got {other:?}"),
}
}
#[test]
fn parsed_regular_cli_translates_into_shared_specs() {
let cli = RegularCli::try_parse_from([
"regular",
"overlap",
"--a",
"left.bed",
"--b",
"right.bed",
"--same-strand",
])
.unwrap();
let spec = translate_regular_cli(cli).unwrap();
assert_eq!(
spec,
IntervalCommandSpec::Overlap(OverlapCommandSpec {
inputs: BinaryInputs {
left_path: PathBuf::from("left.bed"),
right_path: PathBuf::from("right.bed"),
},
columns: IntervalColumns {
chrom_col: DEFAULT_CHROM_COL.into(),
start_col: DEFAULT_START_COL.into(),
end_col: DEFAULT_END_COL.into(),
strand_col: Some(DEFAULT_STRAND_COL.into()),
},
strand_constraint: StrandConstraint::Same,
})
);
}
}