From c2cd84305fef4bddfad1e622d16fe4bb0c62dd10 Mon Sep 17 00:00:00 2001 From: Justin Rubek <25621857+justinrubek@users.noreply.github.com> Date: Sun, 1 Oct 2023 02:56:19 -0500 Subject: [PATCH] feat: split loading functionality into discover_and_assume The `discover_and_assume` function adds an opinionated handling of what to do when a value isn't found. It will append directory names to the data, config, and cache directories as subdirectories of the project root. If the project id can't be determined it will check the config directory for a special file. --- crates/cli/src/main.rs | 4 +- crates/prj-base-directory/src/constants.rs | 17 ++- crates/prj-base-directory/src/error.rs | 5 + crates/prj-base-directory/src/lib.rs | 125 +++++++++++++++------ 4 files changed, 106 insertions(+), 45 deletions(-) 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 -}