Skip to content

Commit

Permalink
fix: fix tests, add api docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Shadow53 committed Aug 20, 2023
1 parent c88aa05 commit 7d6c20d
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 15 deletions.
52 changes: 42 additions & 10 deletions src/config/builder/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! The [`Builder`] struct serves as an intermediate step between raw configuration and the
//! [`Config`] type that is used by `hoard`.
use std::collections::{BTreeMap, HashMap};
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::env;
use std::path::{Path, PathBuf};

use clap::Parser;
Expand All @@ -14,11 +13,10 @@ use tokio::{fs, io};
use environment::Environment;

use crate::command::Command;
use crate::config::builder::var_defaults::EnvVarDefaults;
use crate::config::builder::var_defaults::{EnvVarDefaults, EnvVarDefaultsError};
use crate::hoard::PileConfig;
use crate::newtypes::{EnvironmentName, HoardName};
use crate::CONFIG_FILE_STEM;
use crate::env_vars::StringWithEnv;

use super::Config;

Expand All @@ -27,7 +25,7 @@ use self::hoard::Hoard;
pub mod environment;
pub mod envtrie;
pub mod hoard;
mod var_defaults;
pub mod var_defaults;

const DEFAULT_CONFIG_EXT: &str = "toml";
/// The items are listed in descending order of precedence
Expand Down Expand Up @@ -56,9 +54,12 @@ pub enum Error {
"configuration file does not have file extension \".toml\", \".yaml\", or \".yml\": {0}"
)]
InvalidExtension(PathBuf),
/// Failed to set one or more environment variable default.
#[error(transparent)]
EnvVarDefaults(#[from] EnvVarDefaultsError),
}

