Skip to content

Commit

Permalink
Merge 9e7c842 into 0efe071
Browse files Browse the repository at this point in the history
  • Loading branch information
rubik committed May 20, 2021
2 parents 0efe071 + 9e7c842 commit bb3acf7
Show file tree
Hide file tree
Showing 15 changed files with 347 additions and 164 deletions.
2 changes: 0 additions & 2 deletions Makefile → Justfile
@@ -1,7 +1,5 @@
.PHONY: test
test:
cargo test -- --test-threads 1

.PHONY: f
f:
rustfmt $(shell find src -name "*.rs" -type f)
90 changes: 54 additions & 36 deletions src/hydro.rs
Expand Up @@ -6,7 +6,8 @@ use dotenv_parser::parse_dotenv;
use serde::Deserialize;

use crate::settings::HydroSettings;
use crate::utils::{config_locations, dotenv_location};
use crate::sources::FileSources;
use crate::utils::path_to_string;

type Table = HashMap<String, Value>;

Expand All @@ -15,6 +16,7 @@ pub struct Hydroconf {
config: Config,
orig_config: Config,
hydro_settings: HydroSettings,
sources: FileSources,
}

impl Default for Hydroconf {
Expand All @@ -29,34 +31,42 @@ impl Hydroconf {
config: Config::default(),
orig_config: Config::default(),
hydro_settings,
sources: FileSources::default(),
}
}

pub fn hydrate<'de, T: Deserialize<'de>>(
mut self,
) -> Result<T, ConfigError> {
self.load_config()?;
self.merge()?;
self.discover_sources();
self.load_settings()?;
self.merge_settings()?;
self.override_from_dotenv()?;
self.override_from_env()?;
self.try_into()
}

