pray_core/
project_context.rs1use 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}