summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/config.rs234
-rw-r--r--src/file/json.rs16
-rw-r--r--src/file/mod.rs28
-rw-r--r--src/file/toml.rs16
-rw-r--r--src/file/yaml.rs25
-rw-r--r--src/value.rs1
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<String, Value>, map: &HashMap<String, Value>) {
+ 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<String, Value>, 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<String, Value>, key: &str, value: &Value) {
- let key_segments: VecDeque<&str> = key.splitn(2, KEY_DELIM).collect();
+fn require_table(r: &mut HashMap<String, Value>, 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<String, Value>, 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<String, Value>, map: &HashMap<String, Value>) {
- for (key, value) in map {
- merge_in(r, key, value);
- }
+fn path_set_str(root: &mut HashMap<String, Value>, 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<Value>
{
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<Value>
{
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<Source>, Box<Error>> {
+ pub fn parse(text: &str, namespace: Option<&String>) -> Result<Box<Source>, Box<Error>> {
// 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<Source>, Box<Error>> {
+ fn parse(&self, text: &str, namespace: Option<&String>) -> Result<Box<Source>, Box<Error>> {
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<Source>, Box<Error>>;
+ fn try_build(&self, format: FileFormat, namespace: Option<&String>) -> Result<Box<Source>, Box<Error>>;
}
pub struct FileSourceString(String);
impl FileSource for FileSourceString {
- fn try_build(&self, format: FileFormat) -> Result<Box<Source>, Box<Error>> {
- format.parse(&self.0)
+ fn try_build(&self, format: FileFormat, namespace: Option<&String>) -> Result<Box<Source>, Box<Error>> {
+ format.parse(&self.0, namespace)
}
}
@@ -123,7 +123,7 @@ impl FileSourceFile {
}
impl FileSource for FileSourceFile {
- fn try_build(&self, format: FileFormat) -> Result<Box<Source>, Box<Error>> {
+ fn try_build(&self, format: FileFormat, namespace: Option<&String>) -> Result<Box<Source>, Box<Error>> {
// 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<T: FileSource> File<T> {
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<Source>, Box<Error>> {
- self.source.try_build(self.format)
+ self.source.try_build(self.format, self.namespace.as_ref())
}
}
@@ -191,10 +195,6 @@ impl File<FileSourceFile> {
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<T: FileSource> SourceBuilder for File<T> {
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<Source>, Box<Error>> {
+ pub fn parse(text: &str, namespace: Option<&String>) -> Result<Box<Source>, Box<Error>> {
// 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<Source>, Box<Error>> {
+ pub fn parse(text: &str, namespace: Option<&String>) -> Result<Box<Source>, Box<Error>> {
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),