Skip to main content

ch_rds/project/
mod.rs

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