project_base_directory/
lib.rs

1#![allow(clippy::result_large_err)]
2use crate::error::{Error, Result};
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use tokio::io::AsyncReadExt;
6use tracing::debug;
7
8pub mod constants;
9pub mod error;
10
11#[derive(Default, Debug, Deserialize, Serialize)]
12pub struct Project {
13    /// The absolute path to the project root directory.
14    /// This is the top-level directory of the project.
15    pub root_directory: Option<PathBuf>,
16    /// A unique identifier for the project.
17    pub project_id: Option<String>,
18    /// The directory for storing project specific configuration.
19    pub config_home: Option<PathBuf>,
20    /// The directory for storing project specific cache data.
21    pub cache_home: Option<PathBuf>,
22    /// The directory for storing project specific data files.
23    pub data_home: Option<PathBuf>,
24}
25
26impl Project {
27    /// Retrieve the project information detected from current directory.
28    pub fn discover() -> Result<Self> {
29        let project_root = get_project_root()?;
30        let project_data = std::env::var(constants::PROJECT_DATA_HOME)
31            .map(PathBuf::from)
32            .ok();
33        let project_config = std::env::var(constants::PROJECT_CONFIG_HOME)
34            .map(PathBuf::from)
35            .ok();
36        let project_cache = std::env::var(constants::PROJECT_CACHE)
37            .map(PathBuf::from)
38            .ok();
39        let project_id = std::env::var(constants::PROJECT_ID).ok();
40
41        Ok(Self {
42            root_directory: project_root,
43            project_id,
44            data_home: project_data,
45            config_home: project_config,
46            cache_home: project_cache,
47        })
48    }
49
50    /// Retrieve the project information detected from the given directory.
51    /// If a property is not set, then an opinionated default is used.
52    pub async fn discover_and_assume() -> Result<Self> {
53        let mut value = Self::discover()?;
54        // If the project root is not found, give up.
55        match value.root_directory {
56            Some(_) => {}
57            None => return Err(Error::ProjectRootNotFound(std::env::current_dir().unwrap())),
58        }
59
60        match value.config_home {
61            Some(_) => {}
62            None => {
63                let mut directory = value.root_directory.clone().unwrap();
64                directory.push(constants::DEFAULT_CONFIG_HOME);
65                value.config_home = Some(directory);
66            }
67        }
68
69        match value.data_home {
70            Some(_) => {}
71            None => {
72                let mut directory = value.root_directory.clone().unwrap();
73                directory.push(constants::DEFAULT_DATA_HOME);
74                value.data_home = Some(directory);
75            }
76        }
77
78        match value.cache_home {
79            Some(_) => {}
80            None => {
81                let mut directory = value.root_directory.clone().unwrap();
82                directory.push(constants::DEFAULT_CACHE_HOME);
83                value.cache_home = Some(directory);
84            }
85        }
86
87        match value.project_id {
88            Some(_) => {}
89            None => {
90                let mut file = value.config_home.clone().unwrap();
91                file.push(constants::PROJECT_ID_FILE);
92                if file.exists() {
93                    let mut file = tokio::fs::File::open(file).await.unwrap();
94                    let mut contents = String::new();
95                    file.read_to_string(&mut contents).await.unwrap();
96                    value.project_id = Some(contents.trim().to_string());
97                }
98            }
99        }
100
101        Ok(value)
102    }
103
104    /// Retrieve the project information as a HashMap composed of the environment variables as keys
105    /// and their values as values.
106    pub fn project_hashmap(&self) -> std::collections::HashMap<String, Option<String>> {
107        let mut hashmap = std::collections::HashMap::new();
108
109        hashmap.insert(
110            constants::PROJECT_ROOT.to_string(),
111            self.root_directory
112                .as_ref()
113                .map(|p| p.to_str().unwrap().to_string()),
114        );
115        hashmap.insert(
116            constants::PROJECT_DATA_HOME.to_string(),
117            self.data_home
118                .as_ref()
119                .map(|p| p.to_str().unwrap().to_string()),
120        );
121        hashmap.insert(
122            constants::PROJECT_CONFIG_HOME.to_string(),
123            self.config_home
124                .as_ref()
125                .map(|p| p.to_str().unwrap().to_string()),
126        );
127        hashmap.insert(
128            constants::PROJECT_CACHE.to_string(),
129            self.cache_home
130                .as_ref()
131                .map(|p| p.to_str().unwrap().to_string()),
132        );
133        hashmap.insert(
134            constants::PROJECT_ID.to_string(),
135            self.project_id.as_ref().map(|p| p.to_string()),
136        );
137
138        hashmap
139    }
140}
141
142/// An absolute path that points to the project root directory.
143/// If the environment variable $PRJ_ROOT is set its value will be used.
144/// Otherwise, a best effort is made to find the project root using the following techniques:
145/// - Searching upwards for a git repository
146pub fn get_project_root() -> Result<Option<PathBuf>> {
147    let project_root = std::env::var(constants::PROJECT_ROOT).ok();
148    if let Some(project_root) = project_root {
149        debug!(
150            "using {} environment variable as project root",
151            constants::PROJECT_ROOT
152        );
153        let path = PathBuf::from(project_root);
154        return Ok(Some(path));
155    }
156
157    #[cfg(feature = "git")]
158    {
159        let current_dir = std::env::current_dir().unwrap();
160        let git_repository = gix::discover(current_dir)?;
161        if let Some(directory) = git_repository.work_dir() {
162            debug!(?directory, "using git repository as project root");
163            return Ok(Some(directory.to_owned()));
164        }
165    }
166
167    Ok(None)
168}