summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml2
-rw-r--r--README.md15
-rw-r--r--src/config.rs132
-rw-r--r--src/env.rs4
-rw-r--r--src/file/json.rs8
-rw-r--r--src/file/mod.rs4
-rw-r--r--src/file/toml.rs4
-rw-r--r--src/lib.rs5
-rw-r--r--src/path.rs134
-rw-r--r--src/value.rs37
10 files changed, 323 insertions, 22 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 46cd9ce..df037f7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 }
diff --git a/README.md b/README.md
index f6aa545..7a386d7 100644
--- a/README.md
+++ b/README.md
@@ -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);
+ }
+ }
}
diff --git a/src/env.rs b/src/env.rs
index 60e020e..dd237db 100644
--- a/src/env.rs
+++ b/src/env.rs
@@ -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!();
+ }
}
}
diff --git a/src/lib.rs b/src/lib.rs
index bec6189..073815f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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)
}
}