use crate::errors::{FgumiError, Result};
use bytesize::ByteSize;
use std::fmt::Display;
use std::path::Path;
pub fn validate_file_exists<P: AsRef<Path>>(path: P, description: &str) -> Result<()> {
let path_ref = path.as_ref();
if !path_ref.exists() {
return Err(FgumiError::InvalidFileFormat {
file_type: description.to_string(),
path: path_ref.display().to_string(),
reason: "File does not exist".to_string(),
});
}
Ok(())
}
pub fn validate_files_exist<P: AsRef<Path>>(files: &[(P, &str)]) -> Result<()> {
for (path, desc) in files {
validate_file_exists(path, desc)?;
}
Ok(())
}
#[allow(clippy::needless_pass_by_value)]
pub fn validate_min_max<T: Ord + Display>(
min_val: T,
max_val: Option<T>,
min_name: &str,
max_name: &str,
) -> Result<()> {
if let Some(max) = max_val {
if max < min_val {
return Err(FgumiError::InvalidParameter {
parameter: max_name.to_string(),
reason: format!("{max_name} ({max}) must be >= {min_name} ({min_val})"),
});
}
}
Ok(())
}
pub fn validate_error_rate(rate: f64, _name: &str) -> Result<()> {
if !(0.0..=1.0).contains(&rate) {
return Err(FgumiError::InvalidFrequency { value: rate, min: 0.0, max: 1.0 });
}
Ok(())
}
pub fn validate_quality_score(quality: u8, _name: &str) -> Result<()> {
if quality > 93 {
return Err(FgumiError::InvalidQuality { value: quality, max: 93 });
}
Ok(())
}
#[allow(clippy::needless_pass_by_value)]
pub fn validate_positive<T: Ord + Display + Default>(value: T, name: &str) -> Result<()> {
if value <= T::default() {
return Err(FgumiError::InvalidParameter {
parameter: name.to_string(),
reason: format!("Must be positive (> 0), got: {value}"),
});
}
Ok(())
}
pub fn parse_memory_size(size_str: &str) -> Result<u64> {
let trimmed = size_str.trim();
if trimmed.is_empty() {
return Err(FgumiError::InvalidMemorySize {
reason: "Memory size cannot be empty".to_string(),
});
}
if trimmed.starts_with('-') {
return Err(FgumiError::InvalidMemorySize {
reason: format!("Memory size cannot be negative: '{trimmed}'"),
});
}
if let Ok(mb_value) = trimmed.parse::<u64>() {
if mb_value == 0 {
return Err(FgumiError::InvalidMemorySize {
reason: "Memory size cannot be zero".to_string(),
});
}
if mb_value > 1_000_000 {
return Err(FgumiError::InvalidMemorySize {
reason: format!(
"Plain number memory size too large: {} MiB. Use human-readable format like '{}GB' instead.",
mb_value,
mb_value / 1000
),
});
}
return mb_value.checked_mul(1024 * 1024).ok_or_else(|| FgumiError::InvalidMemorySize {
reason: format!("Memory size calculation overflow for {mb_value} MiB"),
});
}
if trimmed.contains('e') || trimmed.contains('E') {
return Err(FgumiError::InvalidMemorySize {
reason: format!(
"Scientific notation not supported: '{trimmed}'. Use integer values or human-readable formats like '2GB'."
),
});
}
if trimmed.contains('.') && trimmed.chars().all(|c| c.is_ascii_digit() || c == '.') {
return Err(FgumiError::InvalidMemorySize {
reason: format!(
"Plain decimal numbers not supported: '{trimmed}'. Use an integer for MiB (e.g. '768') or a human-readable format (e.g. '1.5GB')."
),
});
}
match trimmed.parse::<ByteSize>() {
Ok(size) => {
if size.0 == 0 {
return Err(FgumiError::InvalidMemorySize {
reason: format!("Memory size cannot be zero: '{trimmed}'"),
});
}
Ok(size.0)
}
Err(_) => Err(FgumiError::InvalidMemorySize {
reason: format!(
"Invalid memory size '{trimmed}'. Valid formats:\n\
- Plain numbers (interpreted as MiB): '768', '4096'\n\
- Human-readable (decimal): '2GB', '1024MB'\n\
- Human-readable (binary): '1GiB', '512MiB'"
),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use std::path::PathBuf;
use tempfile::NamedTempFile;
#[test]
fn test_validate_file_exists_valid() {
let temp_file = NamedTempFile::new().expect("creating temp file/dir should succeed");
validate_file_exists(temp_file.path(), "Test file")
.expect("file validation should succeed");
}
#[test]
fn test_validate_file_exists_invalid() {
let result = validate_file_exists("/nonexistent/file.bam", "Input file");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Input file"));
assert!(err_msg.contains("does not exist"));
}
#[test]
fn test_validate_files_exist_all_valid() {
let temp1 = NamedTempFile::new().expect("creating temp file/dir should succeed");
let temp2 = NamedTempFile::new().expect("creating temp file/dir should succeed");
let files =
vec![(temp1.path().to_path_buf(), "File 1"), (temp2.path().to_path_buf(), "File 2")];
validate_files_exist(&files).expect("file validation should succeed");
}
#[test]
fn test_validate_files_exist_one_invalid() {
let temp1 = NamedTempFile::new().expect("creating temp file/dir should succeed");
let files = vec![
(temp1.path().to_path_buf(), "File 1"),
(PathBuf::from("/nonexistent.bam"), "File 2"),
];
let result = validate_files_exist(&files);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("File 2"));
}
#[test]
fn test_validate_min_max_valid() -> Result<()> {
validate_min_max(1, Some(10), "min-reads", "max-reads")?;
validate_min_max(5, Some(5), "min-reads", "max-reads")?;
validate_min_max(1, None, "min-reads", "max-reads")?;
Ok(())
}
#[test]
fn test_validate_min_max_invalid() {
let result = validate_min_max(10, Some(5), "min-reads", "max-reads");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("max-reads"));
assert!(err_msg.contains("min-reads"));
assert!(err_msg.contains(">="));
}
#[rstest]
#[case(0.0, true, "minimum valid rate")]
#[case(0.01, true, "typical low rate")]
#[case(0.5, true, "middle rate")]
#[case(1.0, true, "maximum valid rate")]
#[case(-0.1, false, "negative rate")]
#[case(1.5, false, "above maximum")]
#[case(2.0, false, "far above maximum")]
fn test_validate_error_rate(
#[case] rate: f64,
#[case] should_succeed: bool,
#[case] description: &str,
) {
let result = validate_error_rate(rate, "error-rate");
if should_succeed {
assert!(result.is_ok(), "Failed for: {description}");
} else {
assert!(result.is_err(), "Should have failed for: {description}");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Invalid frequency threshold"),
"Missing expected error for: {description}"
);
assert!(err_msg.contains("between 0 and 1"), "Missing range info for: {description}");
}
}
#[rstest]
#[case(0, true, "minimum valid quality")]
#[case(30, true, "typical quality")]
#[case(93, true, "maximum valid quality")]
#[case(60, true, "high quality")]
#[case(94, false, "just above maximum")]
#[case(100, false, "far above maximum")]
fn test_validate_quality_score(
#[case] score: u8,
#[case] should_succeed: bool,
#[case] description: &str,
) {
let result = validate_quality_score(score, "min-base-quality");
if should_succeed {
assert!(result.is_ok(), "Failed for: {description}");
} else {
assert!(result.is_err(), "Should have failed for: {description}");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Invalid quality threshold"),
"Missing expected error for: {description}"
);
assert!(err_msg.contains("between 0 and 93"), "Missing range info for: {description}");
}
}
#[test]
fn test_validate_positive_valid() -> Result<()> {
validate_positive(1, "min-reads")?;
validate_positive(100, "min-reads")?;
validate_positive(1_usize, "threshold")?;
Ok(())
}
#[test]
fn test_validate_positive_zero() {
let result = validate_positive(0, "min-reads");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Invalid parameter 'min-reads'"));
assert!(err_msg.contains("Must be positive"));
assert!(err_msg.contains("got: 0"));
}
#[test]
fn test_validate_positive_negative() {
let result = validate_positive(-5, "threshold");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Invalid parameter 'threshold'"));
assert!(err_msg.contains("Must be positive"));
assert!(err_msg.contains("got: -5"));
}
#[test]
fn test_parse_memory_size_plain_numbers() {
assert_eq!(
parse_memory_size("768").expect("parse '768' should succeed"),
768 * 1024 * 1024
);
assert_eq!(parse_memory_size("1").expect("parse '1' should succeed"), 1024 * 1024);
assert_eq!(
parse_memory_size("4096").expect("parse '4096' should succeed"),
4096 * 1024 * 1024
);
}
#[test]
fn test_parse_memory_size_human_readable() {
assert_eq!(
parse_memory_size("2GB").expect("parse '2GB' should succeed"),
2 * 1000 * 1000 * 1000
);
assert_eq!(
parse_memory_size("2G").expect("parse '2G' should succeed"),
2 * 1000 * 1000 * 1000
);
assert_eq!(
parse_memory_size("1024MB").expect("parse '1024MB' should succeed"),
1024 * 1000 * 1000
);
assert_eq!(
parse_memory_size("1024M").expect("parse '1024M' should succeed"),
1024 * 1000 * 1000
);
assert_eq!(
parse_memory_size("1GiB").expect("parse '1GiB' should succeed"),
1024 * 1024 * 1024
);
assert_eq!(
parse_memory_size("512MiB").expect("parse '512MiB' should succeed"),
512 * 1024 * 1024
);
}
#[test]
fn test_parse_memory_size_invalid() {
assert!(parse_memory_size("invalid").is_err());
assert!(parse_memory_size("").is_err());
assert!(parse_memory_size("GB2").is_err());
}
#[test]
fn test_parse_memory_size_zero() {
assert!(parse_memory_size("0").is_err());
assert!(parse_memory_size("0MB").is_err());
assert!(parse_memory_size("0GB").is_err());
}
#[test]
fn test_parse_memory_size_edge_cases() {
assert!(parse_memory_size("").is_err());
assert!(parse_memory_size(" ").is_err());
assert!(parse_memory_size("-100").is_err());
assert!(parse_memory_size("-1GB").is_err());
assert!(parse_memory_size("1.5").is_err());
assert!(parse_memory_size("1.5GB").is_ok());
assert!(parse_memory_size("2.5GB").is_ok());
assert!(parse_memory_size("1e3").is_err());
assert!(parse_memory_size("1E6").is_err());
assert!(parse_memory_size("9999999").is_err());
}
#[test]
fn test_parse_memory_size_whitespace_handling() {
assert_eq!(
parse_memory_size(" 768 ").expect("parse trimmed '768' should succeed"),
768 * 1024 * 1024
);
assert_eq!(
parse_memory_size("\t1GB\n").expect("parse trimmed '1GB' should succeed"),
1000 * 1000 * 1000
);
}
#[test]
fn test_parse_memory_size_overflow() {
let very_large = format!("{}", u64::MAX / 1024);
assert!(parse_memory_size(&very_large).is_err());
}
}