use nord_usb::wire::Location;
use std::path::PathBuf;
#[derive(Debug)]
pub enum Target {
File(PathBuf),
Slot(Location),
}
pub fn target(s: &str) -> Result<Target, String> {
let path = PathBuf::from(s);
if path.exists() {
return Ok(Target::File(path));
}
match parse(s) {
Ok(at) => Ok(Target::Slot(at)),
Err(e) => Err(format!("{s}: no such file, and not a slot ({e})")),
}
}
pub fn parse(s: &str) -> Result<Location, String> {
let (b, l) = s
.split_once([':', '-'])
.ok_or_else(|| format!("expected BANK:SLOT (e.g. 7:4), got {s:?}"))?;
let bank: u32 = b.trim().parse().map_err(|_| format!("bad bank {b:?}"))?;
let slot: u32 = l.trim().parse().map_err(|_| format!("bad slot {l:?}"))?;
if bank == 0 || slot == 0 {
return Err("banks and slots are numbered from 1, as shown on the instrument".into());
}
Ok(Location::from_user(bank, slot))
}
pub fn parse_all(slots: &[String]) -> Result<Vec<Location>, String> {
slots.iter().map(|s| parse(s)).collect()
}
pub fn shown(at: Location) -> String {
format!("bank {} slot {}", at.bank + 1, at.slot + 1)
}
pub fn addr(at: Location) -> String {
format!("{}:{}", at.bank + 1, at.slot + 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn both_slot_separators_parse_to_the_same_place() {
let colon = parse("7:4").unwrap();
let dash = parse("7-4").unwrap();
assert_eq!(colon, dash);
assert_eq!((colon.bank, colon.slot), (6, 3));
}
#[test]
fn whitespace_around_the_numbers_is_tolerated() {
assert_eq!(parse(" 8 : 14 ").unwrap(), parse("8:14").unwrap());
}
#[test]
fn zero_is_rejected_because_the_panel_counts_from_one() {
for bad in ["0:1", "1:0", "0:0"] {
let err = parse(bad).unwrap_err();
assert!(err.contains("numbered from 1"), "{bad}: {err}");
}
}
#[test]
fn malformed_slots_say_what_was_expected() {
assert!(parse("74").unwrap_err().contains("BANK:SLOT"));
assert!(parse("7:x").unwrap_err().contains("bad slot"));
assert!(parse("x:4").unwrap_err().contains("bad bank"));
}
#[test]
fn a_bad_slot_in_a_list_fails_the_whole_list() {
let slots = ["7:1".to_string(), "nope".to_string()];
assert!(parse_all(&slots).is_err());
assert_eq!(parse_all(&slots[..1]).unwrap().len(), 1);
}
#[test]
fn an_existing_path_wins_over_a_slot_reading() {
let existing = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml");
assert!(matches!(target(existing), Ok(Target::File(_))));
assert!(matches!(target("7:4"), Ok(Target::Slot(_))));
}
#[test]
fn an_ambiguous_target_error_names_both_readings() {
let err = target("no-such-file.ne5p").unwrap_err();
assert!(err.contains("no such file"), "{err}");
assert!(err.contains("not a slot"), "{err}");
}
}