ch-rds 0.1.12

Runtime resource loading and local service orchestration
Documentation
use std::env;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, anyhow};
use ch_config::environment::{EnvFiles, EnvLoad};

/// Filesystem layout discovered for the running application.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Project {
    root: PathBuf,
}

impl Project {
    /// Discovers the application root from the working directory or executable.
    pub fn discover() -> Result<Self> {
        let current_dir =
            env::current_dir().context("failed to determine the working directory")?;
        if let Some(project) = Self::find_from(&current_dir) {
            return Ok(project);
        }

        let executable = env::current_exe().context("failed to determine the executable path")?;
        Self::find_from(&executable).ok_or_else(|| {
            anyhow!(
                "failed to discover the application root from {} or {}",
                current_dir.display(),
                executable.display()
            )
        })
    }

    /// Discovers an application root from a caller-provided path.
    pub fn discover_from(start: impl AsRef<Path>) -> Result<Self> {
        Self::find_from(start.as_ref()).ok_or_else(|| {
            anyhow!(
                "failed to discover an application root from {}",
                start.as_ref().display()
            )
        })
    }

    /// Returns the application root.
    pub fn root(&self) -> &Path {
        &self.root
    }

    /// Resolves a path relative to the application root.
    pub fn path(&self, path: impl AsRef<Path>) -> PathBuf {
        self.root.join(path)
    }

    pub(crate) fn load_environment(&self) -> Result<EnvLoad> {
        let selected = env::var_os("ENV_FILE")
            .filter(|path| !path.is_empty())
            .map(PathBuf::from)
            .map(|path| {
                if path.is_absolute() {
                    path
                } else {
                    self.path(path)
                }
            });
        let files = match selected {
            Some(path) => EnvFiles {
                base: Some(path),
                override_file: None,
            },
            None => EnvFiles {
                base: Some(self.path(".env")),
                override_file: Some(self.path(".env.local")),
            },
        };

        files.load().map_err(Into::into)
    }

    fn find_from(start: &Path) -> Option<Self> {
        let start = discovery_root(start);
        let ancestors = start.ancestors().collect::<Vec<_>>();
        let root = ancestors
            .iter()
            .copied()
            .find(|candidate| is_application_root(candidate))
            .or_else(|| {
                ancestors
                    .iter()
                    .copied()
                    .find(|candidate| candidate.join("Cargo.toml").is_file())
            })?;

        Some(Self {
            root: root.to_path_buf(),
        })
    }
}

pub(crate) fn discovery_root(start: &Path) -> &Path {
    match (start.is_file(), start.parent()) {
        (true, Some(parent)) => parent,
        _ => start,
    }
}

fn is_application_root(path: &Path) -> bool {
    path.join("web").is_dir()
        || path.join("services.env").is_file()
        || (path.join("Cargo.toml").is_file() && path.join("app").is_dir())
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::*;

    #[test]
    fn discovers_the_project_above_an_app_source_directory() {
        let temporary = tempdir().expect("temporary directory should be created");
        let root = temporary.path().join("example");
        let source = root.join("app/src");
        fs::create_dir_all(root.join("web")).expect("web directory should be created");
        fs::create_dir_all(&source).expect("source directory should be created");
        fs::write(root.join("Cargo.toml"), "[workspace]\n")
            .expect("workspace manifest should be written");
        fs::write(root.join("app/Cargo.toml"), "[package]\nname='example'\n")
            .expect("app manifest should be written");

        let project = Project::discover_from(&source).expect("project should be discovered");

        assert_eq!(project.root(), root);
        assert_eq!(
            project.path("web/build/client"),
            root.join("web/build/client")
        );
    }

    #[test]
    fn discovers_a_packaged_runtime_from_its_web_directory() {
        let temporary = tempdir().expect("temporary directory should be created");
        let root = temporary.path().join("runtime");
        fs::create_dir_all(root.join("web/build/client"))
            .expect("runtime client directory should be created");

        let project = Project::discover_from(root.join("server"))
            .expect("runtime project should be discovered");

        assert_eq!(project.root(), root);
    }
}