From 115fe07e2c11aa72e91a5ce9b028ed1c1ff7d806 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 2 Feb 2017 12:03:55 -0800 Subject: Add support for Table/Array and deep merging of configuration values --- src/config.rs | 243 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 160 insertions(+), 83 deletions(-) (limited to 'src/config.rs') diff --git a/src/config.rs b/src/config.rs index b3e296b..126d31b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use source::{Source, SourceBuilder}; use std::error::Error; use std::fmt; use std::borrow::Cow; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; #[derive(Default, Debug)] pub struct FrozenError { } @@ -22,10 +22,12 @@ impl Error for FrozenError { } // Underlying storage for the configuration -enum ConfigStore<'a> { +enum ConfigStore { Mutable { - defaults: HashMap>, - overrides: HashMap>, + defaults: HashMap, + overrides: HashMap, + + // Ordered list of sources sources: Vec>, }, @@ -34,7 +36,7 @@ enum ConfigStore<'a> { Frozen, } -impl<'a> Default for ConfigStore<'a> { +impl Default for ConfigStore { fn default() -> Self { ConfigStore::Mutable { defaults: HashMap::new(), @@ -44,7 +46,60 @@ impl<'a> Default for ConfigStore<'a> { } } -impl<'a> ConfigStore<'a> { +const KEY_DELIM: char = '.'; + +fn merge_in(r: &mut HashMap, key: &str, value: &Value) { + let key_segments: VecDeque<&str> = key.splitn(2, KEY_DELIM).collect(); + + if key_segments.len() > 1 { + // Ensure there is at least an empty hash map + let key = key_segments[0].to_string(); + if r.contains_key(&key) { + // Coerce to table + match *r.get(&key).unwrap() { + Value::Table(_) => { + // Do nothing; already table + } + + _ => { + // Override with empty table + r.insert(key.clone(), Value::Table(HashMap::new())); + } + } + } else { + // Insert table + r.insert(key.clone(), Value::Table(HashMap::new())); + } + + // Continue to merge + if let Value::Table(ref mut table) = *r.get_mut(&key).unwrap() { + merge_in(table, key_segments[1], value); + } + + return; + } + + // Check if we are setting a table (and if we should do a deep merge) + if let Value::Table(ref table) = *value { + let inner_v = r.get_mut(key); + if let Some(&mut Value::Table(ref mut inner_table)) = inner_v { + merge_in_all(inner_table, table); + + return; + } + } + + // Direct set/override whatever is here + r.insert(key.into(), value.clone()); +} + +fn merge_in_all(r: &mut HashMap, map: &HashMap) { + for (key, value) in map { + merge_in(r, key, value); + } +} + +impl ConfigStore { fn merge(&mut self, source: T) -> Result<(), Box> where T: SourceBuilder { @@ -58,10 +113,10 @@ impl<'a> ConfigStore<'a> { } fn set_default(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { if let ConfigStore::Mutable { ref mut defaults, .. } = *self { - defaults.insert(key.to_lowercase(), value.into()); + merge_in(defaults, &key.to_lowercase(), &value.into()); Ok(()) } else { @@ -70,10 +125,10 @@ impl<'a> ConfigStore<'a> { } fn set(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { if let ConfigStore::Mutable { ref mut overrides, .. } = *self { - overrides.insert(key.to_lowercase(), value.into()); + merge_in(overrides, &key.to_lowercase(), &value.into()); Ok(()) } else { @@ -81,53 +136,38 @@ impl<'a> ConfigStore<'a> { } } - fn get(&self, key: &str) -> Option> { + fn collect(&self) -> Result, Box> { if let ConfigStore::Mutable { ref overrides, ref sources, ref defaults } = *self { - // Check explicit override - if let Some(value) = overrides.get(key) { - return Some(Cow::Borrowed(value)); - } + let mut r = HashMap::::new(); - // Check sources - for source in &mut sources.iter().rev() { - if let Some(value) = source.get(key) { - return Some(value); - } - } + merge_in_all(&mut r, defaults); - // Check explicit defaults - if let Some(value) = defaults.get(key) { - return Some(Cow::Borrowed(value)); + for source in sources { + merge_in_all(&mut r, &source.collect()); } - } - None + merge_in_all(&mut r, overrides); + + Ok(r) + } else { + Err(FrozenError::default().into()) + } } } #[derive(Default)] -pub struct Config<'a> { - store: ConfigStore<'a>, +pub struct Config { + store: ConfigStore, + + /// Top-level table of the cached configuration + /// + /// As configuration sources are merged with `Config::merge`, this + /// cache is updated. + cache: HashMap, } -// TODO(@rust): There must be a way to remove this function and use Value::as_str -#[allow(needless_lifetimes)] -fn value_into_str<'a>(value: Value<'a>) -> Option> { - if let Value::String(value) = value { - Some(value) - } else if let Value::Integer(value) = value { - Some(Cow::Owned(value.to_string())) - } else if let Value::Float(value) = value { - Some(Cow::Owned(value.to_string())) - } else if let Value::Boolean(value) = value { - Some(Cow::Owned(value.to_string())) - } else { - None - } -} - -impl<'a> Config<'a> { - pub fn new() -> Config<'a> { +impl Config { + pub fn new() -> Self { Default::default() } @@ -135,73 +175,73 @@ impl<'a> Config<'a> { pub fn merge(&mut self, source: T) -> Result<(), Box> where T: SourceBuilder { - self.store.merge(source) + self.store.merge(source)?; + self.refresh()?; + + Ok(()) } /// Sets the default value for this key. The default value is only used /// when no other value is provided. pub fn set_default(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { - self.store.set_default(key, value) + self.store.set_default(key, value)?; + self.refresh()?; + + Ok(()) } /// Sets an override for this key. pub fn set(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { - self.store.set(key, value) - } + self.store.set(key, value)?; + self.refresh()?; - pub fn get(&self, key: &str) -> Option> { - self.store.get(key) + Ok(()) } - pub fn get_str(&'a self, key: &str) -> Option> { - // TODO(@rust): This is a bit nasty looking; 3x match and requires this - // odd into_str method - if let Some(value) = self.get(key) { - match value { - Cow::Borrowed(value) => { - match value.as_str() { - Some(value) => { - match value { - Cow::Borrowed(value) => Some(Cow::Borrowed(value)), - Cow::Owned(value) => Some(Cow::Owned(value)), - } - } + /// Refresh the configuration cache with fresh + /// data from associated sources. + /// + /// Configuration is automatically refreshed after a mutation + /// operation (`set`, `merge`, `set_default`, etc.). + pub fn refresh(&mut self) -> Result<(), Box> { + self.cache = self.store.collect()?; - _ => None, - } - } + Ok(()) + } - Cow::Owned(value) => value_into_str(value), - } - } else { - None - } + pub fn get<'a>(&'a self, key: &str) -> Option<&'a Value> { + self.cache.get(key) + } + + pub fn get_str<'a>(&'a self, key: &str) -> Option> { + self.get(key).and_then(Value::as_str) } pub fn get_int(&self, key: &str) -> Option { - // TODO(@rust): Why doesn't .and_then(Value::as_int) work? - self.get(key).and_then(|v| v.as_int()) + self.get(key).and_then(Value::as_int) } pub fn get_float(&self, key: &str) -> Option { - // TODO(@rust): Why doesn't .and_then(Value::as_float) work? - self.get(key).and_then(|v| v.as_float()) + self.get(key).and_then(Value::as_float) } pub fn get_bool(&self, key: &str) -> Option { - // TODO(@rust): Why doesn't .and_then(Value::as_bool) work? - self.get(key).and_then(|v| v.as_bool()) + self.get(key).and_then(Value::as_bool) + } + + pub fn get_map<'a>(&'a self, key: &str) -> Option<&'a HashMap> { + self.get(key).and_then(Value::as_map) } } #[cfg(test)] mod test { - // use std::env; - use super::Config; + use std::collections::HashMap; + use super::{Value, Config}; // Retrieval of a non-existent key #[test] @@ -351,4 +391,41 @@ mod test { assert_eq!(c.get_bool("key_10"), Some(false)); assert_eq!(c.get_bool("key_11"), None); } + + // Deep merge of tables + #[test] + fn test_merge() { + let mut c = Config::new(); + + { + let mut m = HashMap::new(); + m.insert("port".into(), Value::Integer(6379)); + m.insert("address".into(), Value::String("::1".into())); + + c.set("redis", m).unwrap(); + } + + { + let m = c.get_map("redis").unwrap(); + + assert_eq!(m.get("port").unwrap().as_int().unwrap(), 6379); + assert_eq!(m.get("address").unwrap().as_str().unwrap(), "::1"); + } + + { + let mut m = HashMap::new(); + m.insert("address".into(), Value::String("::0".into())); + m.insert("db".into(), Value::Integer(1)); + + c.set("redis", m).unwrap(); + } + + { + let m = c.get_map("redis").unwrap(); + + assert_eq!(m.get("port").unwrap().as_int().unwrap(), 6379); + assert_eq!(m.get("address").unwrap().as_str().unwrap(), "::0"); + assert_eq!(m.get("db").unwrap().as_str().unwrap(), "1"); + } + } } -- cgit v1.2.3