Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ clap = { version = "4.5.60", features = ["derive", "string"] }
darkbio-clock = { version = "0.4", features = ["crossbeam"] }
darkbio-crypto = { version = "0.20.0", features = ["cbor", "xdsa", "rand"] }
darkbio-trust = { version = "0.7.0", features = ["release", "staging", "develop"] }
darkbio-wire = "0.16.0"
darkbio-wire = "0.17.0"
hex = "0.4.3"
console = "0.15.11"
base64 = "0.23.1"
Expand Down
108 changes: 74 additions & 34 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

use crate::data::samples::{self, Sex};
use clap::{Args, Parser, Subcommand, ValueEnum};
use darkbio_connect::trust::Environment;
use darkbio_connect::{schema::SlotKind, trust::Environment};
use std::path::PathBuf;

/// Lists an enum's variants, and fails to compile when the list misses one.
Expand Down Expand Up @@ -60,8 +60,8 @@ impl Cli {
/// Clap checks conflicts within one parser level, while a global flag may
/// come from any level. This rejects `--dry-run` with `--unlock`, `--quiet`
/// with `--verbose`, `--version` with a command, `--sex` on
/// `ark data samples` without a population or `--all`, and `--sample` on a
/// slot other than snp-indel-calls.
/// `ark data samples` without a population or `--all`, and `--sample` with
/// a slot.
pub fn validate(&self) -> Result<(), crate::error::Error> {
// --sex narrows a list of people, which takes a population or --all
if matches!(
Expand All @@ -78,24 +78,17 @@ impl Cli {
);
}

// --sample fills only snp-indel-calls, refused before anything connects
// A sample fills every personal slot, so it names none
if let Some(Command::Data(Data::Fetch {
slot: Some(slot),
sample: Some(_),
slot: Some(_),
sample: Some(sample),
..
})) = &self.command
&& *slot != darkbio_connect::schema::SlotKind::SnpIndelCalls as i32
{
let name = slot_name(*slot);
let mut error = crate::error::Error::new(
2,
"usage",
format!("--sample fills snp-indel-calls, not {name}"),
return Err(
crate::error::Error::new(2, "usage", "--sample takes no slot")
.hint(format!("run `ark data fetch --sample {}`", sample.name())),
);
if slot_names().any(|known| known == name) {
error = error.hint(format!("run `ark data fetch {name}`"));
}
return Err(error);
}

let dry = matches!(
Expand Down Expand Up @@ -262,12 +255,12 @@ pub(crate) enum Data {
/// Download public data onto the Ark
Fetch {
/// Slot name or id from `ark data list`
#[arg(value_parser = parse_slot, required_unless_present = "all", conflicts_with = "all")]
#[arg(value_parser = parse_slot, required_unless_present_any = ["all", "sample"], conflicts_with = "all")]
slot: Option<i32>,
/// Fetch every reference on offer, in order
#[arg(long)]
all: bool,
/// Fill the slot with a public genome, by name or sample ID
/// Fill personal slots with a public genome, by name or ID
#[arg(long, value_name = "NAME", value_parser = samples::Parser, hide_possible_values = true, conflicts_with = "all")]
sample: Option<samples::Sample>,
/// Show the plan without changing the Ark
Expand Down Expand Up @@ -368,15 +361,15 @@ pub(crate) fn parse_slot(value: &str) -> Result<i32, String> {
return Ok(id);
}
let name = format!("SLOT_KIND_{}", value.replace('-', "_").to_ascii_uppercase());
darkbio_connect::schema::SlotKind::from_str_name(&name)
SlotKind::from_str_name(&name)
.filter(|kind| *kind as i32 > 0 && slot_name(*kind as i32) == value)
.map(i32::from)
.ok_or_else(|| format!("unknown slot {value:?}; use a name or id from `ark data list`"))
}

/// Formats known protocol slot names, retaining unknown IDs as decimal selectors.
pub(crate) fn slot_name(id: i32) -> String {
darkbio_connect::schema::SlotKind::try_from(id)
SlotKind::try_from(id)
.map(|kind| {
kind.as_str_name()
.trim_start_matches("SLOT_KIND_")
Expand All @@ -386,19 +379,21 @@ pub(crate) fn slot_name(id: i32) -> String {
.unwrap_or_else(|_| id.to_string())
}

/// Lists the name of every slot kind, spelled as [`parse_slot`] accepts it.
/// Lists the name of every slot kind this build knows, in id order, spelled as
/// [`parse_slot`] accepts it.
///
/// The wire's slot kinds are non-exhaustive, so no match can prove this list
/// complete, and a new kind is added here by hand.
pub(crate) fn slot_names() -> impl Iterator<Item = String> {
use darkbio_connect::schema::SlotKind;
variants!(
SlotKind,
SlotKind::Unspecified,
[
SlotKind::ReferenceGenome,
SlotKind::GeneAnnotations,
SlotKind::SnpIndelCalls,
SlotKind::SmallVariants,
SlotKind::VariantCatalog,
)
SlotKind::StructuralVariants,
SlotKind::CopyNumberVariants,
]
.into_iter()
.filter(|kind| *kind as i32 > 0)
.map(|kind| slot_name(kind as i32))
}

Expand Down Expand Up @@ -427,6 +422,43 @@ fn parse_timeout(value: &str) -> Result<u64, String> {
mod tests {
use super::*;

/// Checks that `--sample` parses without a slot, and that naming any slot
/// with it fails with a usage error hinting at the slotless form.
#[test]
fn test_sample_fetch_takes_no_slot() {
for name in ["HG00096", "notable-owen"] {
let cli = Cli::try_parse_from(["ark", "data", "fetch", "--sample", name]).unwrap();
cli.validate().unwrap();
assert!(matches!(
cli.command,
Some(Command::Data(Data::Fetch {
slot: None,
sample: Some(_),
..
}))
));

for slot in [
"small-variants",
"structural-variants",
"copy-number-variants",
"1",
"42",
] {
let cli =
Cli::try_parse_from(["ark", "data", "fetch", slot, "--sample", name]).unwrap();
let error = cli.validate().unwrap_err();
assert_eq!((error.class, error.code), (2, "usage"), "{slot}");
assert_eq!(error.message, "--sample takes no slot", "{slot}");
assert_eq!(
error.hints,
["run `ark data fetch --sample notable-owen`"],
"{slot}"
);
}
}
}

/// Checks that every environment name parses, alone and as a global flag,
/// while an unknown name fails.
#[test]
Expand All @@ -453,12 +485,19 @@ mod tests {
/// positive id parses, known or not.
#[test]
fn slots_are_exact_and_future_ids_remain_addressable() {
for (id, name) in [
let known = [
(1, "reference-genome"),
(2, "gene-annotations"),
(3, "snp-indel-calls"),
(3, "small-variants"),
(4, "variant-catalog"),
] {
(5, "structural-variants"),
(6, "copy-number-variants"),
];
assert_eq!(
slot_names().collect::<Vec<_>>(),
known.map(|(_, name)| name)
);
for (id, name) in known {
assert_eq!(slot_name(id), name);
assert_eq!(parse_slot(name), Ok(id));
assert_eq!(parse_slot(&id.to_string()), Ok(id));
Expand All @@ -470,9 +509,10 @@ mod tests {
"0",
"unspecified",
"-1",
"SNP-INDEL-CALLS",
"snp_indel_calls",
"snp",
"SMALL-VARIANTS",
"small_variants",
"small",
"snp-indel-calls",
"2147483648",
] {
assert!(parse_slot(invalid).is_err());
Expand Down
Loading
Loading