diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index ee86460..a820f4a 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -15,14 +15,14 @@ async fn main() -> std::result::Result<(), Box> { let args = commands::Args::parse(); match args.command { Commands::Project(project_cmd) => { - let project = Project::discover().await?; + let project = Project::discover_and_assume().await?; match project_cmd.command { commands::ProjectCommands::Exec(exec) => { debug!(?project, ?exec, "Running command in project"); let mut process = tokio::process::Command::new(exec.command) - .current_dir(&project.project_root.unwrap()) + .current_dir(&project.root_directory.unwrap()) .args(exec.args) .spawn()?; diff --git a/crates/prj-base-directory/src/constants.rs b/crates/prj-base-directory/src/constants.rs index bcb760e..34c31d7 100644 --- a/crates/prj-base-directory/src/constants.rs +++ b/crates/prj-base-directory/src/constants.rs @@ -1,7 +1,12 @@ -const PRJ_ROOT: &str = "PRJ_ROOT"; -const PRJ_CONFIG_HOME: &str = "PRJ_CONFIG_HOME"; -const PRJ_DATA_HOME: &str = "PRJ_DATA_HOME"; -const PRJ_ID: &str = "PRJ_ID"; -const PRJ_CACHE: &str = "PRJ_CACHE"; +pub const PROJECT_ROOT: &str = "PRJ_ROOT"; +pub const PROJECT_CONFIG_HOME: &str = "PRJ_CONFIG_HOME"; +pub const PROJECT_DATA_HOME: &str = "PRJ_DATA_HOME"; +pub const PROJECT_ID: &str = "PRJ_ID"; +pub const PROJECT_CACHE: &str = "PRJ_CACHE"; -const PRJ_ID_FILE: &str = "prj_id"; +pub const PROJECT_ID_FILE: &str = "prj_id"; + +// default values for directories that are not set +pub const DEFAULT_CONFIG_HOME: &str = ".config"; +pub const DEFAULT_DATA_HOME: &str = ".data"; +pub const DEFAULT_CACHE_HOME: &str = ".cache"; diff --git a/crates/prj-base-directory/src/error.rs b/crates/prj-base-directory/src/error.rs index b21ecb0..811f7ff 100644 --- a/crates/prj-base-directory/src/error.rs +++ b/crates/prj-base-directory/src/error.rs @@ -4,6 +4,11 @@ pub enum Error { GixDiscoveryError(#[from] gix::discover::Error), #[error(transparent)] StdIo(#[from] std::io::Error), + #[error(transparent)] + StdEnv(#[from] std::env::VarError), + + #[error("failed to find project root directory in search from {0}")] + ProjectRootNotFound(std::path::PathBuf), } pub type Result = std::result::Result; diff --git a/crates/prj-base-directory/src/lib.rs b/crates/prj-base-directory/src/lib.rs index 448507e..3b5e4ee 100644 --- a/crates/prj-base-directory/src/lib.rs +++ b/crates/prj-base-directory/src/lib.rs @@ -1,39 +1,118 @@ -use crate::error::Result; +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(Debug, Deserialize, Serialize)] +#[derive(Default, Debug, Deserialize, Serialize)] pub struct Project { - pub project_root: Option, + /// The absolute path to the project root directory. + /// This is the top-level directory of the project. + pub root_directory: Option, + /// A unique identifier for the project. pub project_id: Option, + /// The directory for storing project specific configuration. + pub config_home: Option, + /// The directory for storing project specific cache data. + pub cache_home: Option, + /// The directory for storing project specific data files. + pub data_home: Option, } impl Project { /// Retrieve the project information detected from current directory. pub async fn discover() -> Result { let project_root = get_project_root().await?; - let project_id = get_project_id().await; + 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 { - project_root, + 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 { + let mut value = Self::discover().await?; + + // 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) + } } /// 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 technies: /// - Searching upwards for a git repository -#[tracing::instrument] pub async fn get_project_root() -> Result> { - let project_root = std::env::var("PRJ_ROOT").ok(); + let project_root = std::env::var(constants::PROJECT_ROOT).ok(); if let Some(project_root) = project_root { - debug!("Using PRJ_ROOT environment variable as project root"); + debug!( + "using {} environment variable as project root", + constants::PROJECT_ROOT + ); let path = PathBuf::from(project_root); return Ok(Some(path)); } @@ -43,38 +122,10 @@ pub async fn get_project_root() -> Result> { 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"); + debug!(?directory, "using git repository as project root"); return Ok(Some(directory.to_owned())); } } Ok(None) } - -/// The project id is an optional unique identifier for a project. -/// Specification -/// -/// The PRJ_ID value MUST pass the following regular expression: ^[a-zA-Z0-9_-]{,32}$. It can be a UUIDv4 or some other random identifier. -/// If the environment variable $PRJ_ID is set, it MUST be used as the project id. -/// Otherwise, if the PRJ_CONFIG_HOME is set and a prj_id file exists, it will be loaded after stripping any trailing white spaces. -/// Otherwise, the tool is free to pick its own logic. -pub async fn get_project_id() -> Option { - let project_id = std::env::var("PRJ_ID").ok(); - if project_id.is_some() { - return project_id; - } - - let config_home = std::env::var("PRJ_CONFIG_HOME").ok(); - if config_home.is_some() { - let mut path = std::path::PathBuf::from(config_home.unwrap()); - path.push("prj_id"); - if path.exists() { - let mut file = tokio::fs::File::open(path).await.unwrap(); - let mut contents = String::new(); - file.read_to_string(&mut contents).await.unwrap(); - return Some(contents.trim().to_string()); - } - } - - None -}