From 65714bea49036cb2f9188072a8a5d143ac9b9eb9 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 8 Feb 2017 02:04:56 -0800 Subject: Implement 'namespace' on File --- src/config.rs | 234 +++++++++++++++++++++++++++++++++++++++++++++---------- src/file/json.rs | 16 +++- src/file/mod.rs | 28 +++---- src/file/toml.rs | 16 +++- src/file/yaml.rs | 25 ++++-- src/value.rs | 1 + 6 files changed, 257 insertions(+), 63 deletions(-) diff --git a/src/config.rs b/src/config.rs index cfe5ea5..4df5377 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,8 +4,9 @@ use path; use std::error::Error; use std::fmt; +use std::str::FromStr; use std::borrow::Cow; -use std::collections::{HashMap, VecDeque}; +use std::collections::HashMap; #[derive(Default, Debug)] pub struct FrozenError { } @@ -47,57 +48,164 @@ impl Default for ConfigStore { } } -const KEY_DELIM: char = '.'; +fn merge_in_all(r: &mut HashMap, map: &HashMap) { + for (key, value) in map { + path_set_str(r, key, value); + } +} + +// Child ( Child ( Identifier( "x" ), "y" ), "z" ) +fn path_get_mut<'a>(root: &'a mut HashMap, expr: path::Expression) -> Option<&'a mut Value> { + match expr { + path::Expression::Identifier(text) => Some(root.entry(text.clone()).or_insert(Value::Nil)), + + path::Expression::Child(expr, member) => { + match path_get_mut(root, *expr) { + Some(&mut Value::Table(ref mut table)) => Some(table.entry(member.clone()).or_insert(Value::Nil)), + + Some(v @ _) => { + *v = Value::Table(HashMap::new()); + if let Value::Table(ref mut table) = *v { + Some(table.entry(member.clone()).or_insert(Value::Nil)) + } else { + None + } + } + + _ => None, + } + } + + path::Expression::Subscript(expr, mut index) => { + match path_get_mut(root, *expr) { + Some(&mut Value::Array(ref mut array)) => { + let len = array.len() as i32; + + if index < 0 { + index = len + index; + } + + if index < 0 { + None + } else { + // Ensure there is enough room + array.resize((index + 1) as usize, Value::Nil); + + Some(&mut array[index as usize]) + } + } + + _ => None, + } + } + } +} -fn merge_in(r: &mut HashMap, key: &str, value: &Value) { - let key_segments: VecDeque<&str> = key.splitn(2, KEY_DELIM).collect(); +fn require_table(r: &mut HashMap, key: &String) { + if r.contains_key(key) { + // Coerce to table + match *r.get(key).unwrap() { + Value::Table(_) => { + // Do nothing; already table + } - 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())); + } +} + +fn path_set(root: &mut HashMap, expr: path::Expression, value: &Value) { + match expr { + path::Expression::Identifier(text) => { + match *value { + Value::Table(ref table_v) => { + require_table(root, &text); + if let Value::Table(ref mut target) = *root.get_mut(&text).unwrap() { + merge_in_all(target, table_v); + } } _ => { - // Override with empty table - r.insert(key.clone(), Value::Table(HashMap::new())); + root.insert(text, value.clone()); } } - } 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); + path::Expression::Child(expr, member) => { + if let Some(parent) = path_get_mut(root, *expr) { + match *parent { + Value::Table(ref mut table) => { + path_set(table, path::Expression::Identifier(member), value); + } + + _ => { + // Coerce to a table and do the insert anyway + *parent = Value::Table(HashMap::new()); + if let Value::Table(ref mut table) = *parent { + path_set(table, path::Expression::Identifier(member), value); + } + } + } + } } - return; - } + path::Expression::Subscript(inner_expr, mut index) => { + if let Some(parent) = path_get_mut(root, *inner_expr) { + match *parent { + Value::Array(ref mut array) => { + let len = array.len() as i32; - // 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); + if index < 0 { + index = len + index; + } - return; + if index >= 0 { + array[index as usize] = value.clone(); + } + } + + Value::Nil => { + // Add an array and do this again + *parent = Value::Array(Vec::new()); + if let Value::Array(ref mut array) = *parent { + let len = array.len() as i32; + + if index < 0 { + index = len + index; + } + + if index >= 0 { + array.resize((index + 1) as usize, Value::Nil); + array[index as usize] = value.clone(); + } + } + } + + _ => { + // Do nothing + } + } + } } } - - // 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); - } +fn path_set_str(root: &mut HashMap, key: &str, value: &Value) { + match path::Expression::from_str(key) { + Ok(expr) => { + path_set(root, expr, value); + }, + + Err(_) => { + // TODO: Log warning here + } + }; } impl ConfigStore { @@ -117,7 +225,7 @@ impl ConfigStore { where T: Into { if let ConfigStore::Mutable { ref mut defaults, .. } = *self { - merge_in(defaults, &key.to_lowercase(), &value.into()); + path_set_str(defaults, &key.to_lowercase(), &value.into()); Ok(()) } else { @@ -129,7 +237,7 @@ impl ConfigStore { where T: Into { if let ConfigStore::Mutable { ref mut overrides, .. } = *self { - merge_in(overrides, &key.to_lowercase(), &value.into()); + path_set_str(overrides, &key.to_lowercase(), &value.into()); Ok(()) } else { @@ -215,7 +323,7 @@ impl Config { } // Child ( Child ( Identifier( "x" ), "y" ), "z" ) - fn path_get<'a, 'b>(&'a self, expr: path::Expression) -> Option<&'a Value> { + fn path_get<'a>(&'a self, expr: path::Expression) -> Option<&'a Value> { match expr { path::Expression::Identifier(text) => self.cache.get(&text), @@ -250,7 +358,7 @@ impl Config { } pub fn get<'a>(&'a self, key_path: &str) -> Option<&'a Value> { - let key_expr: path::Expression = match key_path.parse() { + let key_expr: path::Expression = match key_path.to_lowercase().parse() { Ok(expr) => expr, Err(_) => { // TODO: Log warning here @@ -545,4 +653,52 @@ mod test { assert_eq!(m.get("db").unwrap().as_int().unwrap(), 2); } } + + #[test] + fn test_map_set_coerce() { + let mut c = Config::new(); + + // Coerce value to table + c.set("redis", 10).unwrap(); + c.set("redis.port", 6379).unwrap(); + + assert_eq!(c.get_int("redis.port"), Some(6379)); + + // Coerce nil to table + c.set("server.port", 80).unwrap(); + + assert_eq!(c.get_int("server.port"), Some(80)); + } + + #[test] + fn test_slice_set_coerce() { + let mut c = Config::new(); + + // Coerce nil to slice + c.set("values[2]", 45).unwrap(); + + assert_eq!(c.get_int("values[2]"), Some(45)); + } + + #[test] + fn test_file_namespace() { + use file::{File, FileFormat}; + + let mut c = Config::new(); + let text = r#" + [development] + port = 8080 + + [production] + port = 80 + "#; + + c.merge(File::from_str(text, FileFormat::Toml).namespace("development")).unwrap(); + + assert_eq!(c.get_int("port"), Some(8080)); + + c.merge(File::from_str(text, FileFormat::Toml).namespace("production")).unwrap(); + + assert_eq!(c.get_int("port"), Some(80)); + } } diff --git a/src/file/json.rs b/src/file/json.rs index 86612c9..64c6236 100644 --- a/src/file/json.rs +++ b/src/file/json.rs @@ -11,9 +11,21 @@ pub struct Content { } impl Content { - pub fn parse(text: &str) -> Result, Box> { + pub fn parse(text: &str, namespace: Option<&String>) -> Result, Box> { // Parse - let root = serde_json::from_str(text)?; + let mut root: serde_json::Value = serde_json::from_str(text)?; + + // Limit to namespace + if let Some(namespace) = namespace { + if let serde_json::Value::Object(mut root_map) = root { + if let Some(value) = root_map.remove(namespace) { + root = value; + } else { + // TODO: Warn? + root = serde_json::Value::Object(serde_json::Map::new()); + } + } + } Ok(Box::new(Content { root: root })) } diff --git a/src/file/mod.rs b/src/file/mod.rs index 8f9578d..519a4bf 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -47,29 +47,29 @@ impl FileFormat { } #[allow(unused_variables)] - fn parse(&self, text: &str) -> Result, Box> { + fn parse(&self, text: &str, namespace: Option<&String>) -> Result, Box> { match *self { #[cfg(feature = "toml")] - FileFormat::Toml => toml::Content::parse(text), + FileFormat::Toml => toml::Content::parse(text, namespace), #[cfg(feature = "json")] - FileFormat::Json => json::Content::parse(text), + FileFormat::Json => json::Content::parse(text, namespace), #[cfg(feature = "yaml")] - FileFormat::Yaml => yaml::Content::parse(text), + FileFormat::Yaml => yaml::Content::parse(text, namespace), } } } pub trait FileSource { - fn try_build(&self, format: FileFormat) -> Result, Box>; + fn try_build(&self, format: FileFormat, namespace: Option<&String>) -> Result, Box>; } pub struct FileSourceString(String); impl FileSource for FileSourceString { - fn try_build(&self, format: FileFormat) -> Result, Box> { - format.parse(&self.0) + fn try_build(&self, format: FileFormat, namespace: Option<&String>) -> Result, Box> { + format.parse(&self.0, namespace) } } @@ -123,7 +123,7 @@ impl FileSourceFile { } impl FileSource for FileSourceFile { - fn try_build(&self, format: FileFormat) -> Result, Box> { + fn try_build(&self, format: FileFormat, namespace: Option<&String>) -> Result, Box> { // Find file let filename = self.find_file(format)?; @@ -133,7 +133,7 @@ impl FileSource for FileSourceFile { file.read_to_string(&mut text)?; // Parse the file - format.parse(&text) + format.parse(&text, namespace) } } @@ -181,9 +181,13 @@ impl File { File { required: required, ..self } } + pub fn namespace(self, namespace: &str) -> Self { + File { namespace: Some(namespace.into()), ..self } + } + // Build normally and return error on failure fn try_build(&self) -> Result, Box> { - self.source.try_build(self.format) + self.source.try_build(self.format, self.namespace.as_ref()) } } @@ -191,10 +195,6 @@ impl File { pub fn path(self, path: &str) -> Self { File { source: FileSourceFile { path: Some(path.into()), ..self.source }, ..self } } - - pub fn namespace(self, namespace: &str) -> Self { - File { namespace: Some(namespace.into()), ..self } - } } impl SourceBuilder for File { diff --git a/src/file/toml.rs b/src/file/toml.rs index 28a1507..e3fb0d8 100644 --- a/src/file/toml.rs +++ b/src/file/toml.rs @@ -1,6 +1,6 @@ use toml; use source::Source; -use std::collections::HashMap; +use std::collections::{HashMap, BTreeMap}; use std::error::Error; use value::Value; @@ -10,11 +10,21 @@ pub struct Content { } impl Content { - pub fn parse(text: &str) -> Result, Box> { + pub fn parse(text: &str, namespace: Option<&String>) -> Result, Box> { // Parse let mut parser = toml::Parser::new(text); // TODO: Get a solution to make this return an Error-able - let root = parser.parse().unwrap(); + let mut root = parser.parse().unwrap(); + + // Limit to namespace + if let Some(namespace) = namespace { + if let Some(toml::Value::Table(table)) = root.remove(namespace) { + root = table; + } else { + // TODO: Warn? + root = BTreeMap::new(); + } + } Ok(Box::new(Content { root: toml::Value::Table(root) })) } diff --git a/src/file/yaml.rs b/src/file/yaml.rs index 72a987a..00b749c 100644 --- a/src/file/yaml.rs +++ b/src/file/yaml.rs @@ -13,14 +13,29 @@ pub struct Content { } impl Content { - pub fn parse(text: &str) -> Result, Box> { + pub fn parse(text: &str, namespace: Option<&String>) -> Result, Box> { let mut docs = yaml::YamlLoader::load_from_str(text)?; - match docs.len() { - 0 => Ok(Box::new(Content { root: yaml::Yaml::Hash(BTreeMap::new()) })), - 1 => Ok(Box::new(Content { root: mem::replace(&mut docs[0], yaml::Yaml::Null) })), - n => Err(Box::new(MultipleDocumentsError(n))), + // Designate root + let mut root = match docs.len() { + 0 => yaml::Yaml::Hash(BTreeMap::new()), + 1 => mem::replace(&mut docs[0], yaml::Yaml::Null), + n => { return Err(Box::new(MultipleDocumentsError(n))); } + }; + + // Limit to namespace + if let Some(namespace) = namespace { + if let yaml::Yaml::Hash(mut root_map) = root { + if let Some(value) = root_map.remove(&yaml::Yaml::String(namespace.clone())) { + root = value; + } else { + // TODO: Warn? + root = yaml::Yaml::Hash(BTreeMap::new()); + } + } } + + Ok(Box::new(Content { root: root })) } pub fn from_yaml(doc: yaml::Yaml) -> Content { diff --git a/src/value.rs b/src/value.rs index fa2f791..bb88c34 100644 --- a/src/value.rs +++ b/src/value.rs @@ -8,6 +8,7 @@ use std::borrow::Cow; /// but will be coerced into the requested type. #[derive(Debug, Clone, PartialEq)] pub enum Value { + Nil, String(String), Integer(i64), Float(f64), -- cgit v1.2.3