use config::{Config, ConfigError, File, Value, ValueKind};
use std::collections::{HashMap, HashSet};
use std::fmt;
use crate::error::{ConfpilerError, Result};
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct FlatConfig {
origin: String,
items: HashMap<String, String>,
}
impl FlatConfig {
pub fn builder() -> FlatConfigBuilder {
FlatConfigBuilder::default()
}
pub fn items(&self) -> &HashMap<String, String> {
&self.items
}
pub fn merge(&mut self, other: &Self) -> Vec<MergeWarning> {
let mut warnings = Vec::new();
for (k, v) in other.items.iter() {
self.items
.entry(k.to_string())
.and_modify(|e| {
if e == v {
warnings.push(MergeWarning::RedundantValue {
overrider: other.origin.clone(),
key: k.to_string(),
value: e.clone(),
});
} else {
*e = v.to_string();
}
})
.or_insert_with(|| v.to_string());
}
warnings
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct FlatConfigBuilder {
prefix: Option<String>,
configs: Vec<String>,
separator: String,
array_separator: String,
}
impl FlatConfigBuilder {
pub const DEFAULT_SEPARATOR: &'static str = "__";
pub const DEFAULT_ARRAY_SEPARATOR: &'static str = ",";
pub fn add_config(&mut self, config: &str) -> &mut Self {
self.configs.push(config.to_string());
self
}
pub fn with_separator(&mut self, separator: &str) -> &mut Self {
self.separator = separator.to_string();
self
}
pub fn with_array_separator(&mut self, separator: &str) -> &mut Self {
self.array_separator = separator.to_string();
self
}
pub fn with_prefix(&mut self, prefix: &str) -> &mut Self {
self.prefix = Some(prefix.to_ascii_uppercase());
self
}
pub fn build(&self) -> Result<(FlatConfig, Vec<MergeWarning>)> {
if self.configs.is_empty() {
return Err(ConfpilerError::NoConfigSpecified);
}
let mut seen_configs: HashSet<&str> = HashSet::new();
let mut flat_config = FlatConfig {
origin: self.configs.first().unwrap().to_string(),
items: HashMap::new(),
};
let mut warnings = Vec::new();
for conf_path in self.configs.iter() {
if seen_configs.contains(conf_path.as_str()) {
return Err(ConfpilerError::DuplicateConfig(conf_path.to_string()));
} else {
seen_configs.insert(conf_path.as_str());
}
let conf = Config::builder()
.add_source(File::with_name(conf_path))
.build()?;
let input = conf.cache.into_table()?;
let mut out = HashMap::new();
flatten_into(
&input,
&mut out,
self.prefix.as_ref(),
&self.separator,
&self.array_separator,
)?;
let working_config = FlatConfig {
origin: conf_path.to_string(),
items: out,
};
let mut working_warnings = flat_config.merge(&working_config);
warnings.append(&mut working_warnings);
}
Ok((flat_config, warnings))
}
}
impl Default for FlatConfigBuilder {
fn default() -> Self {
Self {
prefix: None,
configs: Vec::new(),
separator: Self::DEFAULT_SEPARATOR.to_string(),
array_separator: Self::DEFAULT_ARRAY_SEPARATOR.to_string(),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum MergeWarning {
RedundantValue {
overrider: String,
key: String,
value: String,
},
}
impl fmt::Display for MergeWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RedundantValue {
ref overrider,
ref key,
ref value,
} => {
write!(f, "'{overrider}' is attempting to override '{key}' with '{value}', but the key already contains that value")
}
}
}
}
pub(crate) fn flatten_into(
input: &HashMap<String, Value>,
output: &mut HashMap<String, String>,
prefix: Option<&String>,
separator: &str,
array_separator: &str,
) -> Result<()> {
let mut components = Vec::new();
if let Some(prefix) = prefix {
components.push(prefix.clone());
}
flatten_into_inner(input, output, separator, array_separator, &mut components)
}
fn flatten_into_inner(
input: &HashMap<String, Value>,
output: &mut HashMap<String, String>,
separator: &str,
array_separator: &str,
components: &mut Vec<String>,
) -> Result<()> {
if input.is_empty() {
return Ok(());
}
for (key, value) in input.iter() {
let upper_key = key.to_ascii_uppercase();
components.push(upper_key);
match &value.kind {
ValueKind::Nil => {}
ValueKind::Table(ref table) => {
flatten_into_inner(table, output, separator, array_separator, components)?;
}
ValueKind::Array(ref array) => {
let candidate = components.join(separator);
if output.contains_key(&candidate) {
return Err(ConfpilerError::DuplicateKey(candidate));
}
let val = array
.iter()
.cloned()
.map(|e| e.into_string())
.collect::<std::result::Result<Vec<String>, ConfigError>>()
.map_err(|_| ConfpilerError::UnsupportedArray(candidate.clone()))?
.join(array_separator);
output.insert(candidate, val);
}
_ => {
let candidate = components.join(separator);
if output.contains_key(&candidate) {
return Err(ConfpilerError::DuplicateKey(candidate));
}
output.insert(candidate, value.clone().into_string()?);
}
}
components.pop();
}
Ok(())
}
#[cfg(test)]
mod tests {
mod flat_config {
use super::super::*;
#[test]
fn builder_yields_a_default_builder() {
assert_eq!(FlatConfig::builder(), FlatConfigBuilder::default());
}
#[test]
fn merging() {
let mut a = FlatConfig {
origin: "origin1".to_string(),
items: HashMap::from([
("herp".to_string(), "derp".to_string()),
("hoof".to_string(), "changeme".to_string()),
]),
};
let b = FlatConfig {
origin: "origin2".to_string(),
items: HashMap::from([
("foo".to_string(), "bar".to_string()),
("hoof".to_string(), "doof".to_string()),
]),
};
let expected = FlatConfig {
origin: "origin1".to_string(),
items: HashMap::from([
("foo".to_string(), "bar".to_string()),
("hoof".to_string(), "doof".to_string()),
("herp".to_string(), "derp".to_string()),
]),
};
let warnings = a.merge(&b);
assert_eq!(a, expected);
assert!(warnings.is_empty());
}
#[test]
fn merging_when_overriding_with_same_value_generates_warnings() {
let mut a = FlatConfig {
origin: "origin1".to_string(),
items: HashMap::from([
("herp".to_string(), "derp".to_string()),
("hoof".to_string(), "changeme".to_string()),
]),
};
let b = FlatConfig {
origin: "origin2".to_string(),
items: HashMap::from([
("foo".to_string(), "bar".to_string()),
("herp".to_string(), "derp".to_string()),
("hoof".to_string(), "changeme".to_string()),
]),
};
let expected = FlatConfig {
origin: "origin1".to_string(),
items: HashMap::from([
("foo".to_string(), "bar".to_string()),
("herp".to_string(), "derp".to_string()),
("hoof".to_string(), "changeme".to_string()),
]),
};
let warnings = a.merge(&b);
assert_eq!(a, expected);
assert_eq!(warnings.len(), 2);
assert!(warnings.contains(&MergeWarning::RedundantValue {
overrider: "origin2".to_string(),
key: "herp".to_string(),
value: "derp".to_string(),
}));
assert!(warnings.contains(&MergeWarning::RedundantValue {
overrider: "origin2".to_string(),
key: "hoof".to_string(),
value: "changeme".to_string(),
}));
}
}
mod flat_config_builder {
use super::super::*;
#[test]
fn defaults() {
let builder = FlatConfigBuilder::default();
assert!(builder.configs.is_empty());
assert_eq!(builder.separator, "__".to_string());
assert_eq!(builder.array_separator, ",".to_string());
}
#[test]
fn adding_configs() {
let mut builder = FlatConfigBuilder::default();
builder.add_config("foo/bar");
builder.add_config("foo/baz");
let expected = vec!["foo/bar".to_string(), "foo/baz".to_string()];
assert_eq!(builder.configs, expected);
}
#[test]
fn specifying_prefix() {
let mut builder = FlatConfigBuilder::default();
builder.with_prefix("foo");
assert_eq!(builder.prefix, Some("FOO".to_string()));
}
#[test]
fn specifying_separator() {
let mut builder = FlatConfigBuilder::default();
builder.with_separator("*");
assert_eq!(builder.separator, "*".to_string());
}
#[test]
fn specifying_array_separator() {
let mut builder = FlatConfigBuilder::default();
builder.with_array_separator("---");
assert_eq!(builder.array_separator, "---".to_string());
}
}
mod flatten_into {
use super::super::*;
fn valid_input() -> HashMap<String, Value> {
let origin = "test".to_string();
let input = HashMap::from([
(
"foo".to_string(),
Value::new(Some(&origin), ValueKind::Float(10.2)),
),
(
"bar".to_string(),
Value::new(Some(&origin), ValueKind::String("Hello".to_string())),
),
(
"baz".to_string(),
Value::new(
Some(&origin),
ValueKind::Table(HashMap::from([
(
"herp".to_string(),
Value::new(Some(&origin), ValueKind::Boolean(false)),
),
(
"derp".to_string(),
Value::new(Some(&origin), ValueKind::I64(15)),
),
(
"hoof".to_string(),
Value::new(
Some(&origin),
ValueKind::Table(HashMap::from([(
"doof".to_string(),
Value::new(Some(&origin), ValueKind::I64(999)),
)])),
),
),
])),
),
),
(
"biz".to_string(),
Value::new(
Some(&origin),
ValueKind::Array(vec![
Value::new(Some(&origin), ValueKind::Boolean(false)),
Value::new(Some(&origin), ValueKind::I64(1111)),
Value::new(Some(&origin), ValueKind::String("Goodbye".to_string())),
]),
),
),
]);
input
}
#[test]
fn accepts_empty_input() {
let mut out = HashMap::new();
let input = HashMap::new();
let res = flatten_into(&input, &mut out, None, "__", ",");
assert!(res.is_ok());
assert!(out.is_empty());
}
#[test]
fn flattens_valid_input() {
let mut out = HashMap::new();
let input = valid_input();
let expected: HashMap<String, String> = HashMap::from([
("FOO".to_string(), "10.2".to_string()),
("BAR".to_string(), "Hello".to_string()),
("BAZ__HERP".to_string(), "false".to_string()),
("BAZ__DERP".to_string(), "15".to_string()),
("BAZ__HOOF__DOOF".to_string(), "999".to_string()),
("BIZ".to_string(), "false,1111,Goodbye".to_string()),
]);
let res = flatten_into(&input, &mut out, None, "__", ",");
assert!(res.is_ok());
assert_eq!(out, expected);
}
#[test]
fn supports_prefixing() {
let mut out = HashMap::new();
let input = valid_input();
let expected: HashMap<String, String> = HashMap::from([
("PRE__FOO".to_string(), "10.2".to_string()),
("PRE__BAR".to_string(), "Hello".to_string()),
("PRE__BAZ__HERP".to_string(), "false".to_string()),
("PRE__BAZ__DERP".to_string(), "15".to_string()),
("PRE__BAZ__HOOF__DOOF".to_string(), "999".to_string()),
("PRE__BIZ".to_string(), "false,1111,Goodbye".to_string()),
]);
let prefix = Some("PRE".to_string());
let res = flatten_into(&input, &mut out, prefix.as_ref(), "__", ",");
assert!(res.is_ok());
assert_eq!(out, expected);
}
#[test]
fn uses_the_specified_separators() {
let mut out = HashMap::new();
let input = valid_input();
let expected: HashMap<String, String> = HashMap::from([
("FOO".to_string(), "10.2".to_string()),
("BAR".to_string(), "Hello".to_string()),
("BAZ*HERP".to_string(), "false".to_string()),
("BAZ*DERP".to_string(), "15".to_string()),
("BAZ*HOOF*DOOF".to_string(), "999".to_string()),
("BIZ".to_string(), "false 1111 Goodbye".to_string()),
]);
let res = flatten_into(&input, &mut out, None, "*", " ");
assert!(res.is_ok());
assert_eq!(out, expected);
}
#[test]
fn errors_on_duplicate_keys() {
let mut out = HashMap::new();
let valid = valid_input();
let mut invalid = valid.clone();
invalid.insert(
"fOo".to_string(),
Value::new(Some(&"test".to_string()), ValueKind::Float(1.0)),
);
let res = flatten_into(&invalid, &mut out, None, "__", ",");
assert!(res.is_err());
match res.unwrap_err() {
ConfpilerError::DuplicateKey(key) => assert_eq!(key, "FOO".to_string()),
e => panic!("unexpected error variant: {}", e),
};
let mut invalid = valid.clone();
invalid.insert(
"baz__herp".to_string(),
Value::new(Some(&"test".to_string()), ValueKind::Boolean(true)),
);
let mut out = HashMap::new();
let res = flatten_into(&invalid, &mut out, None, "__", ",");
assert!(res.is_err());
match res.unwrap_err() {
ConfpilerError::DuplicateKey(key) => assert_eq!(key, "BAZ__HERP".to_string()),
e => panic!("unexpected error variant: {}", e),
};
}
#[test]
fn errors_on_unsupported_array() {
let mut out = HashMap::new();
let valid = valid_input();
let origin = "test".to_string();
let mut invalid = valid.clone();
invalid.insert(
"biz".to_string(),
Value::new(
Some(&"test".to_string()),
ValueKind::Array(vec![
Value::new(Some(&origin), ValueKind::Boolean(false)),
Value::new(Some(&origin), ValueKind::Table(HashMap::new())),
Value::new(Some(&origin), ValueKind::String("Goodbye".to_string())),
]),
),
);
let res = flatten_into(&invalid, &mut out, None, "__", ",");
assert!(res.is_err());
match res.unwrap_err() {
ConfpilerError::UnsupportedArray(key) => assert_eq!(key, "BIZ".to_string()),
e => panic!("unexpected error variant: {}", e),
};
}
}
}