1use std::env;
2use std::path::{Path, PathBuf};
3
4use crate::RuntimeError;
5use ch_config::environment::{EnvFiles, EnvLoad};
6
7#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct Project {
10 root: PathBuf,
11}
12
13impl Project {
14 pub fn discover() -> Result<Self, RuntimeError> {
16 let current_dir = env::current_dir()?;
17 if let Some(project) = Self::find_from(¤t_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 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 pub fn root(&self) -> &Path {
33 &self.root
34 }
35
36 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}