Skip to content

Commit

Permalink
feat: split loading functionality into discover_and_assume
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
justinrubek committed Oct 1, 2023
1 parent 452cb89 commit c2cd843
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 45 deletions.
4 changes: 2 additions & 2 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ async fn main() -> std::result::Result<(), Box<dyn Error>> {
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()?;

Expand Down
17 changes: 11 additions & 6 deletions crates/prj-base-directory/src/constants.rs
Original file line number Diff line number Diff line change
@@ -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";
5 changes: 5 additions & 0 deletions crates/prj-base-directory/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = std::result::Result<T, Error>;
125 changes: 88 additions & 37 deletions crates/prj-base-directory/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf>,
/// 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 async fn discover() -> Result<Self> {
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<Self> {
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<Option<PathBuf>> {
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));
}
Expand All @@ -43,38 +122,10 @@ pub async fn get_project_root() -> Result<Option<PathBuf>> {
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<String> {
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
}

0 comments on commit c2cd843

Please sign in to comment.