/// Intermediate data structure to build a [`Config`](crate::config::Config).
/// Intermediate data structure to build a [`Config`].
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Parser)]
#[clap(author, version, about, long_about = None, rename_all = "kebab")]
#[serde(rename_all = "snake_case")]
Expand All @@ -68,6 +69,7 @@ pub struct Builder {
#[serde(rename = "envs")]
environments: Option<BTreeMap<EnvironmentName, Environment>>,
#[clap(skip)]
#[serde(default)]
var_defaults: EnvVarDefaults,
#[clap(skip)]
exclusivity: Option<Vec<Vec<EnvironmentName>>>,
Expand Down Expand Up @@ -441,6 +443,9 @@ mod tests {
mod builder {
use super::*;

const DEFAULT_VAR: &str = "UNSET";
const DEFAULT_VAR_VALUE: &str = "no longer unset";

fn get_default_populated_builder() -> Builder {
Builder {
config_file: Some(Builder::default_config_file()),
Expand All @@ -452,6 +457,7 @@ mod tests {
hoards: None,
force: false,
global_config: None,
var_defaults: EnvVarDefaults::default(),
}
}

Expand All @@ -468,6 +474,11 @@ mod tests {
hoards: None,
force: false,
global_config: None,
var_defaults: {
let mut defaults = EnvVarDefaults::default();
defaults.insert(DEFAULT_VAR.into(), DEFAULT_VAR_VALUE.into());
defaults
}
}
}

Expand All @@ -488,6 +499,7 @@ mod tests {
exclusivity: None,
force: false,
global_config: None,
var_defaults: EnvVarDefaults::default(),
};

assert_eq!(
Expand Down Expand Up @@ -518,16 +530,28 @@ mod tests {

#[test]
fn layered_builder_prefers_argument_to_self() {
let layer1 = get_default_populated_builder();
let layer2 = get_non_default_populated_builder();
let mut layer1 = get_default_populated_builder();
let mut layer2 = get_non_default_populated_builder();

// Add extra env values to ensure they merge properly
layer1.var_defaults.insert("ENV1".into(), "1".into());
layer1.var_defaults.insert(DEFAULT_VAR.into(), "non default".into());
layer2.var_defaults.insert("ENV2".into(), "2".into());

let mut expected = layer2.clone();
expected.var_defaults.insert("ENV1".into(), "1".into());

assert_eq!(
layer2,
expected,
layer1.clone().layer(layer2.clone()),
"layer() should prefer the argument"
);

let mut expected = layer1.clone();
expected.var_defaults.insert("ENV2".into(), "2".into());

assert_eq!(
layer1,
expected,
layer2.layer(layer1.clone()),
"layer() should prefer the argument"
);
Expand Down Expand Up @@ -577,5 +601,13 @@ mod tests {
assert_eq!(Some(config.config_file), builder.config_file);
assert_eq!(Some(config.command), builder.command);
}

#[test]
#[serial_test::serial]

Check failure on line 606 in src/config/builder/mod.rs

View workflow job for this annotation

GitHub Actions / Check Commit (macos-latest)

failed to resolve: use of undeclared crate or module `serial_test`
fn builder_sets_env_vars_correctly() {
let builder = get_non_default_populated_builder();
builder.build().unwrap();
assert_eq!(std::env::var(DEFAULT_VAR).unwrap(), DEFAULT_VAR_VALUE);
}
}
}
43 changes: 38 additions & 5 deletions src/config/builder/var_defaults.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
//! See [`EnvVarDefaults`].

use std::collections::BTreeMap;
use std::env;
use std::{env, fmt};
use std::fmt::Formatter;
use serde::{Deserialize, Serialize};
use crate::env_vars::StringWithEnv;

/// Failed to apply one or more environment variables in [`EnvVarDefaults::apply`].
///
/// Most common reasons for this occurring is trying to use an unset variable in a default value,
/// or having two unset variables' values dependent on each other.
#[derive(Debug, thiserror::Error)]
pub struct EnvVarDefaultsError(BTreeMap<String, String>);

impl fmt::Display for EnvVarDefaultsError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
assert!(!self.0.is_empty());
write!(f, "Could not apply environment variable defaults. One or more default values requires an unset variable.")?;
for (var, value) in &self.0 {
write!(f, "\n{var}: {value:?}")?;
}
Ok(())
}
}

/// Define variables and their default values, should that variable not otherwise be defined.
///
/// Variable default values can interpolate the values of other environment variables.
/// See [`StringWithEnv`] for the required syntax.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(transparent)]
#[repr(transparent)]
pub struct EnvVarDefaults(BTreeMap<String, String>);

Check failure on line 34 in src/config/builder/var_defaults.rs

View workflow job for this annotation

GitHub Actions / clippy

item name ends with its containing module's name

error: item name ends with its containing module's name --> src/config/builder/var_defaults.rs:34:12 | 34 | pub struct EnvVarDefaults(BTreeMap<String, String>); | ^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#module_name_repetitions note: the lint level is defined here --> src/lib.rs:24:9 | 24 | #![deny(clippy::pedantic)] | ^^^^^^^^^^^^^^^^ = note: `#[deny(clippy::module_name_repetitions)]` implied by `#[deny(clippy::pedantic)]`

impl EnvVarDefaults {
#[cfg(test)]
pub(super) fn insert(&mut self, var: String, value: String) -> Option<String> {
self.0.insert(var, value)
}

pub(super) fn merge_with(&mut self, other: Self) {
for (var, value) in other.0 {
self.0.insert(var, value);
Expand All @@ -19,14 +49,17 @@ impl EnvVarDefaults {
/// values first.
///
/// This will repeatedly attempt to apply variables as long as at least one successfully applies.
pub(super) fn apply(self) -> Result<(), ()> {
///
/// # Errors
///
/// See [`EnvVarDefaultsError`].
pub(super) fn apply(self) -> Result<(), EnvVarDefaultsError> {
// Add one just so the `while` condition is true once.
let mut remaining_last_loop = self.0.len() + 1;
let mut last_loop = BTreeMap::new();
let mut this_loop = self.0;

while remaining_last_loop != this_loop.len() {
last_loop = std::mem::take(&mut this_loop);
let last_loop = std::mem::take(&mut this_loop);
remaining_last_loop = last_loop.len();
for (var, value) in last_loop {
if env::var_os(&var).is_none() {
Expand All @@ -45,7 +78,7 @@ impl EnvVarDefaults {
if this_loop.is_empty() {
Ok(())
} else {
Err(())
Err(EnvVarDefaultsError(this_loop))
}
}
}

0 comments on commit 7d6c20d

Please sign in to comment.