anydir 0.1.15

A crate for working with both compile-time embedded directories and runtime directories using an abstraction.
Documentation
pub use anydir_macro::embed_dir;
use include_dir::Dir;
use std::{
    fs, io,
    path::{Path, PathBuf},
};

pub trait FileEntry {
    fn path(&self) -> &Path;
    fn read_bytes(&self) -> Result<Vec<u8>, io::Error>;
    fn read_string(&self) -> Result<String, io::Error>;
}

#[derive(Debug, Clone)]
pub struct CtFileEntry {
    relative_path: PathBuf,
    file: &'static include_dir::File<'static>,
}

impl FileEntry for CtFileEntry {
    fn path(&self) -> &Path {
        &self.relative_path
    }

    fn read_bytes(&self) -> Result<Vec<u8>, io::Error> {
        Ok(self.file.contents().to_vec())
    }

    fn read_string(&self) -> io::Result<String> {
        self.file.contents_utf8().map(String::from).ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                "Embedded file is not valid UTF-8",
            )
        })
    }
}

#[derive(Debug, Clone)]
pub struct RtFileEntry {
    absolute_path: PathBuf,
    relative_path: PathBuf,
}

impl FileEntry for RtFileEntry {
    fn path(&self) -> &Path {
        &self.relative_path
    }

    fn read_bytes(&self) -> Result<Vec<u8>, io::Error> {
        fs::read(&self.absolute_path)
    }

    fn read_string(&self) -> io::Result<String> {
        fs::read_to_string(&self.absolute_path)
    }
}

#[derive(Debug, Clone)]
pub enum AnyFileEntry {
    Ct(CtFileEntry),
    Rt(RtFileEntry),
}

impl FileEntry for AnyFileEntry {
    fn path(&self) -> &Path {
        match self {
            AnyFileEntry::Ct(entry) => entry.path(),
            AnyFileEntry::Rt(entry) => entry.path(),
        }
    }
    fn read_bytes(&self) -> io::Result<Vec<u8>> {
        match self {
            AnyFileEntry::Ct(entry) => entry.read_bytes(),
            AnyFileEntry::Rt(entry) => entry.read_bytes(),
        }
    }
    fn read_string(&self) -> io::Result<String> {
        match self {
            AnyFileEntry::Ct(entry) => entry.read_string(),
            AnyFileEntry::Rt(entry) => entry.read_string(),
        }
    }
}

pub trait DirOps {
    fn file_entries(&self) -> Vec<AnyFileEntry>;
}

#[derive(Debug, Clone, PartialEq)]
pub struct CtDir {
    pub dir: &'static Dir<'static>,
}

impl DirOps for CtDir {
    fn file_entries(&self) -> Vec<AnyFileEntry> {
        self.dir
            .files()
            .map(|f| {
                AnyFileEntry::Ct(CtFileEntry {
                    relative_path: f.path().to_path_buf(),
                    file: f,
                })
            })
            .collect()
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct RtDir {
    pub path: PathBuf,
}

impl RtDir {
    pub fn path(&self) -> &PathBuf {
        &self.path
    }
}

impl DirOps for RtDir {
    fn file_entries(&self) -> Vec<AnyFileEntry> {
        let base_dir = &self.path;
        if let Ok(entries) = fs::read_dir(base_dir) {
            entries
                .flatten()
                .filter_map(|entry| {
                    let absolute_path = entry.path();
                    if absolute_path.is_file() {
                        let relative_path = absolute_path
                            .strip_prefix(base_dir)
                            .unwrap_or(&absolute_path)
                            .to_path_buf();
                        Some(AnyFileEntry::Rt(RtFileEntry {
                            absolute_path,
                            relative_path,
                        }))
                    } else {
                        None
                    }
                })
                .collect()
        } else {
            Vec::new()
        }
    }
}

pub enum AnyDir {
    Ct(CtDir),
    Rt(RtDir),
}

impl AnyDir {
    pub fn as_rt(&self) -> Option<&RtDir> {
        match self {
            AnyDir::Rt(rt) => Some(rt),
            _ => None,
        }
    }
}

impl DirOps for AnyDir {
    fn file_entries(&self) -> Vec<AnyFileEntry> {
        match self {
            AnyDir::Ct(c) => c.file_entries(),
            AnyDir::Rt(r) => r.file_entries(),
        }
    }
}

pub fn anydir_rt<P: Into<std::path::PathBuf>>(path: P) -> AnyDir {
    AnyDir::Rt(RtDir { path: path.into() })
}

#[macro_export]
macro_rules! anydir {
    (ct, $path:literal) => {{
        $crate::AnyDir::Ct($crate::CtDir {
            dir: embed_dir!($path),
        })
    }};
    (rt, $path:expr) => {
        $crate::anydir_rt($path)
    };
}

#[test]
fn basic() {
    let dir = anydir!(ct, "$CARGO_MANIFEST_DIR");
    println!("-----------");
    for entry in dir.file_entries() {
        println!("{:?}", entry.path());
    }

    let dir = anydir!(rt, ".");
    if let Some(rt) = dir.as_rt() {
        println!("{}", rt.path().display());
    }
}