use crate::{Error, InputBam, InputBamBuilder};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum PathOrURLOrStdin {
#[default]
Stdin,
Path(PathBuf),
URL(Url),
}
impl FromStr for PathOrURLOrStdin {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" {
return Ok(PathOrURLOrStdin::Stdin);
}
if let Ok(parsed_url) = Url::parse(s) {
const ALLOWED_SCHEMES: &[&str] = &["http", "https", "ftp"];
if ALLOWED_SCHEMES.contains(&parsed_url.scheme()) {
return Ok(PathOrURLOrStdin::URL(parsed_url));
}
}
let path = PathBuf::from(s);
Ok(PathOrURLOrStdin::Path(path))
}
}
impl fmt::Display for PathOrURLOrStdin {
#[expect(
clippy::pattern_type_mismatch,
reason = "&self/self/etc. does not make a difference to readability here"
)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PathOrURLOrStdin::Stdin => String::from("-"),
PathOrURLOrStdin::Path(v) => v.to_string_lossy().to_string(),
PathOrURLOrStdin::URL(v) => v.to_string(),
}
.fmt(f)
}
}
impl From<PathOrURLOrStdin> for InputBam {
fn from(val: PathOrURLOrStdin) -> Self {
InputBamBuilder::default()
.bam_path(val)
.build()
.expect("InputBam builder should not fail with only bam_path set")
}
}
#[cfg(test)]
#[expect(
clippy::panic,
reason = "panic is acceptable in tests for assertion failures"
)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write as _;
use uuid::Uuid;
#[test]
fn from_str_parses_stdin() {
let result = PathOrURLOrStdin::from_str("-").expect("should parse stdin");
assert!(matches!(result, PathOrURLOrStdin::Stdin));
}
#[test]
fn from_str_parses_url() {
let result =
PathOrURLOrStdin::from_str("https://example.com/file.bam").expect("should parse URL");
match result {
PathOrURLOrStdin::URL(u) => {
assert_eq!(u.scheme(), "https");
assert_eq!(u.host_str(), Some("example.com"));
assert_eq!(u.path(), "/file.bam");
}
PathOrURLOrStdin::Stdin | PathOrURLOrStdin::Path(_) => {
panic!("Expected URL variant")
}
}
}
#[test]
fn from_str_parses_http_url() {
let result =
PathOrURLOrStdin::from_str("http://example.com/data").expect("should parse URL");
match result {
PathOrURLOrStdin::URL(u) => {
assert_eq!(u.scheme(), "http");
}
PathOrURLOrStdin::Stdin | PathOrURLOrStdin::Path(_) => {
panic!("Expected URL variant")
}
}
}
#[test]
fn from_str_parses_existing_path() {
let temp_dir = std::env::temp_dir();
let temp_filename = temp_dir.join(format!("nanalogue_test_{}.txt", Uuid::new_v4()));
{
let mut file = File::create(&temp_filename).expect("should create temp file");
file.write_all(b"test content")
.expect("should write to file");
}
let result = PathOrURLOrStdin::from_str(
temp_filename
.to_str()
.expect("temp path should be valid UTF-8"),
)
.expect("should parse path");
match result {
PathOrURLOrStdin::Path(p) => {
assert_eq!(p, temp_filename);
}
PathOrURLOrStdin::Stdin | PathOrURLOrStdin::URL(_) => {
panic!("Expected Path variant")
}
}
std::fs::remove_file(&temp_filename).expect("should remove temp file");
}
#[test]
fn from_str_accepts_nonexistent_path() {
let nonexistent_path = format!("/nonexistent/path/to/{}.txt", Uuid::new_v4());
let result =
PathOrURLOrStdin::from_str(&nonexistent_path).expect("should accept nonexistent path");
assert!(
matches!(result, PathOrURLOrStdin::Path(_)),
"Expected Path variant for non-existent path"
);
}
#[test]
fn from_str_accepts_any_string_as_path() {
let result =
PathOrURLOrStdin::from_str("not a url or valid path").expect("should accept as path");
assert!(
matches!(result, PathOrURLOrStdin::Path(_)),
"Expected Path variant for arbitrary string"
);
}
#[test]
fn default_is_stdin() {
let default_val = PathOrURLOrStdin::default();
assert!(matches!(default_val, PathOrURLOrStdin::Stdin));
}
#[test]
fn url_scheme_variants() {
let ftp_result = PathOrURLOrStdin::from_str("ftp://example.com/file.txt");
assert!(
matches!(ftp_result, Ok(PathOrURLOrStdin::URL(_))),
"ftp:// should be recognized as URL"
);
let file_result = PathOrURLOrStdin::from_str("file:///path/to/file");
assert!(
matches!(file_result, Ok(PathOrURLOrStdin::Path(_))),
"file:// scheme should be treated as Path, not URL"
);
let data_result = PathOrURLOrStdin::from_str("data:text/plain,hello");
assert!(
matches!(data_result, Ok(PathOrURLOrStdin::Path(_))),
"data: scheme should be treated as Path"
);
let windows_path = PathOrURLOrStdin::from_str("C:/path/to/file.txt");
assert!(
matches!(windows_path, Ok(PathOrURLOrStdin::Path(_))),
"Windows-like paths should be treated as Path"
);
}
#[test]
fn display_stdin() {
let stdin = PathOrURLOrStdin::Stdin;
assert_eq!(stdin.to_string(), "-");
}
#[test]
fn display_path() {
let path = PathOrURLOrStdin::Path("/some/path/to/file.bam".into());
assert_eq!(path.to_string(), "/some/path/to/file.bam");
}
#[test]
fn display_url() {
let url = PathOrURLOrStdin::URL(Url::parse("https://example.com/data.bam").unwrap());
assert_eq!(url.to_string(), "https://example.com/data.bam");
}
#[test]
fn from_path_or_url_or_stdin_to_input_bam_stdin() {
let input = PathOrURLOrStdin::Stdin;
let bam: InputBam = input.into();
assert_eq!(bam.bam_path, PathOrURLOrStdin::Stdin);
}
#[test]
fn from_path_or_url_or_stdin_to_input_bam_path() {
let input = PathOrURLOrStdin::Path("/some/path.bam".into());
let bam: InputBam = input.clone().into();
assert_eq!(bam.bam_path, input);
}
#[test]
fn from_path_or_url_or_stdin_to_input_bam_url() {
let url = Url::parse("https://example.com/data.bam").unwrap();
let input = PathOrURLOrStdin::URL(url.clone());
let bam: InputBam = input.clone().into();
assert_eq!(bam.bam_path, input);
}
}