pub fn load_config(&mut self) -> Result<&mut Self, ConfigError> {
if let Some(p) = self.root_path() {
let (settings, secrets) = config_locations(p);
if let Some(settings_path) = settings {
self.orig_config.merge(File::from(settings_path))?;
}
if let Some(secrets_path) = secrets {
self.orig_config.merge(File::from(secrets_path))?;
}
pub fn discover_sources(&mut self) {
self.sources = self
.root_path()
.map(|p| {
FileSources::from_root(p, self.hydro_settings.env.as_str())
})
.unwrap_or_else(|| FileSources::default());
}

pub fn load_settings(&mut self) -> Result<&mut Self, ConfigError> {
if let Some(ref settings_path) = self.sources.settings {
self.orig_config.merge(File::from(settings_path.clone()))?;
}
if let Some(ref secrets_path) = self.sources.secrets {
self.orig_config.merge(File::from(secrets_path.clone()))?;
}

Ok(self)
}

pub fn merge(&mut self) -> Result<&mut Self, ConfigError> {
pub fn merge_settings(&mut self) -> Result<&mut Self, ConfigError> {
for &name in &["default", self.hydro_settings.env.as_str()] {
let table_value: Option<Table> = self.orig_config.get(name).ok();
if let Some(value) = table_value {
Expand All @@ -70,25 +80,33 @@ impl Hydroconf {
}

pub fn override_from_dotenv(&mut self) -> Result<&mut Self, ConfigError> {
if let Some(p) = self.root_path() {
if let Some(dotenv_path) = dotenv_location(p) {
let uri =
dotenv_path.clone().into_os_string().into_string().ok();
let source = std::fs::read_to_string(dotenv_path.clone())
.map_err(|e| ConfigError::FileParse {
uri: uri.clone(),
cause: e.into(),
})?;
let map = parse_dotenv(&source).map_err(|e| {
ConfigError::FileParse {
uri,
cause: e.into(),
}
for dotenv_path in &self.sources.dotenv {
let source = std::fs::read_to_string(dotenv_path.clone())
.map_err(|e| ConfigError::FileParse {
uri: path_to_string(dotenv_path.clone()),
cause: e.into(),
})?;
let map =
parse_dotenv(&source).map_err(|e| ConfigError::FileParse {
uri: path_to_string(dotenv_path.clone()),
cause: e.into(),
})?;

for (key, val) in map.iter() {
self.config.set::<String>(key, val.into())?;
for (key, val) in map.iter() {
if val.is_empty() {
continue;
}
let prefix =
self.hydro_settings.envvar_prefix.to_lowercase() + "_";
let mut key = key.to_lowercase();
if !key.starts_with(&prefix) {
continue;
} else {
key = key[prefix.len()..].to_string();
}
let sep = self.hydro_settings.envvar_nested_sep.clone();
key = key.replace(&sep, ".");
self.config.set::<String>(&key, val.into())?;
}
}

Expand Down Expand Up @@ -117,13 +135,13 @@ impl Hydroconf {
self.config.try_into()
}

pub fn refresh(&mut self) -> Result<&mut Self, ConfigError> {
self.orig_config.refresh()?;
self.config.cache = Value::new(None, Table::new());
self.merge()?;
self.override_from_env()?;
Ok(self)
}
//pub fn refresh(&mut self) -> Result<&mut Self, ConfigError> {
//self.orig_config.refresh()?;
//self.config.cache = Value::new(None, Table::new());
//self.merge()?;
//self.override_from_env()?;
//Ok(self)
//}

pub fn set_default<T>(
&mut self,
Expand Down
6 changes: 5 additions & 1 deletion src/lib.rs
Expand Up @@ -33,7 +33,7 @@
//! Then, in your executable source (make sure to add `serde = { version = "1.0",
//! features = ["derive"] }` to your dependencies):
//!
//! ```rust
//! ```no_run
//! use serde::Deserialize;
//! use hydroconf::Hydroconf;
//!
Expand Down Expand Up @@ -145,6 +145,8 @@
//! a `HydroSettings` struct manually and pass it to `Hydroconf`:
//!
//! ```rust
//! # use hydroconf::{Hydroconf, HydroSettings};
//!
//! let hydro_settings = HydroSettings::default()
//! .set_envvar_prefix("MYAPP".into())
//! .set_env("staging".into());
Expand Down Expand Up @@ -225,7 +227,9 @@
mod env;
mod hydro;
mod settings;
mod sources;
mod utils;

pub use hydro::{Config, ConfigError, Environment, File, Hydroconf};
pub use settings::HydroSettings;
pub use sources::FileSources;
176 changes: 176 additions & 0 deletions src/sources.rs
@@ -0,0 +1,176 @@
use std::path::{Path, PathBuf};

const SETTINGS_FILE_EXTENSIONS: &[&str] =
&["toml", "json", "yaml", "ini", "hjson"];
const SETTINGS_DIRS: &[&str] = &["", "config"];

#[derive(Clone, Debug, Default, PartialEq)]
pub struct FileSources {
pub settings: Option<PathBuf>,
pub secrets: Option<PathBuf>,
pub dotenv: Vec<PathBuf>,
}

impl FileSources {
pub fn from_root(root_path: PathBuf, env: &str) -> Self {
let mut sources = Self {
settings: None,
secrets: None,
dotenv: Vec::new(),
};
let mut settings_found = false;
let candidates = walk_to_root(root_path);

for cand in candidates {
let dotenv_cand = cand.join(".env");
if dotenv_cand.exists() {
sources.dotenv.push(dotenv_cand);
}
let dotenv_cand = cand.join(format!(".env.{}", env));
if dotenv_cand.exists() {
sources.dotenv.push(dotenv_cand);
}
'outer: for &settings_dir in SETTINGS_DIRS {
let dir = cand.join(settings_dir);
for &ext in SETTINGS_FILE_EXTENSIONS {
let settings_cand = dir.join(format!("settings.{}", ext));
if settings_cand.exists() {
sources.settings = Some(settings_cand);
settings_found = true;
}
let secrets_cand = dir.join(format!(".secrets.{}", ext));
if secrets_cand.exists() {
sources.secrets = Some(secrets_cand);
settings_found = true;
}
if settings_found {
break 'outer;
}
}
}

if sources.any() {
break;
}
}

sources
}

fn any(&self) -> bool {
self.settings.is_some()
|| self.secrets.is_some()
|| !self.dotenv.is_empty()
}
}

pub fn walk_to_root(mut path: PathBuf) -> Vec<PathBuf> {
let mut candidates = Vec::new();
if path.is_file() {
path = path.parent().unwrap_or_else(|| Path::new("/")).into();
}
for ancestor in path.ancestors() {
candidates.push(ancestor.to_path_buf());
}
candidates
}

#[cfg(test)]
mod test {
use super::*;
use std::env;

fn get_data_path(suffix: &str) -> PathBuf {
let mut target_dir = PathBuf::from(
env::current_exe()
.expect("exe path")
.parent()
.expect("exe parent"),
);
while target_dir.file_name() != Some(std::ffi::OsStr::new("target")) {
if !target_dir.pop() {
panic!("Cannot find target directory");
}
}
target_dir.pop();
target_dir.join(format!("tests/data{}", suffix))
}

#[test]
fn test_walk_to_root_dir() {
assert_eq!(
walk_to_root(PathBuf::from("/a/dir/located/somewhere")),
vec![
PathBuf::from("/a/dir/located/somewhere"),
PathBuf::from("/a/dir/located"),
PathBuf::from("/a/dir"),
PathBuf::from("/a"),
PathBuf::from("/"),
],
);
}

#[test]
fn test_walk_to_root_root() {
assert_eq!(walk_to_root(PathBuf::from("/")), vec![PathBuf::from("/")],);
}

#[test]
fn test_sources() {
let data_path = get_data_path("");
assert_eq!(
FileSources::from_root(data_path.clone(), "development"),
FileSources {
settings: Some(data_path.clone().join("config/settings.toml")),
secrets: Some(data_path.join("config/.secrets.toml")),
dotenv: vec![data_path.join(".env")],
},
);

let data_path = get_data_path("2");
assert_eq!(
FileSources::from_root(data_path.clone(), "development"),
FileSources {
settings: Some(data_path.clone().join("config/settings.toml")),
secrets: Some(data_path.join("config/.secrets.toml")),
dotenv: vec![
data_path.join(".env"),
data_path.join(".env.development")
],
},
);

let data_path = get_data_path("2");
assert_eq!(
FileSources::from_root(data_path.clone(), "production"),
FileSources {
settings: Some(data_path.clone().join("config/settings.toml")),
secrets: Some(data_path.join("config/.secrets.toml")),
dotenv: vec![data_path.join(".env")],
},
);

let data_path = get_data_path("3");
assert_eq!(
FileSources::from_root(data_path.clone(), "development"),
FileSources {
settings: Some(data_path.clone().join("settings.toml")),
secrets: Some(data_path.join(".secrets.toml")),
dotenv: vec![data_path.join(".env")],
},
);

let data_path = get_data_path("3");
assert_eq!(
FileSources::from_root(data_path.clone(), "production"),
FileSources {
settings: Some(data_path.clone().join("settings.toml")),
secrets: Some(data_path.join(".secrets.toml")),
dotenv: vec![
data_path.join(".env"),
data_path.join(".env.production")
],
},
);
}
}

0 comments on commit bb3acf7

Please sign in to comment.