diff options
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | README.md | 15 | ||||
-rw-r--r-- | src/config.rs | 132 | ||||
-rw-r--r-- | src/env.rs | 4 | ||||
-rw-r--r-- | src/file/json.rs | 8 | ||||
-rw-r--r-- | src/file/mod.rs | 4 | ||||
-rw-r--r-- | src/file/toml.rs | 4 | ||||
-rw-r--r-- | src/lib.rs | 5 | ||||
-rw-r--r-- | src/path.rs | 134 | ||||
-rw-r--r-- | src/value.rs | 37 |
10 files changed, 323 insertions, 22 deletions
@@ -15,6 +15,8 @@ json = ["serde_json"] yaml = ["yaml-rust"] [dependencies] +nom = "^2.1" + toml = { version = "0.2.1", optional = true } serde_json = { version = "0.9", optional = true } yaml-rust = { version = "0.3.5", optional = true } @@ -5,12 +5,13 @@ > Layered configuration system for Rust applications (with strong support for [12-factor] applications). [12-factor]: https://12factor.net/config - + - Set defaults - Set explicit values (to programmatically override) - Read from [JSON] and [TOML] files - Read from environment - Loosely typed — Configuration values may be read in any supported type, as long as there exists a reasonable conversion + - Access nested fields using a formatted path — Uses a subset of JSONPath. Currently supports the child ( `redis.port` ) and subscript operators ( `databases[0].name` ). [JSON]: https://github.com/serde-rs/json [TOML]: https://github.com/toml-lang/toml @@ -29,7 +30,7 @@ config = "0.2" ## Usage -Configuration is gathered by building a `Source` and then merging that source into the +Configuration is gathered by building a `Source` and then merging that source into the current state of the configuration. ```rust @@ -46,11 +47,11 @@ fn main() { } ``` -Note that in the above example the calls to `config::merge` could have -been re-ordered to influence the priority as each successive merge +Note that in the above example the calls to `config::merge` could have +been re-ordered to influence the priority as each successive merge is evaluated on top of the previous. -Configuration values can be retrieved with a call to `config::get` and then +Configuration values can be retrieved with a call to `config::get` and then coerced into a type with `as_*`. ```toml @@ -72,14 +73,14 @@ fn main() { } ``` -See the [examples](https://github.com/mehcode/config-rs/tree/master/examples) for +See the [examples](https://github.com/mehcode/config-rs/tree/master/examples) for more usage information. ## Roadmap - [ ] Read from remote source — [etcd](https://github.com/jimmycuadra/rust-etcd) and [consul](https://github.com/stusmall/consul-rust) - [ ] Read from YAML files - [ ] Read from Libconfig files - + All suggestions are welcome. Please make an issue. ## License diff --git a/src/config.rs b/src/config.rs index 126d31b..6c81244 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use value::Value; use source::{Source, SourceBuilder}; +use path; use std::error::Error; use std::fmt; @@ -213,8 +214,55 @@ impl Config { Ok(()) } - pub fn get<'a>(&'a self, key: &str) -> Option<&'a Value> { - self.cache.get(key) + // Child ( Child ( Identifier( "x" ), "y" ), "z" ) + fn path_get<'a, 'b>(&'a self, expr: path::Expression) -> Option<&'a Value> { + match expr { + path::Expression::Identifier(text) => { + self.cache.get(&text) + } + + path::Expression::Child(expr, member) => { + match self.path_get(*expr) { + Some(&Value::Table(ref table)) => { + table.get(&member) + } + + _ => None + } + } + + path::Expression::Subscript(expr, mut index) => { + match self.path_get(*expr) { + Some(&Value::Array(ref array)) => { + let len = array.len() as i32; + + if index < 0 { + index = len + index; + } + + if index < 0 || index >= len { + None + } else { + Some(&array[index as usize]) + } + } + + _ => None + } + } + } + } + + pub fn get<'a>(&'a self, key_path: &str) -> Option<&'a Value> { + let key_expr: path::Expression = match key_path.parse() { + Ok(expr) => expr, + Err(_) => { + // TODO: Log warning here + return None; + } + }; + + self.path_get(key_expr) } pub fn get_str<'a>(&'a self, key: &str) -> Option<Cow<'a, str>> { @@ -236,6 +284,10 @@ impl Config { pub fn get_map<'a>(&'a self, key: &str) -> Option<&'a HashMap<String, Value>> { self.get(key).and_then(Value::as_map) } + + pub fn get_slice<'a>(&'a self, key: &str) -> Option<&'a [Value]> { + self.get(key).and_then(Value::as_slice) + } } #[cfg(test)] @@ -392,9 +444,41 @@ mod test { assert_eq!(c.get_bool("key_11"), None); } - // Deep merge of tables #[test] - fn test_merge() { + fn test_slice() { + let mut c = Config::new(); + + c.set("values", vec![ + Value::Integer(10), + Value::Integer(325), + Value::Integer(12), + ]).unwrap(); + + let values = c.get_slice("values").unwrap(); + + assert_eq!(values.len(), 3); + assert_eq!(values[1].as_int(), Some(325)); + } + + #[test] + fn test_slice_into() { + let mut c = Config::new(); + + c.set("values", vec![ + 10, + 325, + 12, + ]).unwrap(); + + let values = c.get_slice("values").unwrap(); + + assert_eq!(values.len(), 3); + assert_eq!(values[1].as_int(), Some(325)); + + } + + #[test] + fn test_map() { let mut c = Config::new(); { @@ -428,4 +512,44 @@ mod test { assert_eq!(m.get("db").unwrap().as_str().unwrap(), "1"); } } + + #[test] + fn test_path() { + use file::{File, FileFormat}; + + let mut c = Config::new(); + + c.merge(File::from_str(r#" + [redis] + address = "localhost:6379" + + [[databases]] + name = "test_db" + options = { trace = true } + "#, FileFormat::Toml)).unwrap(); + + assert_eq!(c.get_str("redis.address").unwrap(), "localhost:6379"); + assert_eq!(c.get_str("databases[0].name").unwrap(), "test_db"); + assert_eq!(c.get_str("databases[0].options.trace").unwrap(), "true"); + } + + #[test] + fn test_map_into() { + let mut c = Config::new(); + + { + let mut m = HashMap::new(); + m.insert("port".into(), 6379); + m.insert("db".into(), 2); + + 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("db").unwrap().as_int().unwrap(), 2); + } + } } @@ -34,7 +34,9 @@ impl source::Source for Environment { // Make prefix pattern let prefix_pat = if let Some(ref prefix) = self.prefix { Some(prefix.clone() + "_".into()) - } else { None }; + } else { + None + }; for (key, value) in env::vars() { let mut key = key.to_string(); diff --git a/src/file/json.rs b/src/file/json.rs index d193da4..86612c9 100644 --- a/src/file/json.rs +++ b/src/file/json.rs @@ -21,9 +21,7 @@ impl Content { fn from_json_value(value: &serde_json::Value) -> Value { match *value { - serde_json::Value::String(ref value) => { - Value::String(value.clone()) - } + serde_json::Value::String(ref value) => Value::String(value.clone()), serde_json::Value::Number(ref value) => { if let Some(value) = value.as_i64() { @@ -58,7 +56,9 @@ fn from_json_value(value: &serde_json::Value) -> Value { } // TODO: What's left is JSON Null; how should we handle that? - _ => { unimplemented!(); } + _ => { + unimplemented!(); + } } } diff --git a/src/file/mod.rs b/src/file/mod.rs index 5207508..8f9578d 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -171,7 +171,7 @@ impl File<FileSourceFile> { source: FileSourceFile { name: name.into(), path: None, - } + }, } } } @@ -189,7 +189,7 @@ impl<T: FileSource> File<T> { impl File<FileSourceFile> { pub fn path(self, path: &str) -> Self { - File { source: FileSourceFile { path: Some(path.into()), ..self.source } , ..self } + File { source: FileSourceFile { path: Some(path.into()), ..self.source }, ..self } } pub fn namespace(self, namespace: &str) -> Self { diff --git a/src/file/toml.rs b/src/file/toml.rs index 4de23ef..28a1507 100644 --- a/src/file/toml.rs +++ b/src/file/toml.rs @@ -47,7 +47,9 @@ fn from_toml_value(value: &toml::Value) -> Value { Value::Array(l) } - _ => { unimplemented!(); } + _ => { + unimplemented!(); + } } } @@ -1,6 +1,7 @@ #![feature(drop_types_in_const)] #![allow(unknown_lints)] +#![feature(trace_macros)] //! Configuration is gathered by building a `Source` and then merging that source into the //! current state of the configuration. @@ -40,6 +41,9 @@ //! See the [examples](https://github.com/mehcode/config-rs/tree/master/examples) for //! more usage information. +#[macro_use] +extern crate nom; + #[cfg(feature = "toml")] extern crate toml; @@ -53,6 +57,7 @@ mod value; mod source; mod file; mod env; +mod path; mod config; use std::error::Error; diff --git a/src/path.rs b/src/path.rs new file mode 100644 index 0000000..b6d3b10 --- /dev/null +++ b/src/path.rs @@ -0,0 +1,134 @@ +use nom::*; +use std::str::{FromStr, from_utf8}; + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum Expression { + Identifier(String), + Child(Box<Expression>, String), + Subscript(Box<Expression>, i32), +} + +named!(ident_<String>, + map!( + map_res!(is_a!( + "abcdefghijklmnopqrstuvwxyz \ + ABCDEFGHIJKLMNOPQRSTUVWXYZ \ + 0123456789 \ + _-" + ), from_utf8), + |s: &str| { + s.to_string() + } + ) +); + +named!(integer <i32>, + map_res!( + map_res!( + ws!(digit), + from_utf8 + ), + FromStr::from_str + ) +); + +named!(ident<Expression>, map!(ident_, Expression::Identifier)); + +fn postfix(expr: Expression) -> Box<Fn(&[u8]) -> IResult<&[u8], Expression>> { + return Box::new(move |i: &[u8]| { + alt!(i, + do_parse!( + tag!(".") >> + id: ident_ >> + (Expression::Child(Box::new(expr.clone()), id)) + ) | + delimited!( + char!('['), + do_parse!( + negative: opt!(tag!("-")) >> + num: integer >> + (Expression::Subscript( + Box::new(expr.clone()), + num * (if negative.is_none() { 1 } else { -1 }) + )) + ), + char!(']') + ) + ) + }); +} + +fn expr(input: &[u8]) -> IResult<&[u8], Expression> { + match ident(input) { + IResult::Done(mut rem, mut expr) => { + while rem.len() > 0 { + match postfix(expr)(rem) { + IResult::Done(rem_, expr_) => { + rem = rem_; + expr = expr_; + } + + // Forward Incomplete and Error + result @ _ => { + return result; + } + } + } + + IResult::Done(&[], expr) + } + + // Forward Incomplete and Error + result @ _ => result, + } +} + +impl FromStr for Expression { + type Err = ErrorKind; + + fn from_str(s: &str) -> Result<Expression, ErrorKind> { + expr(s.as_bytes()).to_result() + } +} + +#[cfg(test)] +mod test { + use super::*; + use super::Expression::*; + + #[test] + fn test_id() { + let parsed: Expression = "abcd".parse().unwrap(); + assert_eq!(parsed, Identifier("abcd".into())); + } + + #[test] + fn test_id_dash() { + let parsed: Expression = "abcd-efgh".parse().unwrap(); + assert_eq!(parsed, Identifier("abcd-efgh".into())); + } + + #[test] + fn test_child() { + let parsed: Expression = "abcd.efgh".parse().unwrap(); + let expected = Child(Box::new(Identifier("abcd".into())), "efgh".into()); + + assert_eq!(parsed, expected); + } + + #[test] + fn test_subscript() { + let parsed: Expression = "abcd[12]".parse().unwrap(); + let expected = Subscript(Box::new(Identifier("abcd".into())), 12); + + assert_eq!(parsed, expected); + } + + #[test] + fn test_subscript_neg() { + let parsed: Expression = "abcd[-1]".parse().unwrap(); + let expected = Subscript(Box::new(Identifier("abcd".into())), -1); + + assert_eq!(parsed, expected); + } +} diff --git a/src/value.rs b/src/value.rs index 5228d91..1630d85 100644 --- a/src/value.rs +++ b/src/value.rs @@ -77,6 +77,13 @@ impl Value { pub fn as_map(&self) -> Option<&HashMap<String, Value>> { match *self { Value::Table(ref value) => Some(value), + _ => None, + } + } + /// Gets the underlying type as a slice; only works if the type is actually a slice. + pub fn as_slice(&self) -> Option<&[Value]> { + match *self { + Value::Array(ref value) => Some(value), _ => None } } @@ -115,8 +122,32 @@ impl From<bool> for Value { } } -impl From<HashMap<String, Value>> for Value { - fn from(value: HashMap<String, Value>) -> Value { - Value::Table(value) +// impl From<HashMap<String, Value>> for Value { +// fn from(value: HashMap<String, Value>) -> Value { +// Value::Table(value) +// } +// } + +impl<T> From<HashMap<String, T>> for Value where T: Into<Value> { + fn from(values: HashMap<String, T>) -> Value { + let mut r = HashMap::new(); + + for (k, v) in values { + r.insert(k.clone(), v.into()); + } + + Value::Table(r) + } +} + +impl<T> From<Vec<T>> for Value where T: Into<Value> { + fn from(values: Vec<T>) -> Value { + let mut l = Vec::new(); + + for v in values { + l.push(v.into()); + } + + Value::Array(l) } } |