pray-core 1.6.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use crate::dotenv::load_dotenv_variables;
use crate::{PrayError, PrayResult};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

const ENV_PROJECT_PATH: &str = "PRAY_PATH";
const ENV_MANIFEST_PATH: &str = "PRAY_FILE_PATH";
const ENV_ENVIRONMENT: &str = "PRAY_ENV";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectInvocationContext {
    pub project_root: PathBuf,
    pub manifest_path: PathBuf,
    pub environment: Option<String>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ProjectInvocationOptions {
    pub project_root: Option<PathBuf>,
    pub manifest_path: Option<PathBuf>,
    pub environment: Option<String>,
}

impl ProjectInvocationContext {
    pub fn from_current_directory() -> PrayResult<Self> {
        Self::from_options(ProjectInvocationOptions::default())
    }

    pub fn from_options(options: ProjectInvocationOptions) -> PrayResult<Self> {
        let cwd = env::current_dir().map_err(PrayError::from)?;
        let dotenv = load_dotenv_variables(&cwd);
        let project_root_hint = options
            .project_root
            .clone()
            .or_else(|| env_value(ENV_PROJECT_PATH).map(PathBuf::from))
            .or_else(|| dotenv.get(ENV_PROJECT_PATH).cloned().map(PathBuf::from))
            .unwrap_or_else(|| cwd.clone());
        let project_root = canonicalize_path(&cwd, &project_root_hint)?;
        let manifest_hint = options
            .manifest_path
            .clone()
            .or_else(|| env_value(ENV_MANIFEST_PATH).map(PathBuf::from))
            .or_else(|| dotenv.get(ENV_MANIFEST_PATH).cloned().map(PathBuf::from))
            .unwrap_or_else(|| PathBuf::from("Prayfile"));
        let manifest_path = if manifest_hint.is_absolute() {
            manifest_hint
        } else {
            project_root.join(manifest_hint)
        };
        let environment = options
            .environment
            .or_else(|| env_value(ENV_ENVIRONMENT))
            .or_else(|| dotenv.get(ENV_ENVIRONMENT).cloned())
            .map(|value| value.trim().to_string())
            .filter(|value| !value.is_empty());
        Ok(Self {
            project_root,
            manifest_path,
            environment,
        })
    }

    pub fn lockfile_path(&self) -> PathBuf {
        self.project_root.join("Prayfile.lock")
    }
}

fn env_value(key: &str) -> Option<String> {
    match env::var(key) {
        Ok(value) if !value.trim().is_empty() => Some(value),
        _ => None,
    }
}

fn canonicalize_path(base: &Path, path: &Path) -> PrayResult<PathBuf> {
    let resolved = if path.is_absolute() {
        path.to_path_buf()
    } else {
        base.join(path)
    };
    fs::canonicalize(&resolved).or(Ok(resolved))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::sync::{Mutex, OnceLock};
    use std::time::{SystemTime, UNIX_EPOCH};

    fn env_lock() -> &'static Mutex<()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
    }

    #[test]
    fn cli_options_override_process_environment() {
        let _guard = env_lock().lock().expect("env lock");
        let stamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("clock")
            .as_nanos();
        let temp = env::temp_dir().join(format!("pray-context-test-{stamp}"));
        let project_root = temp.join("project");
        fs::create_dir_all(&project_root).expect("project dir");
        fs::write(project_root.join("Prayfile"), "prayfile \"1\"\n").expect("prayfile");
        env::set_current_dir(&temp).expect("chdir");
        env::set_var(ENV_PROJECT_PATH, "ignored");
        env::set_var(ENV_ENVIRONMENT, "ignored");

        let context = ProjectInvocationContext::from_options(ProjectInvocationOptions {
            project_root: Some(project_root.clone()),
            manifest_path: None,
            environment: Some("development".to_string()),
        })
        .expect("context");

        let expected_root = fs::canonicalize(&project_root).expect("canonical project root");
        assert_eq!(context.project_root, expected_root);
        assert_eq!(
            fs::canonicalize(&context.manifest_path).expect("canonical manifest path"),
            fs::canonicalize(project_root.join("Prayfile")).expect("canonical expected manifest")
        );
        assert_eq!(context.environment.as_deref(), Some("development"));

        env::remove_var(ENV_PROJECT_PATH);
        env::remove_var(ENV_ENVIRONMENT);
        let _ = env::set_current_dir(env::temp_dir());
        let _ = fs::remove_dir_all(&temp);
    }
}