use std::env;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow};
use ch_config::environment::{EnvFiles, EnvLoad};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Project {
root: PathBuf,
}
impl Project {
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(¤t_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()
)
})
}
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()
)
})
}
pub fn root(&self) -> &Path {
&self.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);
}
}