Skip to main content

pray_core/
project_context.rs

1use crate::dotenv::load_dotenv_variables;
2use crate::{PrayError, PrayResult};
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7const ENV_PROJECT_PATH: &str = "PRAY_PATH";
8const ENV_MANIFEST_PATH: &str = "PRAY_FILE_PATH";
9const ENV_ENVIRONMENT: &str = "PRAY_ENV";
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ProjectInvocationContext {
13    pub project_root: PathBuf,
14    pub manifest_path: PathBuf,
15    pub environment: Option<String>,
16}
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
19pub struct ProjectInvocationOptions {
20    pub project_root: Option<PathBuf>,
21    pub manifest_path: Option<PathBuf>,
22    pub environment: Option<String>,
23}
24
25impl ProjectInvocationContext {
26    pub fn from_current_directory() -> PrayResult<Self> {
27        Self::from_options(ProjectInvocationOptions::default())
28    }
29
30    pub fn from_options(options: ProjectInvocationOptions) -> PrayResult<Self> {
31        let cwd = env::current_dir().map_err(PrayError::from)?;
32        let dotenv = load_dotenv_variables(&cwd);
33        let project_root_hint = options
34            .project_root
35            .clone()
36            .or_else(|| env_value(ENV_PROJECT_PATH).map(PathBuf::from))
37            .or_else(|| dotenv.get(ENV_PROJECT_PATH).cloned().map(PathBuf::from))
38            .unwrap_or_else(|| cwd.clone());
39        let project_root = canonicalize_path(&cwd, &project_root_hint)?;
40        let manifest_hint = options
41            .manifest_path
42            .clone()
43            .or_else(|| env_value(ENV_MANIFEST_PATH).map(PathBuf::from))
44            .or_else(|| dotenv.get(ENV_MANIFEST_PATH).cloned().map(PathBuf::from))
45            .unwrap_or_else(|| PathBuf::from("Prayfile"));
46        let manifest_path = if manifest_hint.is_absolute() {
47            manifest_hint
48        } else {
49            project_root.join(manifest_hint)
50        };
51        let environment = options
52            .environment
53            .or_else(|| env_value(ENV_ENVIRONMENT))
54            .or_else(|| dotenv.get(ENV_ENVIRONMENT).cloned())
55            .map(|value| value.trim().to_string())
56            .filter(|value| !value.is_empty());
57        Ok(Self {
58            project_root,
59            manifest_path,
60            environment,
61        })
62    }
63
64    pub fn lockfile_path(&self) -> PathBuf {
65        self.project_root.join("Prayfile.lock")
66    }
67}
68
69fn env_value(key: &str) -> Option<String> {
70    match env::var(key) {
71        Ok(value) if !value.trim().is_empty() => Some(value),
72        _ => None,
73    }
74}
75
76fn canonicalize_path(base: &Path, path: &Path) -> PrayResult<PathBuf> {
77    let resolved = if path.is_absolute() {
78        path.to_path_buf()
79    } else {
80        base.join(path)
81    };
82    fs::canonicalize(&resolved).or(Ok(resolved))
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use std::fs;
89    use std::sync::{Mutex, OnceLock};
90    use std::time::{SystemTime, UNIX_EPOCH};
91
92    fn env_lock() -> &'static Mutex<()> {
93        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
94        LOCK.get_or_init(|| Mutex::new(()))
95    }
96
97    #[test]
98    fn cli_options_override_process_environment() {
99        let _guard = env_lock().lock().expect("env lock");
100        let stamp = SystemTime::now()
101            .duration_since(UNIX_EPOCH)
102            .expect("clock")
103            .as_nanos();
104        let temp = env::temp_dir().join(format!("pray-context-test-{stamp}"));
105        let project_root = temp.join("project");
106        fs::create_dir_all(&project_root).expect("project dir");
107        fs::write(project_root.join("Prayfile"), "prayfile \"1\"\n").expect("prayfile");
108        env::set_current_dir(&temp).expect("chdir");
109        env::set_var(ENV_PROJECT_PATH, "ignored");
110        env::set_var(ENV_ENVIRONMENT, "ignored");
111
112        let context = ProjectInvocationContext::from_options(ProjectInvocationOptions {
113            project_root: Some(project_root.clone()),
114            manifest_path: None,
115            environment: Some("development".to_string()),
116        })
117        .expect("context");
118
119        let expected_root = fs::canonicalize(&project_root).expect("canonical project root");
120        assert_eq!(context.project_root, expected_root);
121        assert_eq!(
122            fs::canonicalize(&context.manifest_path).expect("canonical manifest path"),
123            fs::canonicalize(project_root.join("Prayfile")).expect("canonical expected manifest")
124        );
125        assert_eq!(context.environment.as_deref(), Some("development"));
126
127        env::remove_var(ENV_PROJECT_PATH);
128        env::remove_var(ENV_ENVIRONMENT);
129        let _ = env::set_current_dir(env::temp_dir());
130        let _ = fs::remove_dir_all(&temp);
131    }
132}