1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#![allow(clippy::result_large_err)]
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tokio::io::AsyncReadExt;
use tracing::debug;

pub mod constants;
pub mod error;

#[derive(Default, Debug, Deserialize, Serialize)]
pub struct Project {
    /// The absolute path to the project root directory.
    /// This is the top-level directory of the project.
    pub root_directory: Option<PathBuf>,
    /// A unique identifier for the project.
    pub project_id: Option<String>,
    /// The directory for storing project specific configuration.
    pub config_home: Option<PathBuf>,
    /// The directory for storing project specific cache data.
    pub cache_home: Option<PathBuf>,
    /// The directory for storing project specific data files.
    pub data_home: Option<PathBuf>,
}

impl Project {
    /// Retrieve the project information detected from current directory.
    pub fn discover() -> Result<Self> {
        let project_root = get_project_root()?;
        let project_data = std::env::var(constants::PROJECT_DATA_HOME)
            .map(PathBuf::from)
            .ok();
        let project_config = std::env::var(constants::PROJECT_CONFIG_HOME)
            .map(PathBuf::from)
            .ok();
        let project_cache = std::env::var(constants::PROJECT_CACHE)
            .map(PathBuf::from)
            .ok();
        let project_id = std::env::var(constants::PROJECT_ID).ok();

        Ok(Self {
            root_directory: project_root,
            project_id,
            data_home: project_data,
            config_home: project_config,
            cache_home: project_cache,
        })
    }

    /// Retrieve the project information detected from the given directory.
    /// If a property is not set, then an opinionated default is used.
    pub async fn discover_and_assume() -> Result<Self> {
        let mut value = Self::discover()?;
        // If the project root is not found, give up.
        match value.root_directory {
            Some(_) => {}
            None => return Err(Error::ProjectRootNotFound(std::env::current_dir().unwrap())),
        }

        match value.config_home {
            Some(_) => {}
            None => {
                let mut directory = value.root_directory.clone().unwrap();
                directory.push(constants::DEFAULT_CONFIG_HOME);
                value.config_home = Some(directory);
            }
        }

        match value.data_home {
            Some(_) => {}
            None => {
                let mut directory = value.root_directory.clone().unwrap();
                directory.push(constants::DEFAULT_DATA_HOME);
                value.data_home = Some(directory);
            }
        }

        match value.cache_home {
            Some(_) => {}
            None => {
                let mut directory = value.root_directory.clone().unwrap();
                directory.push(constants::DEFAULT_CACHE_HOME);
                value.cache_home = Some(directory);
            }
        }

        match value.project_id {
            Some(_) => {}
            None => {
                let mut file = value.config_home.clone().unwrap();
                file.push(constants::PROJECT_ID_FILE);
                if file.exists() {
                    let mut file = tokio::fs::File::open(file).await.unwrap();
                    let mut contents = String::new();
                    file.read_to_string(&mut contents).await.unwrap();
                    value.project_id = Some(contents.trim().to_string());
                }
            }
        }

        Ok(value)
    }

    /// Retrieve the project information as a HashMap composed of the environment variables as keys
    /// and their values as values.
    pub fn project_hashmap(&self) -> std::collections::HashMap<String, Option<String>> {
        let mut hashmap = std::collections::HashMap::new();

        hashmap.insert(
            constants::PROJECT_ROOT.to_string(),
            self.root_directory
                .as_ref()
                .map(|p| p.to_str().unwrap().to_string()),
        );
        hashmap.insert(
            constants::PROJECT_DATA_HOME.to_string(),
            self.data_home
                .as_ref()
                .map(|p| p.to_str().unwrap().to_string()),
        );
        hashmap.insert(
            constants::PROJECT_CONFIG_HOME.to_string(),
            self.config_home
                .as_ref()
                .map(|p| p.to_str().unwrap().to_string()),
        );
        hashmap.insert(
            constants::PROJECT_CACHE.to_string(),
            self.cache_home
                .as_ref()
                .map(|p| p.to_str().unwrap().to_string()),
        );
        hashmap.insert(
            constants::PROJECT_ID.to_string(),
            self.project_id.as_ref().map(|p| p.to_string()),
        );

        hashmap
    }
}

/// An absolute path that points to the project root directory.
/// If the environment variable $PRJ_ROOT is set its value will be used.
/// Otherwise, a best effort is made to find the project root using the following techniques:
/// - Searching upwards for a git repository
pub fn get_project_root() -> Result<Option<PathBuf>> {
    let project_root = std::env::var(constants::PROJECT_ROOT).ok();
    if let Some(project_root) = project_root {
        debug!(
            "using {} environment variable as project root",
            constants::PROJECT_ROOT
        );
        let path = PathBuf::from(project_root);
        return Ok(Some(path));
    }

    #[cfg(feature = "git")]
    {
        let current_dir = std::env::current_dir().unwrap();
        let git_repository = gix::discover(current_dir)?;
        if let Some(directory) = git_repository.work_dir() {
            debug!(?directory, "using git repository as project root");
            return Ok(Some(directory.to_owned()));
        }
    }

    Ok(None)
}