From bfc44c331a77d8c341c076e72df5ed0b56fbd422 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 1 Jun 2017 23:22:04 -0700 Subject: Move things around and get some tests in place --- Cargo.toml | 34 +++- examples/basic/Cargo.toml | 7 - examples/basic/src/main.rs | 35 ----- examples/file-json/Cargo.toml | 7 - examples/file-json/Settings.json | 5 - examples/file-json/src/main.rs | 12 -- examples/file-toml/Cargo.toml | 9 -- examples/file-toml/Settings.toml | 4 - examples/file-toml/src/main.rs | 22 --- examples/file-yaml/Cargo.toml | 7 - examples/file-yaml/Settings.yaml | 3 - examples/file-yaml/src/main.rs | 12 -- lib/Cargo.toml | 24 --- lib/src/config.rs | 100 ------------ lib/src/de.rs | 107 ------------- lib/src/error.rs | 155 ------------------ 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 | 249 ----------------------------- src/config.rs | 104 +++++++++++++ src/de.rs | 179 +++++++++++++++++++++ src/error.rs | 155 ++++++++++++++++++ src/file/format/mod.rs | 60 +++++++ src/file/format/toml.rs | 64 ++++++++ src/file/mod.rs | 75 +++++++++ src/file/source/file.rs | 129 +++++++++++++++ src/file/source/mod.rs | 12 ++ src/file/source/string.rs | 21 +++ src/lib.rs | 25 +++ src/path/mod.rs | 41 +++++ src/path/parser.rs | 120 ++++++++++++++ src/source.rs | 9 ++ src/value.rs | 328 +++++++++++++++++++++++++++++++++++++++ tests/Settings.toml | 12 ++ tests/scalar.rs | 44 ++++++ tests/struct.rs | 68 ++++++++ 44 files changed, 1472 insertions(+), 1324 deletions(-) delete mode 100644 examples/basic/Cargo.toml delete mode 100644 examples/basic/src/main.rs delete mode 100644 examples/file-json/Cargo.toml delete mode 100644 examples/file-json/Settings.json delete mode 100644 examples/file-json/src/main.rs delete mode 100644 examples/file-toml/Cargo.toml delete mode 100644 examples/file-toml/Settings.toml delete mode 100644 examples/file-toml/src/main.rs delete mode 100644 examples/file-yaml/Cargo.toml delete mode 100644 examples/file-yaml/Settings.yaml delete mode 100644 examples/file-yaml/src/main.rs delete mode 100644 lib/Cargo.toml delete mode 100644 lib/src/config.rs delete mode 100644 lib/src/de.rs delete mode 100644 lib/src/error.rs delete mode 100644 lib/src/file/format/mod.rs delete mode 100644 lib/src/file/format/toml.rs delete mode 100644 lib/src/file/mod.rs delete mode 100644 lib/src/file/source/file.rs delete mode 100644 lib/src/file/source/mod.rs delete mode 100644 lib/src/file/source/string.rs delete mode 100644 lib/src/lib.rs delete mode 100644 lib/src/path/mod.rs delete mode 100644 lib/src/path/parser.rs delete mode 100644 lib/src/source.rs delete mode 100644 lib/src/value.rs create mode 100644 src/config.rs create mode 100644 src/de.rs create mode 100644 src/error.rs create mode 100644 src/file/format/mod.rs create mode 100644 src/file/format/toml.rs create mode 100644 src/file/mod.rs create mode 100644 src/file/source/file.rs create mode 100644 src/file/source/mod.rs create mode 100644 src/file/source/string.rs create mode 100644 src/lib.rs create mode 100644 src/path/mod.rs create mode 100644 src/path/parser.rs create mode 100644 src/source.rs create mode 100644 src/value.rs create mode 100644 tests/Settings.toml create mode 100644 tests/scalar.rs create mode 100644 tests/struct.rs diff --git a/Cargo.toml b/Cargo.toml index 99ff591..b238ca6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,26 @@ -[workspace] -members = [ - "lib", - "examples/basic", - # "examples/file-json", - "examples/file-toml", - # "examples/file-yaml", -] +[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" + +[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 } + +[dev-dependencies] +serde_derive = "^0.9" diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml deleted file mode 100644 index 25c3f4d..0000000 --- a/examples/basic/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "basic" -version = "0.1.0" -workspace = "../../" - -[dependencies] -config = { path = "../../lib" } diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs deleted file mode 100644 index 49059ef..0000000 --- a/examples/basic/src/main.rs +++ /dev/null @@ -1,35 +0,0 @@ -extern crate config; - -use config::*; - -fn main() { - 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(); - - // // 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")); - - // // 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")); -} diff --git a/examples/file-json/Cargo.toml b/examples/file-json/Cargo.toml deleted file mode 100644 index 1e8765e..0000000 --- a/examples/file-json/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "file-json" -version = "0.1.0" -workspace = "../../" - -[dependencies] -config = { path = "../../lib", default-features = false, features = ["json"] } diff --git a/examples/file-json/Settings.json b/examples/file-json/Settings.json deleted file mode 100644 index 72b28e6..0000000 --- a/examples/file-json/Settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "debug": false, - "pi": 3.14159, - "weight": 150 -} diff --git a/examples/file-json/src/main.rs b/examples/file-json/src/main.rs deleted file mode 100644 index e4ff809..0000000 --- a/examples/file-json/src/main.rs +++ /dev/null @@ -1,12 +0,0 @@ -extern crate config; - -fn main() { - let mut c = config::Config::new(); - - // Read configuration from "Settings.json" - c.merge(config::File::new("Settings", config::FileFormat::Json)).unwrap(); - - println!("debug = {:?}", c.get("debug")); - println!("pi = {:?}", c.get("pi")); - println!("weight = {:?}", c.get("weight")); -} diff --git a/examples/file-toml/Cargo.toml b/examples/file-toml/Cargo.toml deleted file mode 100644 index 0501895..0000000 --- a/examples/file-toml/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "file-toml" -version = "0.1.0" -workspace = "../../" - -[dependencies] -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 deleted file mode 100644 index 21fa1e3..0000000 --- a/examples/file-toml/Settings.toml +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index ddca412..0000000 --- a/examples/file-toml/src/main.rs +++ /dev/null @@ -1,22 +0,0 @@ -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::default(); - - // Read configuration from "Settings.toml" - c.merge(config::File::new("Settings", config::FileFormat::Toml)).unwrap(); - - // 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 deleted file mode 100644 index 4570078..0000000 --- a/examples/file-yaml/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "file-yaml" -version = "0.1.0" -workspace = "../../" - -[dependencies] -config = { path = "../../lib", default-features = false, features = ["yaml"] } diff --git a/examples/file-yaml/Settings.yaml b/examples/file-yaml/Settings.yaml deleted file mode 100644 index d92f6ad..0000000 --- a/examples/file-yaml/Settings.yaml +++ /dev/null @@ -1,3 +0,0 @@ -debug: false -pi: 3.14159 -weight: 150 diff --git a/examples/file-yaml/src/main.rs b/examples/file-yaml/src/main.rs deleted file mode 100644 index 6d72976..0000000 --- a/examples/file-yaml/src/main.rs +++ /dev/null @@ -1,12 +0,0 @@ -extern crate config; - -fn main() { - let mut c = config::Config::new(); - - // Read configuration from "Settings.yaml" - c.merge(config::File::new("Settings", config::FileFormat::Yaml)).unwrap(); - - println!("debug = {:?}", c.get("debug")); - println!("pi = {:?}", c.get("pi")); - println!("weight = {:?}", c.get("weight")); -} diff --git a/lib/Cargo.toml b/lib/Cargo.toml deleted file mode 100644 index 6b04a8e..0000000 --- a/lib/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[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 deleted file mode 100644 index 93d7fc0..0000000 --- a/lib/src/config.rs +++ /dev/null @@ -1,100 +0,0 @@ -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 { - // TODO: We need to actually merge in all the stuff - ConfigKind::Mutable { - ref overrides, - ref sources, - ref defaults, - } => sources[0].collect()?, - - 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 deleted file mode 100644 index 89a3bcf..0000000 --- a/lib/src/de.rs +++ /dev/null @@ -1,107 +0,0 @@ -use serde::de; -use value::{Value, ValueKind}; -use error::*; -use std::borrow::Cow; -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 - where V: de::Visitor - { - // Deserialize based on the underlying type - match self.kind { - ValueKind::Integer(i) => visitor.visit_i64(i), - ValueKind::Boolean(b) => visitor.visit_bool(b), - ValueKind::Float(f) => visitor.visit_f64(f), - ValueKind::String(s) => visitor.visit_string(s), - ValueKind::Array(values) => unimplemented!(), - ValueKind::Table(map) => visitor.visit_map(MapVisitor::new(map)), - _ => { - unimplemented!(); - } - } - } - - #[inline] - fn deserialize_option(self, visitor: V) -> Result - where V: de::Visitor - { - // Match an explicit nil as None and everything else as Some - match self.kind { - ValueKind::Nil => visitor.visit_none(), - _ => visitor.visit_some(self), - } - } - - forward_to_deserialize! { - bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq - seq_fixed_size bytes byte_buf map struct unit enum newtype_struct - struct_field ignored_any unit_struct tuple_struct tuple - } -} - -struct StrDeserializer<'a>(&'a str); - -impl<'a> StrDeserializer<'a> { - fn new(key: &'a str) -> Self { - StrDeserializer(key) - } -} - -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 seq - seq_fixed_size bytes byte_buf map struct unit enum newtype_struct - struct_field ignored_any unit_struct tuple_struct tuple option - } -} - -struct MapVisitor { - elements: Vec<(String, Value)>, - index: usize, -} - -impl MapVisitor { - fn new(mut table: HashMap) -> Self { - 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 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 deleted file mode 100644 index b97ebac..0000000 --- a/lib/src/error.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::error::Error; -use std::borrow::Cow; -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) -> Self { - 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) -> Self { - ConfigError::Message(msg.to_string()) - } -} diff --git a/lib/src/file/format/mod.rs b/lib/src/file/format/mod.rs deleted file mode 100644 index 5c97a7f..0000000 --- a/lib/src/file/format/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index bbe6aa6..0000000 --- a/lib/src/file/format/toml.rs +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index 7534ddb..0000000 --- a/lib/src/file/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -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 deleted file mode 100644 index 124b7dd..0000000 --- a/lib/src/file/source/file.rs +++ /dev/null @@ -1,129 +0,0 @@ -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 deleted file mode 100644 index 4aeafa5..0000000 --- a/lib/src/file/source/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index e1d9f64..0000000 --- a/lib/src/file/source/string.rs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 212e621..0000000 --- a/lib/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -#![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 deleted file mode 100644 index 46e2290..0000000 --- a/lib/src/path/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index ad7ab91..0000000 --- a/lib/src/path/parser.rs +++ /dev/null @@ -1,120 +0,0 @@ -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 deleted file mode 100644 index 7519438..0000000 --- a/lib/src/source.rs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 5b0014a..0000000 --- a/lib/src/value.rs +++ /dev/null @@ -1,249 +0,0 @@ -use std::collections::HashMap; -use std::fmt::Display; -use error::*; - -/// Underlying kind of the configuration value. -#[derive(Debug, Clone)] -pub enum ValueKind { - Nil, - Boolean(bool), - Integer(i64), - Float(f64), - String(String), - Table(Table), - Array(Array), -} - -pub type Array = Vec; -pub type Table = HashMap; - -impl Default for ValueKind { - fn default() -> Self { - ValueKind::Nil - } -} - -impl From> for ValueKind - where T: Into -{ - fn from(value: Option) -> Self { - match value { - Some(value) => value.into(), - None => ValueKind::Nil, - } - } -} - -impl From for ValueKind { - fn from(value: String) -> Self { - ValueKind::String(value.into()) - } -} - -impl<'a> From<&'a str> for ValueKind { - fn from(value: &'a str) -> Self { - ValueKind::String(value.into()) - } -} - -impl From for ValueKind { - fn from(value: i64) -> Self { - ValueKind::Integer(value) - } -} - -impl From for ValueKind { - fn from(value: f64) -> Self { - ValueKind::Float(value) - } -} - -impl From for ValueKind { - fn from(value: bool) -> Self { - ValueKind::Boolean(value) - } -} - -impl From> for ValueKind - where T: Into -{ - fn from(values: HashMap) -> Self { - 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) -> Self { - 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) -> Self - 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")), - } - } - - /// Returns `self` into a str, if possible. - pub fn into_str(self) -> Result { - match self.kind { - ValueKind::String(value) => Ok(value), - - // Cannot convert - ValueKind::Float(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Float(value), "a string")), - ValueKind::Integer(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Integer(value), "a string")), - ValueKind::Boolean(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Bool(value), "a string")), - ValueKind::Nil => Err(ConfigError::invalid_type(self.origin, Unexpected::Unit, "a string")), - ValueKind::Table(_) => Err(ConfigError::invalid_type(self.origin, Unexpected::Map, "a string")), - ValueKind::Array(_) => Err(ConfigError::invalid_type(self.origin, Unexpected::Seq, "a string")), - } - } - - /// Returns `self` into an array, if possible - pub fn into_array(self) -> Result> { - match self.kind { - ValueKind::Array(value) => Ok(value), - - // Cannot convert - ValueKind::Float(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Float(value), "an array")), - ValueKind::String(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Str(value), "an array")), - ValueKind::Integer(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Integer(value), "an array")), - ValueKind::Boolean(value) => Err(ConfigError::invalid_type(self.origin, Unexpected::Bool(value), "an array")), - ValueKind::Nil => Err(ConfigError::invalid_type(self.origin, Unexpected::Unit, "an array")), - ValueKind::Table(_) => Err(ConfigError::invalid_type(self.origin, Unexpected::Map, "an array")), - } - } - - /// 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) -> Self { - Value { - origin: None, - kind: value.into(), - } - } -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4d24a1d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,104 @@ +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 { + // TODO: We need to actually merge in all the stuff + ConfigKind::Mutable { + ref overrides, + ref sources, + ref defaults, + } => sources[0].collect()?, + + ConfigKind::Frozen => { + return Err(ConfigError::Frozen); + } + }; + + Ok(()) + } + + pub fn deserialize(&self) -> Result { + return T::deserialize(self.cache.clone()); + } + + 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/src/de.rs b/src/de.rs new file mode 100644 index 0000000..9a9ef58 --- /dev/null +++ b/src/de.rs @@ -0,0 +1,179 @@ +use serde::de; +use value::{Value, ValueKind}; +use error::*; +use std::borrow::Cow; +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 + where V: de::Visitor + { + // Deserialize based on the underlying type + match self.kind { + ValueKind::Integer(i) => visitor.visit_i64(i), + ValueKind::Boolean(b) => visitor.visit_bool(b), + ValueKind::Float(f) => visitor.visit_f64(f), + ValueKind::String(s) => visitor.visit_string(s), + ValueKind::Array(values) => unimplemented!(), + 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_i8(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type + visitor.visit_i8(self.into_int()? as i8) + } + + #[inline] + fn deserialize_i16(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type + visitor.visit_i16(self.into_int()? as i16) + } + + #[inline] + fn deserialize_i32(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type + visitor.visit_i32(self.into_int()? as i32) + } + + #[inline] + fn deserialize_i64(self, visitor: V) -> Result { + visitor.visit_i64(self.into_int()?) + } + + #[inline] + fn deserialize_u8(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type + visitor.visit_u8(self.into_int()? as u8) + } + + #[inline] + fn deserialize_u16(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type + visitor.visit_u16(self.into_int()? as u16) + } + + #[inline] + fn deserialize_u32(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type + visitor.visit_u32(self.into_int()? as u32) + } + + #[inline] + fn deserialize_u64(self, visitor: V) -> Result { + // FIXME: This should *fail* if the value does not fit in the requets integer type + visitor.visit_u64(self.into_int()? as u64) + } + + #[inline] + fn deserialize_f32(self, visitor: V) -> Result { + visitor.visit_f32(self.into_float()? as f32) + } + + #[inline] + fn deserialize_f64(self, visitor: V) -> Result { + visitor.visit_f64(self.into_float()?) + } + + #[inline] + fn deserialize_str(self, visitor: V) -> Result { + visitor.visit_string(self.into_str()?) + } + + #[inline] + fn deserialize_string(self, visitor: V) -> Result { + visitor.visit_string(self.into_str()?) + } + + #[inline] + fn deserialize_option(self, visitor: V) -> Result + where V: de::Visitor + { + // Match an explicit nil as None and everything else as Some + match self.kind { + ValueKind::Nil => visitor.visit_none(), + _ => visitor.visit_some(self), + } + } + + forward_to_deserialize! { + char seq + seq_fixed_size bytes byte_buf map struct unit enum newtype_struct + struct_field ignored_any unit_struct tuple_struct tuple + } +} + +struct StrDeserializer<'a>(&'a str); + +impl<'a> StrDeserializer<'a> { + fn new(key: &'a str) -> Self { + StrDeserializer(key) + } +} + +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 seq + seq_fixed_size bytes byte_buf map struct unit enum newtype_struct + struct_field ignored_any unit_struct tuple_struct tuple option + } +} + +struct MapVisitor { + elements: Vec<(String, Value)>, + index: usize, +} + +impl MapVisitor { + fn new(mut table: HashMap) -> Self { + 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 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/src/error.rs b/src/error.rs new file mode 100644 index 0000000..b97ebac --- /dev/null +++ b/src/error.rs @@ -0,0 +1,155 @@ +use std::error::Error; +use std::borrow::Cow; +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) -> Self { + 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) -> Self { + ConfigError::Message(msg.to_string()) + } +} diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs new file mode 100644 index 0000000..5c97a7f --- /dev/null +++ b/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/src/file/format/toml.rs b/src/file/format/toml.rs new file mode 100644 index 0000000..bbe6aa6 --- /dev/null +++ b/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(v