From 2dc6a74b84825f65142c1fa7d3e67cd4f35ee3cb Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 8 Mar 2017 11:09:37 -0800 Subject: Initial work on deep serde integration --- Cargo.toml | 30 +- examples/basic/Cargo.toml | 3 +- examples/basic/src/main.rs | 50 +-- examples/file-json/Cargo.toml | 3 +- examples/file-toml/Cargo.toml | 5 +- examples/file-toml/Settings.toml | 3 +- examples/file-toml/src/main.rs | 18 +- examples/file-yaml/Cargo.toml | 3 +- lib/Cargo.toml | 24 ++ lib/src/config.rs | 103 ++++++ lib/src/de.rs | 257 ++++++++++++++ lib/src/error.rs | 154 +++++++++ lib/src/file/format/mod.rs | 60 ++++ lib/src/file/format/toml.rs | 64 ++++ lib/src/file/mod.rs | 75 +++++ lib/src/file/source/file.rs | 129 +++++++ lib/src/file/source/mod.rs | 12 + lib/src/file/source/string.rs | 21 ++ lib/src/lib.rs | 25 ++ lib/src/path/mod.rs | 43 +++ lib/src/path/parser.rs | 120 +++++++ lib/src/source.rs | 9 + lib/src/value.rs | 215 ++++++++++++ src/config.rs | 709 --------------------------------------- src/env.rs | 60 ---- src/file/json.rs | 87 ----- src/file/mod.rs | 219 ------------ src/file/nil.rs | 13 - src/file/toml.rs | 74 ---- src/file/yaml.rs | 101 ------ src/lib.rs | 89 ----- src/path.rs | 134 -------- src/source.rs | 12 - src/value.rs | 157 --------- 34 files changed, 1371 insertions(+), 1710 deletions(-) create mode 100644 lib/Cargo.toml create mode 100644 lib/src/config.rs create mode 100644 lib/src/de.rs create mode 100644 lib/src/error.rs create mode 100644 lib/src/file/format/mod.rs create mode 100644 lib/src/file/format/toml.rs create mode 100644 lib/src/file/mod.rs create mode 100644 lib/src/file/source/file.rs create mode 100644 lib/src/file/source/mod.rs create mode 100644 lib/src/file/source/string.rs create mode 100644 lib/src/lib.rs create mode 100644 lib/src/path/mod.rs create mode 100644 lib/src/path/parser.rs create mode 100644 lib/src/source.rs create mode 100644 lib/src/value.rs delete mode 100644 src/config.rs delete mode 100644 src/env.rs delete mode 100644 src/file/json.rs delete mode 100644 src/file/mod.rs delete mode 100644 src/file/nil.rs delete mode 100644 src/file/toml.rs delete mode 100644 src/file/yaml.rs delete mode 100644 src/lib.rs delete mode 100644 src/path.rs delete mode 100644 src/source.rs delete mode 100644 src/value.rs diff --git a/Cargo.toml b/Cargo.toml index 0f9fad1..99ff591 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,8 @@ -[package] -name = "config" -version = "0.4.1" -description = "Layered configuration system for Rust applications." -homepage = "https://github.com/mehcode/config-rs" -repository = "https://github.com/mehcode/config-rs" -readme = "README.md" -keywords = ["config", "configuration", "settings", "env", "environment"] -authors = ["Ryan Leckey "] -license = "MIT/Apache-2.0" - -[features] -default = ["toml"] -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 } +[workspace] +members = [ + "lib", + "examples/basic", + # "examples/file-json", + "examples/file-toml", + # "examples/file-yaml", +] diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index 7ede162..25c3f4d 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "basic" version = "0.1.0" +workspace = "../../" [dependencies] -config = { path = "../..", default-features = false } +config = { path = "../../lib" } diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 2f947d8..49059ef 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -1,33 +1,35 @@ extern crate config; +use config::*; + fn main() { - let mut c = config::Config::new(); + let mut c = Config::default(); - // Set defaults for `window.width` and `window.height` - c.set_default("window.title", "Basic").unwrap(); - c.set_default("window.width", 640).unwrap(); - c.set_default("window.height", 480).unwrap(); - c.set_default("debug", true).unwrap(); + // // Set defaults for `window.width` and `window.height` + // c.set_default("window.title", "Basic").unwrap(); + // c.set_default("window.width", 640).unwrap(); + // c.set_default("window.height", 480).unwrap(); + // c.set_default("debug", true).unwrap(); - // Note that you can retrieve the stored values as any type as long - // as there exists a reasonable conversion - println!("window.title : {:?}", c.get_str("window.title")); - println!("window.width : {:?}", c.get_str("window.width")); - println!("window.width : {:?}", c.get_int("window.width")); - println!("debug : {:?}", c.get_bool("debug")); - println!("debug : {:?}", c.get_str("debug")); - println!("debug : {:?}", c.get_int("debug")); + // // Note that you can retrieve the stored values as any type as long + // // as there exists a reasonable conversion + // println!("window.title : {:?}", c.get_str("window.title")); + // println!("window.width : {:?}", c.get_str("window.width")); + // println!("window.width : {:?}", c.get_int("window.width")); + // println!("debug : {:?}", c.get_bool("debug")); + // println!("debug : {:?}", c.get_str("debug")); + // println!("debug : {:?}", c.get_int("debug")); - // Attempting to get a value as a type that cannot be reasonably - // converted to will return None - println!("window.title : {:?}", c.get_bool("window.title")); + // // Attempting to get a value as a type that cannot be reasonably + // // converted to will return None + // println!("window.title : {:?}", c.get_bool("window.title")); - // Instead of using a get_* function you can get the variant - // directly - println!("debug : {:?}", c.get("debug")); - println!("debug : {:?}", - c.get("debug").unwrap().into_int()); + // // Instead of using a get_* function you can get the variant + // // directly + // println!("debug : {:?}", c.get("debug")); + // println!("debug : {:?}", + // c.get("debug").unwrap().into_int()); - // Attempting to get a value that does not exist will return None - println!("not-found : {:?}", c.get("not-found")); + // // Attempting to get a value that does not exist will return None + // println!("not-found : {:?}", c.get("not-found")); } diff --git a/examples/file-json/Cargo.toml b/examples/file-json/Cargo.toml index 7223f35..1e8765e 100644 --- a/examples/file-json/Cargo.toml +++ b/examples/file-json/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "file-json" version = "0.1.0" +workspace = "../../" [dependencies] -config = { path = "../..", default-features = false, features = ["json"] } +config = { path = "../../lib", default-features = false, features = ["json"] } diff --git a/examples/file-toml/Cargo.toml b/examples/file-toml/Cargo.toml index 6f45799..0501895 100644 --- a/examples/file-toml/Cargo.toml +++ b/examples/file-toml/Cargo.toml @@ -1,6 +1,9 @@ [package] name = "file-toml" version = "0.1.0" +workspace = "../../" [dependencies] -config = { path = "../.." } +config = { path = "../../lib", features = ["toml"] } +serde = "^0.9" +serde_derive = "^0.9" diff --git a/examples/file-toml/Settings.toml b/examples/file-toml/Settings.toml index 28c5fc6..21fa1e3 100644 --- a/examples/file-toml/Settings.toml +++ b/examples/file-toml/Settings.toml @@ -1,3 +1,4 @@ -debug = false +debug = true pi = 3.14159 weight = 150 +location = { x = 10, y = 30 } diff --git a/examples/file-toml/src/main.rs b/examples/file-toml/src/main.rs index 85db701..ddca412 100644 --- a/examples/file-toml/src/main.rs +++ b/examples/file-toml/src/main.rs @@ -1,12 +1,22 @@ extern crate config; +#[macro_use] +extern crate serde_derive; + +#[derive(Debug, Deserialize)] +struct Point { x: i64, y: i64 } + fn main() { - let mut c = config::Config::new(); + let mut c = config::Config::default(); // Read configuration from "Settings.toml" c.merge(config::File::new("Settings", config::FileFormat::Toml)).unwrap(); - println!("debug = {:?}", c.get("debug")); - println!("pi = {:?}", c.get("pi")); - println!("weight = {:?}", c.get("weight")); + // Simple key access to values + println!("debug = {}", c.get::("debug").unwrap()); + println!("pi = {}", c.get::("pi").unwrap()); + println!("weight = {}", c.get::("weight").unwrap()); + println!("location = {:?}", c.get::("location").unwrap()); + // println!("location.x = {}", c.get::("location.x").unwrap()); + // println!("location.y = {}", c.get::("location.y").unwrap()); } diff --git a/examples/file-yaml/Cargo.toml b/examples/file-yaml/Cargo.toml index 70176b1..4570078 100644 --- a/examples/file-yaml/Cargo.toml +++ b/examples/file-yaml/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "file-yaml" version = "0.1.0" +workspace = "../../" [dependencies] -config = { path = "../..", default-features = false, features = ["yaml"] } +config = { path = "../../lib", default-features = false, features = ["yaml"] } diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..6b04a8e --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "config" +version = "0.5.0-pre" +description = "Layered configuration system for Rust applications." +homepage = "https://github.com/mehcode/config-rs" +repository = "https://github.com/mehcode/config-rs" +readme = "README.md" +keywords = ["config", "configuration", "settings", "env", "environment"] +authors = ["Ryan Leckey "] +license = "MIT/Apache-2.0" +workspace = "../" + +[features] +default = ["toml", "json", "yaml"] +json = ["serde_json"] +yaml = ["yaml-rust"] + +[dependencies] +serde = "^0.9" +nom = "^2.1" + +toml = { version = "^0.3", optional = true } +serde_json = { version = "^0.9", optional = true } +yaml-rust = { version = "^0.3.5", optional = true } diff --git a/lib/src/config.rs b/lib/src/config.rs new file mode 100644 index 0000000..0767f87 --- /dev/null +++ b/lib/src/config.rs @@ -0,0 +1,103 @@ +use std::collections::HashMap; +use serde::de::Deserialize; + +use error::*; +use source::Source; +use value::Value; +use path; + +enum ConfigKind { + // A mutable configuration. This is the default. + Mutable { + defaults: HashMap, + overrides: HashMap, + sources: Vec>, + }, + + // A frozen configuration. + // Configuration can no longer be mutated. + Frozen, +} + +impl Default for ConfigKind { + fn default() -> Self { + ConfigKind::Mutable { + defaults: HashMap::new(), + overrides: HashMap::new(), + sources: Vec::new(), + } + } +} + +/// A prioritized configuration repository. It maintains a set of +/// configuration sources, fetches values to populate those, and provides +/// them according to the source's priority. +#[derive(Default)] +pub struct Config { + kind: ConfigKind, + + /// Root of the cached configuration. + pub cache: Value, +} + +impl Config { + /// Merge in a configuration property source. + pub fn merge(&mut self, source: T) -> Result<()> + where T: 'static, T: Source + Send + Sync + { + match self.kind { + ConfigKind::Mutable { ref mut sources, .. } => { + sources.push(Box::new(source)); + } + + ConfigKind::Frozen => { + return Err(ConfigError::Frozen); + } + } + + self.refresh() + } + + /// Refresh the configuration cache with fresh + /// data from added sources. + /// + /// Configuration is automatically refreshed after a mutation + /// operation (`set`, `merge`, `set_default`, etc.). + pub fn refresh(&mut self) -> Result<()> { + self.cache = match self.kind { + ConfigKind::Mutable { ref overrides, ref sources, ref defaults } => { + let mut cache = Value::new(None, HashMap::::new()); + + // HACK! + cache = sources[0].collect()?; + + cache + } + + ConfigKind::Frozen => { + return Err(ConfigError::Frozen); + } + }; + + Ok(()) + } + + pub fn get(&self, key: &str) -> Result { + // Parse the key into a path expression + let expr: path::Expression = key.to_lowercase().parse()?; + + // Traverse the cache using the path to (possibly) retrieve a value + let value = expr.get(&self.cache).cloned(); + + match value { + Some(value) => { + // Deserialize the received value into the requested type + T::deserialize(value) + } + + None => { + Err(ConfigError::NotFound(key.into())) + } + } + } +} diff --git a/lib/src/de.rs b/lib/src/de.rs new file mode 100644 index 0000000..a59d950 --- /dev/null +++ b/lib/src/de.rs @@ -0,0 +1,257 @@ +use serde::de; +use value::{Value, ValueKind}; +use error::*; +use std::iter::Peekable; +use std::collections::HashMap; +use std::collections::hash_map::Drain; + +impl de::Deserializer for Value { + type Error = ConfigError; + + #[inline] + fn deserialize(self, visitor: V) -> Result { + // Deserialize based on the underlying type + match self.kind { + ValueKind::Boolean(value) => { + visitor.visit_bool(value) + } + + ValueKind::Table(map) => { + visitor.visit_map(MapVisitor::new(map)) + } + + _ => { unimplemented!(); } + } + } + + #[inline] + fn deserialize_bool(self, visitor: V) -> Result { + visitor.visit_bool(self.into_bool()?) + } + + #[inline] + fn deserialize_u8(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_u16(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_u32(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_u64(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_i8(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_i16(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_i32(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_i64(self, visitor: V) -> Result { + visitor.visit_i64(self.into_int()?) + } + + #[inline] + fn deserialize_f32(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_f64(self, visitor: V) -> Result { + visitor.visit_f64(self.into_float()?) + } + + #[inline] + fn deserialize_char(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_str(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_string(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_bytes(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_byte_buf(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_option(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_unit(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_unit_struct(self, + name: &'static str, + visitor: V) + -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_newtype_struct(self, + name: &'static str, + visitor: V) + -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_seq(self, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_seq_fixed_size(self, + len: usize, + visitor: V) + -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_tuple(self, len: usize, visitor: V) -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_tuple_struct(self, + name: &'static str, + len: usize, + visitor: V) + -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_enum(self, + name: &'static str, + variants: &'static [&'static str], + visitor: V) + -> Result { + unimplemented!(); + } + + #[inline] + fn deserialize_ignored_any(self, visitor: V) -> Result { + unimplemented!(); + } + + forward_to_deserialize! { + map + struct + struct_field + } +} + +struct StrDeserializer<'a>(&'a str); + +impl<'a> de::Deserializer for StrDeserializer<'a> { + type Error = ConfigError; + + #[inline] + fn deserialize(self, visitor: V) -> Result { + visitor.visit_str(self.0) + } + + forward_to_deserialize! { + bool + u8 + u16 + u32 + u64 + i8 + i16 + i32 + i64 + f32 + f64 + char + str + string + bytes + byte_buf + option + unit + unit_struct + newtype_struct + seq + seq_fixed_size + tuple + tuple_struct + map + struct + struct_field + enum + ignored_any + } +} + +struct MapVisitor { + elements: Vec<(String, Value)>, + index: usize, +} + +impl MapVisitor { + fn new(mut table: HashMap) -> MapVisitor { + MapVisitor { elements: table.drain().collect(), index: 0 } + } +} + +impl de::MapVisitor for MapVisitor { + type Error = ConfigError; + + fn visit_key_seed(&mut self, seed: K) -> Result> + where K: de::DeserializeSeed, + { + if self.index >= self.elements.len() { + return Ok(None); + } + + let ref key_s = self.elements[0].0; + let key_de = StrDeserializer(&key_s); + let key = de::DeserializeSeed::deserialize(seed, key_de)?; + + Ok(Some(key)) + } + + fn visit_value_seed(&mut self, seed: V) -> Result + where V: de::DeserializeSeed, + { + de::DeserializeSeed::deserialize(seed, self.elements.remove(0).1) + } +} diff --git a/lib/src/error.rs b/lib/src/error.rs new file mode 100644 index 0000000..390057f --- /dev/null +++ b/lib/src/error.rs @@ -0,0 +1,154 @@ +use std::error::Error; +use std::result; +use std::fmt; +use serde::de; +use nom; + +#[derive(Debug)] +pub enum Unexpected { + Bool(bool), + Integer(i64), + Float(f64), + Str(String), + Unit, + Seq, + Map +} + +impl fmt::Display for Unexpected { + fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { + match *self { + Unexpected::Bool(b) => write!(f, "boolean `{}`", b), + Unexpected::Integer(i) => write!(f, "integer `{}`", i), + Unexpected::Float(v) => write!(f, "floating point `{}`", v), + Unexpected::Str(ref s) => write!(f, "string {:?}", s), + Unexpected::Unit => write!(f, "unit value"), + Unexpected::Seq => write!(f, "sequence"), + Unexpected::Map => write!(f, "map"), + } + } +} + +/// Represents all possible errors that can occur when working with +/// configuration. +pub enum ConfigError { + /// Configuration is frozen and no further mutations can be made. + Frozen, + + /// Configuration property was not found + NotFound(String), + + /// Configuration path could not be parsed. + PathParse(nom::ErrorKind), + + /// Configuration could not be parsed from file. + FileParse { uri: Option, cause: Box }, + + /// Value could not be converted into the requested type. + Type { + origin: Option, + unexpected: Unexpected, + expected: &'static str, + }, + + /// Custom message + Message(String), + + /// Unadorned error from a foreign source. + Foreign(Box), +} + +impl ConfigError { + // FIXME: pub(crate) + #[doc(hidden)] + pub fn invalid_type(origin: Option, unexpected: Unexpected, expected: &'static str) -> ConfigError { + ConfigError::Type { + origin: origin, + unexpected: unexpected, + expected: expected + } + } +} + +/// Alias for a `Result` with the error type set to `ConfigError`. +pub type Result = result::Result; + +// Forward Debug to Display for readable panic! messages +impl fmt::Debug for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", *self) + } +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ConfigError::Frozen | ConfigError::PathParse(_) => { + write!(f, "{}", self.description()) + } + + ConfigError::Message(ref s) => { + write!(f, "{}", s) + } + + ConfigError::Foreign(ref cause) => { + write!(f, "{}", cause) + } + + ConfigError::NotFound(ref key) => { + write!(f, "configuration property {:?} not found", key) + } + + ConfigError::Type { ref origin, ref unexpected, expected } => { + write!(f, "invalid type: {}, expected {}", + unexpected, expected)?; + + if let Some(ref origin) = *origin { + write!(f, " in {}", origin)?; + } + + Ok(()) + } + + ConfigError::FileParse { ref cause, ref uri } => { + write!(f, "{}", cause)?; + + if let Some(ref uri) = *uri { + write!(f, " in {}", uri)?; + } + + Ok(()) + } + } + } +} + +impl Error for ConfigError { + fn description(&self) -> &str { + match *self { + ConfigError::Frozen => "configuration is frozen", + ConfigError::NotFound(_) => "configuration property not found", + ConfigError::Type { .. } => "invalid type", + ConfigError::Foreign(ref cause) => cause.description(), + ConfigError::FileParse { ref cause, .. } => cause.description(), + ConfigError::PathParse(ref kind) => kind.description(), + + _ => "configuration error", + } + } + + fn cause(&self) -> Option<&Error> { + match *self { + ConfigError::Foreign(ref cause) => Some(cause.as_ref()), + ConfigError::FileParse { ref cause, .. } => Some(cause.as_ref()), + + _ => None + } + } +} + +impl de::Error for ConfigError { + fn custom(msg: T) -> ConfigError { + ConfigError::Message(msg.to_string()) + } +} diff --git a/lib/src/file/format/mod.rs b/lib/src/file/format/mod.rs new file mode 100644 index 0000000..5c97a7f --- /dev/null +++ b/lib/src/file/format/mod.rs @@ -0,0 +1,60 @@ +use source::Source; +use value::Value; +use std::error::Error; + +#[cfg(feature = "toml")] +mod toml; + +// #[cfg(feature = "json")] +// mod json; + +// #[cfg(feature = "yaml")] +// mod yaml; + +#[derive(Debug, Clone, Copy)] +pub enum FileFormat { + /// TOML (parsed with toml) + #[cfg(feature = "toml")] + Toml, + + // /// JSON (parsed with serde_json) + // #[cfg(feature = "json")] + // Json, + + // /// YAML (parsed with yaml_rust) + // #[cfg(feature = "yaml")] + // Yaml, +} + +impl FileFormat { + // TODO: pub(crate) + #[doc(hidden)] + pub fn extensions(&self) -> Vec<&'static str> { + match *self { + #[cfg(feature = "toml")] + FileFormat::Toml => vec!["toml"], + + // #[cfg(feature = "json")] + // FileFormat::Json => vec!["json"], + + // #[cfg(feature = "yaml")] + // FileFormat::Yaml => vec!["yaml", "yml"], + } + } + + // TODO: pub(crate) + #[doc(hidden)] + #[allow(unused_variables)] + pub fn parse(&self, uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result> { + match *self { + #[cfg(feature = "toml")] + FileFormat::Toml => toml::parse(uri, text, namespace), + + // #[cfg(feature = "json")] + // FileFormat::Json => json::Content::parse(text, namespace), + + // #[cfg(feature = "yaml")] + // FileFormat::Yaml => yaml::Content::parse(text, namespace), + } + } +} diff --git a/lib/src/file/format/toml.rs b/lib/src/file/format/toml.rs new file mode 100644 index 0000000..bbe6aa6 --- /dev/null +++ b/lib/src/file/format/toml.rs @@ -0,0 +1,64 @@ +use toml; +use source::Source; +use std::collections::{HashMap, BTreeMap}; +use std::error::Error; +use value::Value; + +pub fn parse(uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result> { + // Parse a TOML value from the provided text + let mut root: toml::Value = toml::from_str(text)?; + + // Limit to namespace + if let Some(namespace) = namespace { + root = toml::Value::Table(match root { + toml::Value::Table(ref mut table) => { + if let Some(toml::Value::Table(table)) = table.remove(namespace) { + table + } else { + BTreeMap::new() + } + } + + _ => { + BTreeMap::new() + } + }); + } + + Ok(from_toml_value(uri, &root)) +} + +// TODO: Extend value origin with line/column numbers when able +fn from_toml_value(uri: Option<&String>, value: &toml::Value) -> Value { + match *value { + toml::Value::String(ref value) => Value::new(uri, value.to_string()), + toml::Value::Float(value) => Value::new(uri, value), + toml::Value::Integer(value) => Value::new(uri, value), + toml::Value::Boolean(value) => Value::new(uri, value), + + toml::Value::Table(ref table) => { + let mut m = HashMap::new(); + + for (key, value) in table { + m.insert(key.clone(), from_toml_value(uri, value)); + } + + Value::new(uri, m) + } + + toml::Value::Array(ref array) => { + let mut l = Vec::new(); + + for value in array { + l.push(from_toml_value(uri, value)); + } + + Value::new(uri, l) + } + + _ => { + // TODO: DateTime + unimplemented!(); + } + } +} diff --git a/lib/src/file/mod.rs b/lib/src/file/mod.rs new file mode 100644 index 0000000..7534ddb --- /dev/null +++ b/lib/src/file/mod.rs @@ -0,0 +1,75 @@ +mod format; +pub mod source; + +use source::Source; +use error::*; +use value::Value; + +use self::source::FileSource; +pub use self::format::FileFormat; + +pub struct File + where T: FileSource +{ + source: T, + + /// Namespace to restrict configuration from the file + namespace: Option, + + /// Format of file (which dictates what driver to use). + format: Option, + + /// A required File will error if it cannot be found + required: bool, +} + +impl File { + pub fn from_str(s: &str, format: FileFormat) -> Self { + File { + format: Some(format), + required: true, + namespace: None, + source: s.into(), + } + } +} + +impl File { + pub fn new(name: &str, format: FileFormat) -> Self { + File { + format: Some(format), + required: true, + namespace: None, + source: source::file::FileSourceFile::new(name), + } + } +} + +impl File { + pub fn required(&mut self, required: bool) -> &mut Self { + self.required = required; + self + } + + pub fn namespace(&mut self, namespace: &str) -> &mut Self { + self.namespace = Some(namespace.into()); + self + } +} + +impl Source for File { + fn collect(&self) -> Result { + // Coerce the file contents to a string + let (uri, contents) = self.source.resolve(self.format).map_err(|err| { + ConfigError::Foreign(err) + })?; + + // Parse the string using the given format + self.format.unwrap().parse(uri.as_ref(), &contents, self.namespace.as_ref()).map_err(|cause| { + ConfigError::FileParse { + uri: uri, + cause: cause + } + }) + } +} diff --git a/lib/src/file/source/file.rs b/lib/src/file/source/file.rs new file mode 100644 index 0000000..124b7dd --- /dev/null +++ b/lib/src/file/source/file.rs @@ -0,0 +1,129 @@ +use std::str::FromStr; +use std::result; +use std::error::Error; + +use std::path::{PathBuf, Path}; +use std::io::{self, Read}; +use std::fs; +use std::env; + +use source::Source; +use super::{FileFormat, FileSource}; + +/// Describes a file sourced from a file +pub struct FileSourceFile { + /// Basename of configuration file + name: String, + + /// Directory where configuration file is found + /// When not specified, the current working directory (CWD) is considered + path: Option, +} + +impl FileSourceFile { + pub fn new(name: &str) -> FileSourceFile { + FileSourceFile { + name: name.into(), + path: None, + } + } + + fn find_file(&self, format_hint: Option) -> Result> { + // Build expected configuration file + let mut basename = PathBuf::new(); + let extensions = format_hint.unwrap().extensions(); + + if let Some(ref path) = self.path { + basename.push(path.clone()); + } + + basename.push(self.name.clone()); + + // Find configuration file (algorithm similar to .git detection by git) + let mut dir = env::current_dir()?; + let mut filename = dir.as_path().join(basename.clone()); + + loop { + for ext in &extensions { + filename.set_extension(ext); + + if filename.is_file() { + // File exists and is a file + return Ok(filename); + } + } + + // Not found.. travse up via the dir + if !dir.pop() { + // Failed to find the configuration file + return Err(Box::new(io::Error::new(io::ErrorKind::NotFound, + format!("configuration file \"{}\" not found", + basename.to_string_lossy())) + )); + } + } + } +} + +impl FileSource for FileSourceFile { + fn resolve(&self, format_hint: Option) -> Result<(Option, String), Box> { + // Find file + let filename = self.find_file(format_hint)?; + + // Attempt to use a relative path for the URI + let base = env::current_dir()?; + let uri = match path_relative_from(&filename, &base) { + Some(value) => value, + None => filename.clone(), + }; + + // Read contents from file + let mut file = fs::File::open(filename.clone())?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + + Ok((Some(uri.to_string_lossy().into_owned()), text)) + } +} + +// TODO: This should probably be a crate +// https://github.com/rust-lang/rust/blob/master/src/librustc_trans/back/rpath.rs#L128 +fn path_relative_from(path: &Path, base: &Path) -> Option { + use std::path::Component; + + if path.is_absolute() != base.is_absolute() { + if path.is_absolute() { + Some(PathBuf::from(path)) + } else { + None + } + } else { + let mut ita = path.components(); + let mut itb = base.components(); + let mut comps: Vec = vec![]; + loop { + match (ita.next(), itb.next()) { + (None, None) => break, + (Some(a), None) => { + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + (None, _) => comps.push(Component::ParentDir), + (Some(a), Some(b)) if comps.is_empty() && a == b => (), + (Some(a), Some(b)) if b == Component::CurDir => comps.push(a), + (Some(_), Some(b)) if b == Component::ParentDir => return None, + (Some(a), Some(_)) => { + comps.push(Component::ParentDir); + for _ in itb { + comps.push(Component::ParentDir); + } + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + } + } + Some(comps.iter().map(|c| c.as_os_str()).collect()) + } +} diff --git a/lib/src/file/source/mod.rs b/lib/src/file/source/mod.rs new file mode 100644 index 0000000..4aeafa5 --- /dev/null +++ b/lib/src/file/source/mod.rs @@ -0,0 +1,12 @@ +pub mod file; +pub mod string; + +use std::error::Error; + +use source::Source; +use super::FileFormat; + +/// Describes where the file is sourced +pub trait FileSource { + fn resolve(&self, format_hint: Option) -> Result<(Option, String), Box>; +} diff --git a/lib/src/file/source/string.rs b/lib/src/file/source/string.rs new file mode 100644 index 0000000..e1d9f64 --- /dev/null +++ b/lib/src/file/source/string.rs @@ -0,0 +1,21 @@ +use std::str::FromStr; +use std::result; +use std::error::Error; + +use source::Source; +use super::{FileSource, FileFormat}; + +/// Describes a file sourced from a string +pub struct FileSourceString(String); + +impl<'a> From<&'a str> for FileSourceString { + fn from(s: &'a str) -> Self { + FileSourceString(s.into()) + } +} + +impl FileSource for FileSourceString { + fn resolve(&self, _: Option) -> Result<(Option, String), Box> { + Ok((None, self.0.clone())) + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 0000000..212e621 --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,25 @@ +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] + +#[macro_use] +extern crate serde; + +extern crate nom; + +#[cfg(feature = "toml")] +extern crate toml; + +mod error; +mod value; +mod de; +mod path; +mod source; +mod config; +mod file; + +pub use config::Config; +pub use error::ConfigError; +pub use value::Value; +pub use source::Source; +pub use file::{File, FileFormat}; diff --git a/lib/src/path/mod.rs b/lib/src/path/mod.rs new file mode 100644 index 0000000..46e2290 --- /dev/null +++ b/lib/src/path/mod.rs @@ -0,0 +1,43 @@ +use std::str::FromStr; +use nom::ErrorKind; +use error::*; +use value::{Value, ValueKind}; + +mod parser; + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum Expression { + Identifier(String), + Child(Box, String), + Subscript(Box, i32), +} + +impl FromStr for Expression { + type Err = ConfigError; + + fn from_str(s: &str) -> Result { + parser::from_str(s.as_bytes()).to_result().map_err(|kind| { + ConfigError::PathParse(kind) + }) + } +} + +impl Expression { + pub fn get<'a>(self, root: &'a Value) -> Option<&'a Value> { + match self { + Expression::Identifier(id) => { + match root.kind { + // `x` access on a table is equivalent to: map[x] + ValueKind::Table(ref map) => map.get(&id), + + // all other variants return None + _ => None, + } + } + + _ => { + unimplemented!(); + } + } + } +} diff --git a/lib/src/path/parser.rs b/lib/src/path/parser.rs new file mode 100644 index 0000000..ad7ab91 --- /dev/null +++ b/lib/src/path/parser.rs @@ -0,0 +1,120 @@ +use nom::*; +use std::str::{FromStr, from_utf8}; +use super::Expression; + +named!(ident_, + map!( + map_res!(is_a!( + "abcdefghijklmnopqrstuvwxyz \ + ABCDEFGHIJKLMNOPQRSTUVWXYZ \ + 0123456789 \ + _-" + ), from_utf8), + |s: &str| { + s.to_string() + } + ) +); + +named!(integer , + map_res!( + map_res!( + ws!(digit), + from_utf8 + ), + FromStr::from_str + ) +); + +named!(ident, map!(ident_, Expression::Identifier)); + +fn postfix(expr: Expression) -> Box 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!(']') + ) + ) + }); +} + +pub fn from_str(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, + } +} + +#[cfg(test)] +mod test { + use super::*; + use super::Expression::*; + + #[test] + fn test_id() { + let parsed: Expression = from_str("abcd").unwrap(); + assert_eq!(parsed, Identifier("abcd".into())); + } + + #[test] + fn test_id_dash() { + let parsed: Expression = from_str("abcd-efgh").unwrap(); + assert_eq!(parsed, Identifier("abcd-efgh".into())); + } + + #[test] + fn test_child() { + let parsed: Expression = from_str("abcd.efgh").unwrap(); + let expected = Child(Box::new(Identifier("abcd".into())), "efgh".into()); + + assert_eq!(parsed, expected); + } + + #[test] + fn test_subscript() { + let parsed: Expression = from_str("abcd[12]").unwrap(); + let expected = Subscript(Box::new(Identifier("abcd".into())), 12); + + assert_eq!(parsed, expected); + } + + #[test] + fn test_subscript_neg() { + let parsed: Expression = from_str("abcd[-1]").unwrap(); + let expected = Subscript(Box::new(Identifier("abcd".into())), -1); + + assert_eq!(parsed, expected); + } +} diff --git a/lib/src/source.rs b/lib/src/source.rs new file mode 100644 index 0000000..7519438 --- /dev/null +++ b/lib/src/source.rs @@ -0,0 +1,9 @@ +use error::*; +use value::Value; + +/// Describes a generic _source_ of configuration properties. +pub trait Source { + /// Collect all configuration properties available from this source and return + /// a top-level Value (which we expected to be a Table). + fn collect(&self) -> Result; +} diff --git a/lib/src/value.rs b/lib/src/value.rs new file mode 100644 index 0000000..87e45bc --- /dev/null +++ b/lib/src/value.rs @@ -0,0 +1,215 @@ +use std::collections::HashMap; +use std::fmt::Display; +use error::*; + +/// Underlying kind of the configuration value. +#[derive(Debug, Clone)] +pub enum ValueKind { + Nil, + String(String), + Integer(i64), + Float(f64), + Boolean(bool), + Table(HashMap), + Array(Vec), +} + +impl Default for ValueKind { + fn default() -> Self { + ValueKind::Nil + } +} + +impl From> for ValueKind + where T: Into +{ + fn from(value: Option) -> ValueKind { + match value { + Some(value) => value.into(), + None => ValueKind::Nil, + } + } +} + +impl From for ValueKind { + fn from(value: String) -> ValueKind { + ValueKind::String(value.into()) + } +} + +impl<'a> From<&'a str> for ValueKind { + fn from(value: &'a str) -> ValueKind { + ValueKind::String(value.into()) + } +} + +impl From for ValueKind { + fn from(value: i64) -> ValueKind { + ValueKind::Integer(value) + } +} + +impl From for ValueKind { + fn from(value: f64) -> ValueKind { + ValueKind::Float(value) + } +} + +impl From for ValueKind { + fn from(value: bool) -> ValueKind { + ValueKind::Boolean(value) + } +} + +impl From> for ValueKind + where T: Into +{ + fn from(values: HashMap) -> ValueKind { + let mut r = HashMap::new(); + + for (k, v) in values { + r.insert(k.clone(), v.into()); + } + + ValueKind::Table(r) + } +} + +impl From> for ValueKind + where T: Into +{ + fn from(values: Vec) -> ValueKind { + let mut l = Vec::new(); + + for v in values { + l.push(v.into()); + } + + ValueKind::Array(l) + } +} + +/// A configuration value. +#[derive(Default, Debug, Clone)] +pub struct Value { + /// A description of the original location of the value. + /// + /// A Value originating from a File might contain: + /// ``` + /// Settings.toml at line 1 column 2 + /// ``` + /// + /// A Value originating from the environment would contain: + /// ``` + /// the envrionment + /// ``` + /// + /// A Value originating from a remote source might contain: + /// ``` + /// etcd+http://127.0.0.1:2379 + /// ``` + origin: Option, + + /// Underlying kind of the configuration value. + pub kind: ValueKind, +} + +impl Value { + pub fn new(origin: Option<&String>, kind: V) -> Value + where V: Into + { + Value { + origin: origin.cloned(), + kind: kind.into(), + } + } + + /// Returns `self` as a bool, if possible. + pub fn into_bool(self) -> Result { + match self.kind { + ValueKind::Boolean(value) => Ok(value), + ValueKind::Integer(value) => Ok(value != 0), + ValueKind::Float(value) => Ok(value != 0.0), + + ValueKind::String(ref value) => { + match value.to_lowercase().as_ref() { + "1" | "true" | "on" | "yes" => Ok(true), + "0" | "false" | "off" | "no" => Ok(false), + + // Unexpected string value + s @ _ => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Str(s.into()), &"a boolean")), + } + } + + // Unexpected type + ValueKind::Nil => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Unit, &"a boolean")), + ValueKind::Table(_) => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Map, &"a boolean")), + ValueKind::Array(_) => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Seq, &"a boolean")), + } + } + + /// Returns `self` into an i64, if possible. + pub fn into_int(self) -> Result { + match self.kind { + ValueKind::Integer(value) => Ok(value), + + ValueKind::String(ref s) => s.parse().map_err(|_| { + // Unexpected string + ConfigError::invalid_type(self.origin.clone(), Unexpected::Str(s.clone()), &"an integer") + }), + + ValueKind::Boolean(value) => Ok(if value { 1 } else { 0 }), + ValueKind::Float(value) => Ok(value.round() as i64), + + // Unexpected type + ValueKind::Nil => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Unit, &"an integer")), + ValueKind::Table(_) => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Map, &"an integer")), + ValueKind::Array(_) => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Seq, &"an integer")), + } + } + + /// Returns `self` into a f64, if possible. + pub fn into_float(self) -> Result { + match self.kind { + ValueKind::Float(value) => Ok(value), + + ValueKind::String(ref s) => s.parse().map_err(|_| { + // Unexpected string + ConfigError::invalid_type(self.origin.clone(), Unexpected::Str(s.clone()), &"a floating point") + }), + + ValueKind::Integer(value) => Ok(value as f64), + ValueKind::Boolean(value) => Ok(if value { 1.0 } else { 0.0 }), + + // Unexpected type + ValueKind::Nil => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Unit, &"a floating point")), + ValueKind::Table(_) => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Map, &"a floating point")), + ValueKind::Array(_) => Err(ConfigError::invalid_type(self.origin.clone(), Unexpected::Seq, &"a floating point")), + } + } + /// If the `Value` is a Table, returns the associated Map. + pub fn into_table(self) -> Result> { + match self.kind { + ValueKind::Table(value) => Ok(value), + + // Cannot convert + ValueKind::Float(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Float(value), &"a map")), + ValueKind::String(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Str(value), &"a map")), + ValueKind::Integer(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Integer(value), &"a map")), + ValueKind::Boolean(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Bool(value), &"a map")), + ValueKind::Nil => Err(ConfigError::invalid_type(self.origin, Unexpected::Unit, &"a map")), + ValueKind::Array(_) => Err(ConfigError::invalid_type(self.origin, Unexpected::Seq, &"a map")), + } + } +} + +impl From for Value + where T: Into +{ + fn from(value: T) -> Value { + Value { + origin: None, + kind: value.into(), + } + } +} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index ad4cda2..0000000 --- a/src/config.rs +++ /dev/null @@ -1,709 +0,0 @@ -use value::Value; -use source::{Source, SourceBuilder}; -use path; - -use std::error::Error; -use std::fmt; -use std::str::FromStr; -use std::collections::HashMap; - -#[derive(Default, Debug)] -pub struct FrozenError {} - -impl fmt::Display for FrozenError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "FrozenError") - } -} - -impl Error for FrozenError { - fn description(&self) -> &'static str { - "configuration is frozen" - } -} - -// Underlying storage for the configuration -enum ConfigStore { - Mutable { - defaults: HashMap, - overrides: HashMap, - - // Ordered list of sources - sources: Vec>, - }, - - // TODO: Will be used for frozen configuratino soon - #[allow(dead_code)] - Frozen, -} - -impl Default for ConfigStore { - fn default() -> Self { - ConfigStore::Mutable { - defaults: HashMap::new(), - overrides: HashMap::new(), - sources: Vec::new(), - } - } -} - -fn merge_in_all(r: &mut HashMap, map: &HashMap) { - 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, - 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 require_table(r: &mut HashMap, key: &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, 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); - } - } - - _ => { - root.insert(text, value.clone()); - } - } - } - - 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); - } - } - } - } - } - - 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; - - if index < 0 { - index = len + index; - } - - 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 - } - } - } - } - } -} - -fn path_set_str(root: &mut HashMap, key: &str, value: &Value) { - match path::Expression::from_str(key) { - Ok(expr) => { - path_set(root, expr, value); - } - - Err(_) => { - // TODO: Log warning here - } - }; -} - -impl ConfigStore { - fn merge(&mut self, source: T) -> Result<(), Box> - where T: SourceBuilder + Send + Sync - { - if let ConfigStore::Mutable { ref mut sources, .. } = *self { - sources.push(source.build()?); - - Ok(()) - } else { - Err(FrozenError::default().into()) - } - } - - fn set_default(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into - { - if let ConfigStore::Mutable { ref mut defaults, .. } = *self { - path_set_str(defaults, &key.to_lowercase(), &value.into()); - - Ok(()) - } else { - Err(FrozenError::default().into()) - } - } - - fn set(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into - { - if let ConfigStore::Mutable { ref mut overrides, .. } = *self { - path_set_str(overrides, &key.to_lowercase(), &value.into()); - - Ok(()) - } else { - Err(FrozenError::default().into()) - } - } - - fn collect(&self) -> Result, Box> { - if let ConfigStore::Mutable { ref overrides, ref sources, ref defaults } = *self { - let mut r = HashMap::::new(); - - merge_in_all(&mut r, defaults); - - for source in sources { - merge_in_all(&mut r, &source.collect()); - } - - merge_in_all(&mut r, overrides); - - Ok(r) - } else { - Err(FrozenError::default().into()) - } - } -} - -#[derive(Default)] -pub struct Config { - store: ConfigStore, - - /// Top-level table of the cached configuration - /// - /// As configuration sources are merged with `Config::merge`, this - /// cache is updated. - cache: HashMap, -} - -impl Config { - pub fn new() -> Self { - Default::default() - } - - /// Merge in configuration values from the given source. - pub fn merge(&mut self, source: T) -> Result<(), Box> - where T: SourceBuilder + Send + Sync - { - self.store.merge(source)?; - self.refresh()?; - - Ok(()) - } - - /// Sets the default value for this key. The default value is only used - /// when no other value is provided. - pub fn set_default(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into - { - self.store.set_default(key, value)?; - self.refresh()?; - - Ok(()) - } - - /// Sets an override for this key. - pub fn set(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into - { - self.store.set(key, value)?; - self.refresh()?; - - Ok(()) - } - - /// Refresh the configuration cache with fresh - /// data from associated sources. - /// - /// Configuration is automatically refreshed after a mutation - /// operation (`set`, `merge`, `set_default`, etc.). - pub fn refresh(&mut self) -> Result<(), Box> { - self.cache = self.store.collect()?; - - Ok(()) - } - - // Child ( Child ( Identifier( "x" ), "y" ), "z" ) - fn path_get<'a>(&'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(&self, key_path: &str) -> Option { - let key_expr: path::Expression = match key_path.to_lowercase().parse() { - Ok(expr) => expr, - Err(_) => { - // TODO: Log warning here - return None; - } - }; - - self.path_get(key_expr).cloned() - } - - pub fn get_str(&self, key: &str) -> Option { - self.get(key).and_then(Value::into_str) - } - - pub fn get_int(&self, key: &str) -> Option { - self.get(key).and_then(Value::into_int) - } - - pub fn get_float(&self, key: &str) -> Option { - self.get(key).and_then(Value::into_float) - } - - pub fn get_bool(&self, key: &str) -> Option { - self.get(key).and_then(Value::into_bool) - } - - pub fn get_table(&self, key: &str) -> Option> { - self.get(key).and_then(Value::into_table) - } - - pub fn get_array(self, key: &str) -> Option> { - self.get(key).and_then(Value::into_array) - } -} - -#[cfg(test)] -mod test { - use std::collections::HashMap; - use super::{Value, Config}; - - // Retrieval of a non-existent key - #[test] - fn test_not_found() { - let c = Config::new(); - - assert_eq!(c.get_int("key"), None); - } - - // Explicit override - #[test] - fn test_default_override() { - let mut c = Config::new(); - - c.set_default("key_1", false).unwrap(); - c.set_default("key_2", false).unwrap(); - - assert!(!c.get_bool("key_1").unwrap()); - assert!(!c.get_bool("key_2").unwrap()); - - c.set("key_2", true).unwrap(); - - 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 = Config::new(); - - c.set("key", "value").unwrap(); - - assert_eq!(c.get_str("key").unwrap(), "value"); - } - - // Storage and retrieval of Boolean values - #[test] - fn test_bool() { - let mut c = Config::new(); - - c.set("key", true).unwrap(); - - assert_eq!(c.get_bool("key").unwrap(), true); - } - - // Storage and retrieval of Float values - #[test] - fn test_float() { - let mut c = Config::new(); - - c.set("key", 3.14).unwrap(); - - assert_eq!(c.get_float("key").unwrap(), 3.14); - } - - // Storage and retrieval of Integer values - #[test] - fn test_int() { - let mut c = Config::new(); - - c.set("key", 42).unwrap(); - - assert_eq!(c.get_int("key").unwrap(), 42); - } - - // Storage of various values and retrieval as String - #[test] - fn test_retrieve_str() { - let mut c = Config::new(); - - c.set("key_1", 115).unwrap(); - c.set("key_2", 1.23).unwrap(); - c.set("key_3", false).unwrap(); - - assert_eq!(c.get_str("key_1").unwrap(), "115"); - assert_eq!(c.get_str("key_2").unwrap(), "1.23"); - assert_eq!(c.get_str("key_3").unwrap(), "false"); - } - - // Storage of various values and retrieval as Integer - #[test] - fn test_retrieve_int() { - let mut c = Config::new(); - - c.set("key_1", "121").unwrap(); - c.set("key_2", 5.12).unwrap(); - c.set("key_3", 5.72).unwrap(); - c.set("key_4", false).unwrap(); - c.set("key_5", true).unwrap(); - c.set("key_6", "asga").unwrap(); - - 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 = Config::new(); - - c.set("key_1", "121").unwrap(); - c.set("key_2", "121.512").unwrap(); - c.set("key_3", 5).unwrap(); - c.set("key_4", false).unwrap(); - c.set("key_5", true).unwrap(); - c.set("key_6", "asga").unwrap(); - - 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 = Config::new(); - - c.set("key_1", "121").unwrap(); - c.set("key_2", "