summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRyan Leckey <leckey.ryan@gmail.com>2017-01-23 18:58:19 -0800
committerRyan Leckey <leckey.ryan@gmail.com>2017-01-23 18:58:19 -0800
commit40a44a459fde4c4042d5188faedea718a34e2b96 (patch)
tree41dce965b779c29e60504c9f1121286388f0e932
Initial commit.
-rw-r--r--.editorconfig7
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml6
-rw-r--r--README.md86
-rw-r--r--src/lib.rs241
-rw-r--r--src/value.rs143
6 files changed, 485 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..08b88cc
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a9d37c5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+target
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..5222101
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "config"
+version = "0.1.0"
+authors = ["Ryan Leckey <leckey.ryan@gmail.com>"]
+
+[dependencies]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b7d4c64
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+# config-rs
+> Application Configuration for Rust
+
+config-rs is a layered configuration system for Rust applications (including [12-factor]).
+
+[12-factor]: https://12factor.net/config
+
+## Install
+
+```toml
+[dependencies]
+config = { git = "https://github.com/mehcode/config-rs.git" }
+```
+
+## Usage
+
+### Setting Values
+
+Configuration is collected in a series of layers, in order from lowest to highest priority.
+
+1. Explicit Default — `config::set_default`
+2. Source — File, Remote (ETCD, Consul, etc.)
+3. Environment
+4. Explicit Set — `config::set`
+
+#### Defaults
+
+By default, `None` is returned when requesting a configuration value
+that does not exist elsewhere.
+
+Defaults may be established in code.
+
+```rust
+config::set_default("port", 80);
+```
+
+#### Environment
+
+```rust
+// Keep your environment unique and predictable
+config::set_env_prefix("rust");
+
+// Enable environment
+// config::bind_env("port");
+// config::bind_env_to("port", "port");
+config::bind_env_all();
+
+// Environment variables are typically set outside of the application
+std::env::set_var("RUST_PORT", "80");
+std::env::set_var("RUST_PORT2", "602");
+
+config::get_int("port"); //= Some(80)
+config::get_int("port2"); //= Some(602)
+```
+
+#### Source
+
+##### File
+
+Read `${CWD}/Settings.toml` and populate configuration.
+ - `prefix` is used to only pull keys nested under the named key
+ - `required(true)` (default) will cause an error to be returned if the file failed to be read/parsed/etc.
+
+```rust
+config::merge(
+ config::source::File::with_name("Settings")
+ .required(false)
+ .prefix("development")
+).unwrap();
+```
+
+## Getting Values
+
+Values will attempt to convert from their underlying type (from when they were set) when accessed.
+
+ - `config::get::<T>(key: &str) -> T`
+ - `config::get_str(key: &str) -> &str`
+ - `config::get_int(key: &str) -> i64`
+ - `config::get_float(key: &str) -> float`
+ - `config::get_bool(key: &str) -> bool`
+
+```rust
+if config::get("debug").unwrap() {
+ println!("address: {:?}", config::get_int("port"));
+}
+```
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..0be9063
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,241 @@
+#![feature(try_from)]
+
+mod value;
+
+use value::Value;
+
+use std::env;
+use std::convert::TryFrom;
+use std::collections::HashMap;
+
+#[derive(Default)]
+pub struct Config {
+ defaults: HashMap<String, Value>,
+ overrides: HashMap<String, Value>,
+ environ: HashMap<String, Value>,
+}
+
+impl Config {
+ pub fn new() -> Config {
+ Default::default()
+ }
+
+ pub fn set_default<T>(&mut self, key: &str, value: T)
+ where T: Into<Value>
+ {
+ self.defaults.insert(key.into(), value.into());
+ }
+
+ pub fn set<T>(&mut self, key: &str, value: T)
+ where T: Into<Value>
+ {
+ self.overrides.insert(key.into(), value.into());
+ }
+
+ pub fn get<'a, T>(&'a mut self, key: &str) -> Option<T>
+ where T: TryFrom<&'a mut Value>,
+ T: Default
+ {
+ if let Some(value) = self.overrides.get_mut(key) {
+ T::try_from(value).ok()
+ } else if let Ok(value) = env::var(key.to_uppercase()) {
+ // Store the environment variable into an environ
+ // hash map; we want to return references
+
+ // TODO: Key name needs to go through a transform
+ self.environ.insert(key.to_lowercase().into(), value.into());
+
+ T::try_from(self.environ.get_mut(key).unwrap()).ok()
+ } else if let Some(value) = self.defaults.get_mut(key) {
+ T::try_from(value).ok()
+ } else {
+ None
+ }
+ }
+
+ pub fn get_str<'a>(&'a mut self, key: &str) -> Option<&'a str> {
+ self.get(key)
+ }
+
+ pub fn get_int(&mut self, key: &str) -> Option<i64> {
+ self.get(key)
+ }
+
+ pub fn get_float(&mut self, key: &str) -> Option<f64> {
+ self.get(key)
+ }
+
+ pub fn get_bool(&mut self, key: &str) -> Option<bool> {
+ self.get(key)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use std::env;
+
+ // Retrieval of a non-existent key
+ #[test]
+ fn test_not_found() {
+ let mut c = super::Config::new();
+
+ assert_eq!(c.get_int("key"), None);
+ }
+
+ // Environment override
+ #[test]
+ fn test_env_override() {
+ let mut c = super::Config::new();
+
+ c.set_default("key_1", false);
+
+ env::set_var("KEY_1", "1");
+
+ assert_eq!(c.get_bool("key_1"), Some(true));
+ }
+
+ // Explicit override
+ #[test]
+ fn test_default_override() {
+ let mut c = super::Config::new();
+
+ c.set_default("key_1", false);
+ c.set_default("key_2", false);
+
+ assert!(!c.get_bool("key_1").unwrap());
+ assert!(!c.get_bool("key_2").unwrap());
+
+ c.set("key_2", true);
+
+ assert!(!c.get_bool("key_1").unwrap());
+ assert!(c.get_bool("key_2").unwrap());
+ }
+
+ // Storage and retrieval of String values
+ #[test]
+ fn test_str() {
+ let mut c = super::Config::new();
+
+ c.set("key", "value");
+
+ assert_eq!(c.get_str("key").unwrap(), "value");
+ assert!("value" == c.get::<&str>("key").unwrap());
+ }
+
+ // Storage and retrieval of Boolean values
+ #[test]
+ fn test_bool() {
+ let mut c = super::Config::new();
+
+ c.set("key", true);
+
+ assert_eq!(c.get_bool("key").unwrap(), true);
+ assert!(false != c.get("key").unwrap());
+ }
+
+ // Storage and retrieval of Float values
+ #[test]
+ fn test_float() {
+ let mut c = super::Config::new();
+
+ c.set("key", 3.14);
+
+ assert_eq!(c.get_float("key").unwrap(), 3.14);
+ assert!(3.14 >= c.get("key").unwrap());
+ }
+
+ // Storage and retrieval of Integer values
+ #[test]
+ fn test_int() {
+ let mut c = super::Config::new();
+
+ c.set("key", 42);
+
+ assert_eq!(c.get_int("key").unwrap(), 42);
+ assert!(42 == c.get::<i64>("key").unwrap());
+ }
+
+ // Storage of various values and retrieval as String
+ #[test]
+ fn test_retrieve_str() {
+ let mut c = super::Config::new();
+
+ c.set("key_1", 115);
+ c.set("key_2", 1.23);
+ c.set("key_3", false);
+
+ assert_eq!(c.get_str("key_1"), Some("115"));
+ assert_eq!(c.get_str("key_2"), Some("1.23"));
+ assert_eq!(c.get_str("key_3"), Some("false"));
+ }
+
+ // Storage of various values and retrieval as Integer
+ #[test]
+ fn test_retrieve_int() {
+ let mut c = super::Config::new();
+
+ c.set("key_1", "121");
+ c.set("key_2", 5.12);
+ c.set("key_3", 5.72);
+ c.set("key_4", false);
+ c.set("key_5", true);
+ c.set("key_6", "asga");
+
+ assert_eq!(c.get_int("key_1"), Some(121));
+ assert_eq!(c.get_int("key_2"), Some(5));
+ assert_eq!(c.get_int("key_3"), Some(6));
+ assert_eq!(c.get_int("key_4"), Some(0));
+ assert_eq!(c.get_int("key_5"), Some(1));
+ assert_eq!(c.get_int("key_6"), None);
+ }
+
+ // Storage of various values and retrieval as Float
+ #[test]
+ fn test_retrieve_float() {
+ let mut c = super::Config::new();
+
+ c.set("key_1", "121");
+ c.set("key_2", "121.512");
+ c.set("key_3", 5);
+ c.set("key_4", false);
+ c.set("key_5", true);
+ c.set("key_6", "asga");
+
+ assert_eq!(c.get_float("key_1"), Some(121.0));
+ assert_eq!(c.get_float("key_2"), Some(121.512));
+ assert_eq!(c.get_float("key_3"), Some(5.0));
+ assert_eq!(c.get_float("key_4"), Some(0.0));
+ assert_eq!(c.get_float("key_5"), Some(1.0));
+ assert_eq!(c.get_float("key_6"), None);
+ }
+
+ // Storage of various values and retrieval as Boolean
+ #[test]
+ fn test_retrieve_bool() {
+ let mut c = super::Config::new();
+
+ c.set("key_1", "121");
+ c.set("key_2", "1");
+ c.set("key_3", "0");
+ c.set("key_4", "true");
+ c.set("key_5", "");
+ c.set("key_6", 51);
+ c.set("key_7", 0);
+ c.set("key_8", 12.12);
+ c.set("key_9", 1.0);
+ c.set("key_10", 0.0);
+ c.set("key_11", "asga");
+
+ assert_eq!(c.get_bool("key_1"), Some(false));
+ assert_eq!(c.get_bool("key_2"), Some(true));
+ assert_eq!(c.get_bool("key_3"), Some(false));
+ assert_eq!(c.get_bool("key_4"), Some(true));
+ assert_eq!(c.get_bool("key_5"), Some(false));
+ assert_eq!(c.get_bool("key_6"), Some(true));
+ assert_eq!(c.get_bool("key_7"), Some(false));
+ assert_eq!(c.get_bool("key_8"), Some(true));
+ assert_eq!(c.get_bool("key_9"), Some(true));
+ assert_eq!(c.get_bool("key_10"), Some(false));
+ assert_eq!(c.get_bool("key_11"), Some(false));
+ }
+}
diff --git a/src/value.rs b/src/value.rs
new file mode 100644
index 0000000..295c7c0
--- /dev/null
+++ b/src/value.rs
@@ -0,0 +1,143 @@
+use std::convert::{From, TryFrom};
+
+// Variant for a configuration Value
+// The additional Option<String> is used for the textual representation of the
+// underlying type (to cache the string generation) but only if requested.
+pub enum Value {
+ String(String),
+ Integer(i64, Option<String>),
+ Float(f64, Option<String>),
+ Boolean(bool, Option<String>),
+}
+
+// Conversion from type into variant
+impl From<String> for Value {
+ fn from(value: String) -> Value {
+ Value::String(value)
+ }
+}
+
+impl<'a> From<&'a str> for Value {
+ fn from(value: &'a str) -> Value {
+ Value::String(value.into())
+ }
+}
+
+impl From<i64> for Value {
+ fn from(value: i64) -> Value {
+ Value::Integer(value, None)
+ }
+}
+
+impl From<f64> for Value {
+ fn from(value: f64) -> Value {
+ Value::Float(value, None)
+ }
+}
+
+impl From<bool> for Value {
+ fn from(value: bool) -> Value {
+ Value::Boolean(value, None)
+ }
+}
+
+// Conversion from variant into type
+impl<'a> TryFrom<&'a mut Value> for &'a str {
+ type Err = ();
+
+ fn try_from(value: &mut Value) -> Result<&str, ()> {
+ // When converting a non-string value into a string;
+ // cache the conversion and return a reference
+
+ if let Value::String(ref value) = *value {
+ Ok(value)
+ } else if let Value::Integer(value, ref mut text) = *value {
+ if let Some(ref text) = *text {
+ Ok(text)
+ } else {
+ *text = Some(value.to_string());
+
+ Ok(text.as_ref().unwrap())
+ }
+ } else if let Value::Float(value, ref mut text) = *value {
+ if let Some(ref text) = *text {
+ Ok(text)
+ } else {
+ *text = Some(value.to_string());
+
+ Ok(text.as_ref().unwrap())
+ }
+ } else if let Value::Boolean(value, ref mut text) = *value {
+ if let Some(ref text) = *text {
+ Ok(text)
+ } else {
+ *text = Some(value.to_string());
+
+ Ok(text.as_ref().unwrap())
+ }
+ } else {
+ Err(())
+ }
+ }
+}
+
+impl<'a> TryFrom<&'a mut Value> for i64 {
+ type Err = ();
+
+ fn try_from(value: &mut Value) -> Result<i64, ()> {
+ if let Value::Integer(value, ..) = *value {
+ Ok(value)
+ } else if let Value::String(ref value) = *value {
+ value.parse().map_err(|_| {
+ // Drop specific error
+ })
+ } else if let Value::Boolean(value, ..) = *value {
+ Ok(if value { 1 } else { 0 })
+ } else if let Value::Float(value, ..) = *value {
+ Ok(value.round() as i64)
+ } else {
+ Err(())
+ }
+ }
+}
+
+impl<'a> TryFrom<&'a mut Value> for f64 {
+ type Err = ();
+
+ fn try_from(value: &mut Value) -> Result<f64, ()> {
+ if let Value::Float(value, ..) = *value {
+ Ok(value)
+ } else if let Value::String(ref value) = *value {
+ value.parse().map_err(|_| {
+ // Drop specific error
+ })
+ } else if let Value::Integer(value, ..) = *value {
+ Ok(value as f64)
+ } else if let Value::Boolean(value, ..) = *value {
+ Ok(if value { 1.0 } else { 0.0 })
+ } else {
+ Err(())
+ }
+ }
+}
+
+impl<'a> TryFrom<&'a mut Value> for bool {
+ type Err = ();
+
+ fn try_from(value: &mut Value) -> Result<bool, ()> {
+ if let Value::Boolean(value, ..) = *value {
+ Ok(value)
+ } else if let Value::String(ref value) = *value {
+ Ok(match value.to_lowercase().as_ref() {
+ "1" | "true" | "on" | "yes" => true,
+ _ => false,
+ })
+ } else if let Value::Integer(value, ..) = *value {
+ Ok(value != 0)
+ } else if let Value::Float(value, ..) = *value {
+ Ok(value != 0.0)
+ } else {
+ Err(())
+ }
+ }
+}