unclog 0.7.3

unclog allows you to build your changelog from a collection of independent files. This helps prevent annoying and unnecessary merge conflicts when collaborating on shared codebases.
Documentation
//! File system-related utilities to help with manipulating changelogs.

use crate::{Config, Error, Result};
use log::{debug, info};
use std::fs;
use std::path::{Path, PathBuf};

pub fn path_to_str<P: AsRef<Path>>(path: P) -> String {
    path.as_ref().to_string_lossy().to_string()
}

pub fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
    let path = path.as_ref();
    fs::read_to_string(path).map_err(|e| Error::Io(path.to_path_buf(), e))
}

pub fn read_to_string_opt<P: AsRef<Path>>(path: P) -> Result<Option<String>> {
    let path = path.as_ref();
    if fs::metadata(path).is_err() {
        return Ok(None);
    }
    read_to_string(path).map(Some)
}

pub fn ensure_dir(path: &Path) -> Result<()> {
    if fs::metadata(path).is_err() {
        fs::create_dir(path).map_err(|e| Error::Io(path.to_path_buf(), e))?;
        info!("Created directory: {}", path_to_str(path));
    }
    let meta = fs::metadata(path).map_err(|e| Error::Io(path.to_path_buf(), e))?;
    if !meta.is_dir() {
        return Err(Error::ExpectedDir(path_to_str(path)));
    }
    Ok(())
}

pub fn rm_gitkeep(path: &Path) -> Result<()> {
    let path = path.join(".gitkeep");
    if fs::metadata(&path).is_ok() {
        fs::remove_file(&path).map_err(|e| Error::Io(path.to_path_buf(), e))?;
        debug!("Removed .gitkeep file from: {}", path_to_str(&path));
    }
    Ok(())
}

pub fn read_and_filter_dir<F>(path: &Path, filter: F) -> Result<Vec<PathBuf>>
where
    F: Fn(fs::DirEntry) -> Option<Result<PathBuf>>,
{
    fs::read_dir(path)
        .map_err(|e| Error::Io(path.to_path_buf(), e))?
        .filter_map(|r| match r {
            Ok(e) => filter(e),
            Err(e) => Some(Err(Error::Io(path.to_path_buf(), e))),
        })
        .collect::<Result<Vec<PathBuf>>>()
}

pub fn entry_filter(config: &Config, entry: fs::DirEntry) -> Option<Result<PathBuf>> {
    let meta = match entry.metadata() {
        Ok(m) => m,
        Err(e) => return Some(Err(Error::Io(entry.path(), e))),
    };
    let path = entry.path();
    let ext = path.extension()?.to_str()?;
    if meta.is_file() && ext == config.change_sets.entry_ext {
        Some(Ok(path))
    } else {
        None
    }
}

pub fn get_relative_path<P: AsRef<Path>, Q: AsRef<Path>>(path: P, prefix: Q) -> Result<PathBuf> {
    Ok(path.as_ref().strip_prefix(prefix.as_ref())?.to_path_buf())
}

pub fn file_exists<P: AsRef<Path>>(path: P) -> bool {
    let path = path.as_ref();
    if let Ok(meta) = fs::metadata(path) {
        return meta.is_file();
    }
    false
}

pub fn dir_exists<P: AsRef<Path>>(path: P) -> bool {
    let path = path.as_ref();
    if let Ok(meta) = fs::metadata(path) {
        return meta.is_dir();
    }
    false
}

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

    #[test]
    fn relative_path_extraction() {
        assert_eq!(
            "mypackage",
            get_relative_path("/path/to/mypackage", "/path/to")
                .unwrap()
                .to_str()
                .unwrap()
        )
    }
}