ommui_file_loading 0.7.1

Filesystem loading helpers for ommui
Documentation
#![deny(missing_docs)]

//! File system helpers used in OMMUI.

use glob::glob;
use indexmap::IndexSet;
use log::info;
use snafu::{Backtrace, ResultExt, Snafu};
use std::fmt::Debug;
use std::fs::File;
use std::io::Read;
use std::path::Path;

type Result<T, E = Error> = std::result::Result<T, E>;

/// Load a string from a file.
pub fn load_string<P: AsRef<Path>>(path: P) -> Result<String> {
    let path = path.as_ref();
    info!("Loading file {:?}", path);

    let mut file = File::open(path).context(FileOpen {
        path: path.to_path_buf(),
    })?;
    let mut content = String::new();
    file.read_to_string(&mut content).context(FileRead {
        path: path.to_path_buf(),
    })?;
    Ok(content)
}

/// Load a json data structure from a file.
pub fn load_json<T, P: AsRef<Path>>(path: P) -> Result<T>
where
    T: serde::de::DeserializeOwned,
{
    let path = path.as_ref();
    info!("Loading file {:?}", path);
    Ok(serde_json::from_reader(File::open(path).context(FileOpen {
        path: path.to_path_buf(),
    })?)
    .context(JsonLoading {
        path: path.to_path_buf(),
    })?)
}

/// Load a json data structure from a file, buffered into a string first.
pub fn load_json_buffered<T, P: AsRef<Path>>(path: P) -> Result<T>
where
    T: serde::de::DeserializeOwned,
{
    let path = path.as_ref();
    info!("Loading file {:?}", path);
    let s = load_string(path)?;
    Ok(serde_json::from_str(&s).context(JsonLoading {
        path: path.to_path_buf(),
    })?)
}

/// Load a `IndexSet` from a directory, one entry for each glob match.
///
/// This function takes a glob pattern that determines which entries
/// get put into the set. The entries in the set contain the parts
/// that differ from the pattern.
///
/// For example if the directory `/srv/data` contains the files
/// `hello.json`, `world.json` and `readme.txt`, and the glob pattern
/// is `/srv/data/*.json`, then the resulting set will contain
/// `hello` and `world`.
pub fn load_directory_listing<
    T: std::str::FromStr + std::hash::Hash + std::cmp::Eq,
>(
    pattern: &str,
) -> Result<IndexSet<T>> {
    info!("Finding pattern: {}", pattern);

    let listing = glob(pattern)
        .context(GlobPattern {
            pattern: pattern.to_string(),
        })?
        .filter_map(|r| r.ok())
        .filter_map(|p| p.to_str().map(str::to_string))
        .map(|p| diff_extract(pattern, &p))
        .filter_map(|s| T::from_str(&s).ok())
        .collect::<IndexSet<T>>();

    Ok(listing)
}

fn diff_is_right<T>(r: &diff::Result<T>) -> bool {
    match *r {
        diff::Result::Right(_) => true,
        diff::Result::Left(_) | diff::Result::Both(_, _) => false,
    }
}

fn diff_extract_item<T>(r: &diff::Result<T>) -> &T {
    match *r {
        diff::Result::Right(ref t)
        | diff::Result::Left(ref t)
        | diff::Result::Both(ref t, _) => t,
    }
}

fn diff_extract(a: &str, b: &str) -> String {
    let d = diff::chars(a, b);
    d.iter()
        .filter(|r| diff_is_right(*r))
        .map(|r| diff_extract_item(r))
        .collect()
}

/// A trait defining a method for loading data structure from a path.
pub trait PathLoad: Sized {
    /// Load the data structure from a path.
    fn load_from_path<P: AsRef<Path> + Debug>(path: P) -> Result<Self>;
}

impl<T> PathLoad for T
where
    T: serde::de::DeserializeOwned,
{
    fn load_from_path<P: AsRef<Path> + Debug>(path: P) -> Result<T> {
        load_json::<T, P>(path)
    }
}

/// An error which gets produced in this crate.
#[derive(Debug, Snafu)]
pub enum Error {
    /// A glob pattern had problems.
    #[snafu(display("Error in glob pattern: {}: {}", pattern, source))]
    GlobPattern {
        /// The pattern which caused the error.
        pattern: String,
        /// The source of the error.
        source: glob::PatternError,
        /// The captured backtrace
        backtrace: Backtrace,
    },
    /// A file open error happened.
    #[snafu(display("Couldn't open file {}: {}", path.display(), source))]
    FileOpen {
        /// The affected path.
        path: std::path::PathBuf,
        /// The source of the error.
        source: std::io::Error,
        /// The captured backtrace
        backtrace: Backtrace,
    },
    /// A file reading error happened.
    #[snafu(display("Couldn't read from file {}: {}", path.display(), source))]
    FileRead {
        /// The affected path.
        path: std::path::PathBuf,
        /// The source of the error.
        source: std::io::Error,
        /// The captured backtrace
        backtrace: Backtrace,
    },
    /// An error happened while loading JSON data structures.
    #[snafu(display("Couldn't deserialize JSON from file {}: {}", path.display(), source))]
    JsonLoading {
        /// The affected path.
        path: std::path::PathBuf,
        /// The source of the error.
        source: serde_json::Error,
        /// The captured backtrace
        backtrace: Backtrace,
    },
}

#[cfg(test)]
mod tests {
    use indexmap::IndexSet;
    use log::info;
    use std::fs::{create_dir, File};
    use std::io::Write;
    use tempfile::tempdir;

    #[test]
    fn load_string_from_path() {
        use crate::load_string;

        let dir = tempdir().unwrap();

        let path = dir.path().join("example.txt");

        {
            let mut file = File::create(&path).unwrap();
            file.write(b"hello world").unwrap();
        }

        {
            let s = load_string(&path).unwrap();
            assert_eq!(s, "hello world".to_string());
        }
    }

    #[test]
    fn load_string_from_path_string() {
        use crate::load_string;

        let dir = tempdir().unwrap();

        let path =
            dir.path().join("example.txt").to_string_lossy().to_string();

        {
            let mut file = File::create(&path).unwrap();
            file.write(b"hello world").unwrap();
        }

        {
            let s = load_string(&path).unwrap();
            assert_eq!(s, "hello world".to_string());
        }
    }

    #[test]
    fn load_directory_listing() {
        use crate::load_directory_listing;
        let dir = tempdir().unwrap();

        create_dir(dir.path().join("hello_world.new")).unwrap();
        create_dir(dir.path().join("hello_neighbour.new")).unwrap();
        create_dir(dir.path().join("hello_neighbour")).unwrap();
        create_dir(dir.path().join("xhello_neighbour.new")).unwrap();

        let dir = dir.path().to_str().unwrap();
        let pattern = format!("{}/hello_*.new", dir);
        let listing = load_directory_listing(&pattern).unwrap();

        let expected = ["world", "neighbour"]
            .into_iter()
            .map(|s| String::from(*s))
            .collect::<IndexSet<String>>();
        info!("Listing: {:#?}", listing);
        assert_eq!(listing, expected);
    }
}