Skip to main content

ch_rds/
project.rs

1use std::env;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result, anyhow};
5use ch_config::environment::{EnvFiles, EnvLoad};
6
7/// Filesystem layout discovered for the running application.
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct Project {
10    root: PathBuf,
11}
12
13impl Project {
14    /// Discovers the application root from the working directory or executable.
15    pub fn discover() -> Result<Self> {
16        let current_dir =
17            env::current_dir().context("failed to determine the working directory")?;
18        if let Some(project) = Self::find_from(&current_dir) {
19            return Ok(project);
20        }
21
22        let executable = env::current_exe().context("failed to determine the executable path")?;
23        Self::find_from(&executable).ok_or_else(|| {
24            anyhow!(
25                "failed to discover the application root from {} or {}",
26                current_dir.display(),
27                executable.display()
28            )
29        })
30    }
31
32    /// Discovers an application root from a caller-provided path.
33    pub fn discover_from(start: impl AsRef<Path>) -> Result<Self> {
34        Self::find_from(start.as_ref()).ok_or_else(|| {
35            anyhow!(
36                "failed to discover an application root from {}",
37                start.as_ref().display()
38            )
39        })
40    }
41
42    /// Returns the application root.
43    pub fn root(&self) -> &Path {
44        &self.root
45    }
46
47    /// Resolves a path relative to the application root.
48    pub fn path(&self, path: impl AsRef<Path>) -> PathBuf {
49        self.root.join(path)
50    }
51
52    pub(crate) fn load_environment(&self) -> Result<EnvLoad> {
53        let selected = env::var_os("ENV_FILE")
54            .filter(|path| !path.is_empty())
55            .map(PathBuf::from)
56            .map(|path| {
57                if path.is_absolute() {
58                    path
59                } else {
60                    self.path(path)
61                }
62            });
63        let files = match selected {
64            Some(path) => EnvFiles {
65                base: Some(path),
66                override_file: None,
67            },
68            None => EnvFiles {
69                base: Some(self.path(".env")),
70                override_file: Some(self.path(".env.local")),
71            },
72        };
73
74        files.load().map_err(Into::into)
75    }
76
77    fn find_from(start: &Path) -> Option<Self> {
78        let start = discovery_root(start);
79        let ancestors = start.ancestors().collect::<Vec<_>>();
80        let root = ancestors
81            .iter()
82            .copied()
83            .find(|candidate| is_application_root(candidate))
84            .or_else(|| {
85                ancestors
86                    .iter()
87                    .copied()
88                    .find(|candidate| candidate.join("Cargo.toml").is_file())
89            })?;
90
91        Some(Self {
92            root: root.to_path_buf(),
93        })
94    }
95}
96
97pub(crate) fn discovery_root(start: &Path) -> &Path {
98    match (start.is_file(), start.parent()) {
99        (true, Some(parent)) => parent,
100        _ => start,
101    }
102}
103
104fn is_application_root(path: &Path) -> bool {
105    path.join("web").is_dir()
106        || path.join("services.env").is_file()
107        || (path.join("Cargo.toml").is_file() && path.join("app").is_dir())
108}
109
110#[cfg(test)]
111mod tests {
112    use std::fs;
113
114    use tempfile::tempdir;
115
116    use super::*;
117
118    #[test]
119    fn discovers_the_project_above_an_app_source_directory() {
120        let temporary = tempdir().expect("temporary directory should be created");
121        let root = temporary.path().join("example");
122        let source = root.join("app/src");
123        fs::create_dir_all(root.join("web")).expect("web directory should be created");
124        fs::create_dir_all(&source).expect("source directory should be created");
125        fs::write(root.join("Cargo.toml"), "[workspace]\n")
126            .expect("workspace manifest should be written");
127        fs::write(root.join("app/Cargo.toml"), "[package]\nname='example'\n")
128            .expect("app manifest should be written");
129
130        let project = Project::discover_from(&source).expect("project should be discovered");
131
132        assert_eq!(project.root(), root);
133        assert_eq!(
134            project.path("web/build/client"),
135            root.join("web/build/client")
136        );
137    }
138
139    #[test]
140    fn discovers_a_packaged_runtime_from_its_web_directory() {
141        let temporary = tempdir().expect("temporary directory should be created");
142        let root = temporary.path().join("runtime");
143        fs::create_dir_all(root.join("web/build/client"))
144            .expect("runtime client directory should be created");
145
146        let project = Project::discover_from(root.join("server"))
147            .expect("runtime project should be discovered");
148
149        assert_eq!(project.root(), root);
150    }
151}