molconv 0.2.0

Convert between several molecular structure and trajectory file formats.
Documentation
use std::path::{PathBuf,Path};
use std::str::FromStr;
use std::any::type_name;
use std::fs;
use failure::{Error,err_msg,bail};
use molconv::trajectory::TrajectoryType;
use clap::{App,Arg};

/// Parsed CLI data 
pub struct Config {
    /// A vector with the path to all input trajectories
    pub input: Vec<PathBuf>,
    /// Path to the output trajectory
    pub output: Option<PathBuf>,
    /// First frame to read
    pub first: usize,
    /// Last frame to read
    pub last: usize,
    /// Stride for trajectory reading
    pub stride: usize,
    /// Atom ideces to filter trajectories
    pub indeces: Vec<usize>,
}

/// Validate that the file for the given path exists
fn file_exists(val: String) -> Result<(), String> {
    if Path::new(&val).exists() {
        Ok(())
    } else {
        Err(format!("{} does not exist", val))
    }
}

/// Validate that the file is a valid trajectory (examines the file extension)
fn is_trajectory_file(val: String) -> Result<(), String> {
    let path = Path::new(&val);
    match TrajectoryType::from_path(path) {
        Ok(_) => Ok(()),
        Err(err) => Err(format!("{}", err))
    }
}

/// Validate that the file for the given path exists and has the file ext.
/// of a trajectory
fn trajectory_exists(val: String) -> Result<(), String> {
    file_exists(val.clone())?;
    is_trajectory_file(val)
}

// Validate that a String can be represented as a number of type `F`
fn number<F: FromStr>(val: String) -> Result<(), String> {
    if val.parse::<F>().is_ok() {
        Ok(())
    } else {
        Err(format!("Not a {}", type_name::<F>()))
    }
}

/// Read a file from Path that contails usize numbers separated by whitespace
/// and parse them into a vector of numbers
fn read_index_file(path: &Path) -> Result<Vec<usize>, Error> {
    let contents = fs::read_to_string(path)?;
    let indeces = contents.split_whitespace()
        .map(|s| s.parse::<usize>())
        .collect::<Result<Vec<usize>, _>>();
    match indeces {
        Ok(ind) => Ok(ind),
        Err(err) => Err(err_msg(format!("Failed to read index file: {}", err)))
    }
}

/// Parse command line arguments and return a config file
/// Also performs the neccessary validation of input
pub fn parse_args() -> Result<Config, Error> {
    let matches = App::new("molconv")
        .about("Convert between differnet molecular structure and trjectory formats.")
        .after_help("Supported file formats: TRR, XTC")
        .version(crate_version!())
        .arg(
            Arg::with_name("input")
                .help("Input trajectory")
                .long("input")
                .short("f")
                .required(true)
                .takes_value(true)
                .validator(trajectory_exists)
                .min_values(1)
            )
        .arg(
            Arg::with_name("output")
                .help("Output trajectory")
                .long("output")
                .short("o")
                .takes_value(true)
                .validator(is_trajectory_file)
            )
        .arg(
            Arg::with_name("index_file")
                .help("Zero based atom indeces separated by whitespace")
                .long("indexfile")
                .short("i")
                .required(false)
                .takes_value(true)
                .validator(file_exists)
            )
        .arg(
            Arg::with_name("first")
                .help("First frame to read")
                .long("first")
                .default_value("1")
                .required(false)
                .takes_value(true)
                .validator(number::<usize>)
            )
        .arg(
            Arg::with_name("last")
                .help("Last frame to read")
                .long("last")
                .required(false)
                .takes_value(true)
                .validator(number::<usize>)
            )
        .arg(
            Arg::with_name("stride")
                .help("Skip that many frames after each frame")
                .long("stride")
                .default_value("1")
                .required(false)
                .takes_value(true)
                .validator(number::<usize>)
            )
        
        .get_matches();
    let input: Vec<PathBuf> = matches.values_of("input").unwrap()
        .map(|i| PathBuf::from(i))
        .collect();
    let output = if matches.is_present("output") {
        Some(PathBuf::from(matches.value_of("output").unwrap()))
    } else {
        None
    };

    // remove 1 from first and last: input is 1 based, but we need it 0 based
    let first = matches.value_of("first").unwrap().parse::<usize>()? -1;
    let last = if matches.is_present("last") {
        matches.value_of("last").unwrap().parse::<usize>()? - 1
    } else {
        std::usize::MAX - 1  // because we might later do `last + 1` 
    };
    let stride = matches.value_of("stride").unwrap().parse::<usize>()?; 
    if last < first {
        bail!("Last frame cannot be before first frame!")
    }
    let indeces = if matches.is_present("index_file") {
        let index_path = Path::new(matches.value_of("index_file").unwrap());
        read_index_file(&index_path)?
    } else {
        vec![]
    };

    Ok(Config {
        input,
        output,
        first,
        last,
        stride,
        indeces
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_file_exists() {
        let exist = file_exists(String::from("tests/data/1l2y.trr"));
        assert!(exist.is_ok());

        let exist = file_exists(String::from("some/random/path"));
        assert!(exist.is_err());
    }

    #[test]
    fn test_is_trajectory_file() {
        let exist = is_trajectory_file(String::from("some/random/path/1l2y.trr"));
        assert!(exist.is_ok());

        let exist = is_trajectory_file(String::from("some/random/path"));
        assert!(exist.is_err());
    }     

    #[test]
    fn test_trajectory_exists() {
        let exist = trajectory_exists(String::from("tests/data/1l2y.trr"));
        assert!(exist.is_ok());

        let exist = trajectory_exists(String::from("some/random/path"));
        assert!(exist.is_err());

        let exist = trajectory_exists(String::from("/tests/data/1l2y.index"));
        assert!(exist.is_err());
    }     

    #[test]
    fn test_read_index_file() {
        let indeces = read_index_file(Path::new("tests/data/1l2y.index")).unwrap();
        let expected_indeces: Vec<usize> = vec![1, 3, 5];
        indeces.iter().zip(&expected_indeces).for_each(|(i, e_i)| assert!(i == e_i))        
    }     
       
}