diff options
-rw-r--r-- | .editorconfig | 7 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.toml | 6 | ||||
-rw-r--r-- | README.md | 86 | ||||
-rw-r--r-- | src/lib.rs | 241 | ||||
-rw-r--r-- | src/value.rs | 143 |
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(()) + } + } +} |