diff options
author | Ryan Leckey <leckey.ryan@gmail.com> | 2017-03-08 11:09:37 -0800 |
---|---|---|
committer | Ryan Leckey <leckey.ryan@gmail.com> | 2017-03-08 11:09:37 -0800 |
commit | 2dc6a74b84825f65142c1fa7d3e67cd4f35ee3cb (patch) | |
tree | 23b21f732efbb215498db6debf6dbaee3af7e94f | |
parent | c9ee1568fe212e4c352ec1afc52db44b34348fcd (diff) |
Initial work on deep serde integration
33 files changed, 1258 insertions, 1597 deletions
@@ -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 <leckey.ryan@gmail.com>"] -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::<bool>("debug").unwrap()); + println!("pi = {}", c.get::<f64>("pi").unwrap()); + println!("weight = {}", c.get::<i64>("weight").unwrap()); + println!("location = {:?}", c.get::<Point>("location").unwrap()); + // println!("location.x = {}", c.get::<Point>("location.x").unwrap()); + // println!("location.y = {}", c.get::<Point>("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 <leckey.ryan@gmail.com>"] +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<String, Value>, + overrides: HashMap<String, Value>, + sources: Vec<Box<Source + Send + Sync>>, + }, + + // 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<T>(&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::<String, Value>::new()); + + // HACK! + cache = sources[0].collect()?; + + cache + } + + ConfigKind::Frozen => { + return Err(ConfigError::Frozen); + } + }; + + Ok(()) + } + + pub fn get<T: Deserialize>(&self, key: &str) -> Result<T> { + // 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<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + // 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<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + visitor.visit_bool(self.into_bool()?) + } + + #[inline] + fn deserialize_u8<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_u16<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_u32<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_u64<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_i8<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_i16<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_i32<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_i64<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + visitor.visit_i64(self.into_int()?) + } + + #[inline] + fn deserialize_f32<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_f64<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + visitor.visit_f64(self.into_float()?) + } + + #[inline] + fn deserialize_char<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_str<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_string<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_bytes<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_byte_buf<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_option<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_unit<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_unit_struct<V: de::Visitor>(self, + name: &'static str, + visitor: V) + -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_newtype_struct<V: de::Visitor>(self, + name: &'static str, + visitor: V) + -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_seq<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_seq_fixed_size<V: de::Visitor>(self, + len: usize, + visitor: V) + -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_tuple<V: de::Visitor>(self, len: usize, visitor: V) -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_tuple_struct<V: de::Visitor>(self, + name: &'static str, + len: usize, + visitor: V) + -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_enum<V: de::Visitor>(self, + name: &'static str, + variants: &'static [&'static str], + visitor: V) + -> Result<V::Value> { + unimplemented!(); + } + + #[inline] + fn deserialize_ignored_any<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + 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<V: de::Visitor>(self, visitor: V) -> Result<V::Value> { + 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<String, Value>) -> MapVisitor { + MapVisitor { elements: table.drain().collect(), index: 0 } + } +} + +impl de::MapVisitor for MapVisitor { + type Error = ConfigError; + + fn visit_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>> + 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<V>(&mut self, seed: V) -> Result<V::Value> + 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<String>, cause: Box<Error> }, + + /// Value could not be converted into the requested type. + Type { + origin: Option<String>, + unexpected: Unexpected, + expected: &'static str, + }, + + /// Custom message + Message(String), + + /// Unadorned error from a foreign source. + Foreign(Box<Error>), +} + +impl ConfigError { + // FIXME: pub(crate) + #[doc(hidden)] + pub fn invalid_type(origin: Option<String>, 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<T> = result::Result<T, ConfigError>; + +// 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<T: fmt::Display>(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<Value, Box<Error>> { + 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<Value, Box<Error>> { + // Parse a TOML value from the provided text + let mut root: toml::Value = toml::from_str(text)?; + + // Limit to namespace + if let Some(namespace) = namespa |