subplot 0.2.0

tools for specifying, documenting, and implementing automated acceptance tests for systems and software
Documentation
//! Resources for subplot.
//!
//! This module encapsulates a mechanism for subplot to find resource files.
//! Resource files come from a number of locations, such as embedded into the
//! binary, from template paths given on the CLI, or from paths built into the
//! binary and present on disk.

use std::io::{self, Cursor, Read};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use structopt::StructOpt;

#[allow(missing_docs)]
#[derive(Debug, StructOpt)]
// Options which relate to resource management
//
// To use this, include them *flat* in your options struct, and then after
// parsing, call the [ResourceOpts::handle()] function.
pub struct ResourceOpts {
    #[structopt(
        long,
        number_of_values = 1,
        help = "Look for code templates and other resources in DIR",
        name = "DIR"
    )]
    resources: Vec<PathBuf>,
}

impl ResourceOpts {
    /// Handle any supplied resource related arguments
    pub fn handle<P: AsRef<Path>>(&self, doc_path: Option<P>) {
        for rpath in &self.resources {
            add_search_path(rpath);
        }
        if let Some(doc_path) = doc_path.as_ref() {
            add_search_path(doc_path);
        }
    }
}

use lazy_static::lazy_static;

lazy_static! {
    static ref SEARCH_PATHS: Mutex<Vec<PathBuf>> = {
        let ret = Vec::new();
        Mutex::new(ret)
    };
    static ref TEMPLATE_NAME: Mutex<Option<String>> = Mutex::new(None);
}

static EMBEDDED_FILES: &[(&str, &[u8])] = include!(concat!(env!("OUT_DIR"), "/embedded_files.inc"));

/// Put a path at the back of the queue to search in
fn add_search_path<P: AsRef<Path>>(path: P) {
    SEARCH_PATHS
        .lock()
        .expect("Unable to lock SEARCH_PATHS")
        .push(path.as_ref().into());
}

/// Set the template name, for use in searching for content...
pub fn set_template(template: &str) {
    *TEMPLATE_NAME.lock().expect("Unable to lock TEMPLATE_NAME") = Some(template.to_string());
}

/// Open a file for reading, honouring search paths established during
/// startup, and falling back to potentially embedded file content.
///
/// The search path sequence is:
///
/// First any path given to `--resources` in the order of the arguments
/// Second, it's relative to the input document.
/// Third we look in `FALLBACK_PATH` if it's defined
/// Finally we check for an embedded file.
///
/// Then we repeat all the above, inserting the template name in the subpath
/// too.
fn open<P: AsRef<Path>>(subpath: P) -> io::Result<Box<dyn Read>> {
    let subpath = subpath.as_ref();
    match internal_open(subpath) {
        Ok(r) => Ok(r),
        Err(e) => {
            let template = TEMPLATE_NAME.lock().expect("Unable to lock TEMPLATE_NAME");
            if let Some(templ) = template.as_deref() {
                let subpath = Path::new(templ).join(subpath);
                match internal_open(&subpath) {
                    Ok(r) => Ok(r),
                    Err(sub_e) => {
                        if sub_e.kind() != io::ErrorKind::NotFound
                            && e.kind() == io::ErrorKind::NotFound
                        {
                            Err(sub_e)
                        } else {
                            Err(e)
                        }
                    }
                }
            } else {
                Err(e)
            }
        }
    }
}

fn internal_fallback_path() -> Option<&'static Path> {
    match env!("FALLBACK_PATH") {
        "" => None,
        s => Some(Path::new(s)),
    }
}

fn internal_open(subpath: &Path) -> io::Result<Box<dyn Read>> {
    let search_paths = SEARCH_PATHS.lock().expect("Unable to lock SEARCH_PATHS");
    let search_paths = search_paths.iter().map(|p| p.as_path());
    let search_paths = std::iter::empty()
        .chain(search_paths)
        .chain(internal_fallback_path());
    let mut ret = Err(io::Error::new(
        io::ErrorKind::NotFound,
        format!("Unable to find {} in resource paths", subpath.display()),
    ));
    for basepath in search_paths {
        let full_path = basepath.join(subpath);
        ret = std::fs::File::open(full_path);
        if ret.is_ok() {
            break;
        }
    }

    match ret {
        Ok(ret) => Ok(Box::new(ret)),
        Err(e) => {
            if let Some(data) = EMBEDDED_FILES.iter().find(|e| Path::new(e.0) == subpath) {
                Ok(Box::new(Cursor::new(data.1)))
            } else {
                Err(e)
            }
        }
    }
}

/// Read a file, honouring search paths established during startup, and
/// falling back to potentially embedded file content
pub fn read_as_string<P: AsRef<Path>>(subpath: P) -> io::Result<String> {
    let mut f = open(subpath)?;
    let mut ret = String::with_capacity(8192);
    f.read_to_string(&mut ret)?;
    Ok(ret)
}