From 2b438ed9b53fee5689032f3b5fcdda8d15becd5f Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 22 Jun 2017 14:14:29 -0700 Subject: Add builder API to Config --- src/config.rs | 185 +++++++++++++++++++++++++++++++++++++++------- src/env.rs | 13 +++- src/file/format/json.rs | 30 ++++---- src/file/format/mod.rs | 6 +- src/file/format/toml.rs | 26 ++++--- src/file/format/yaml.rs | 34 +++++---- src/file/mod.rs | 32 +++++--- src/file/source/file.rs | 17 ++++- src/file/source/mod.rs | 7 +- src/file/source/string.rs | 5 +- src/source.rs | 11 ++- 11 files changed, 273 insertions(+), 93 deletions(-) (limited to 'src') diff --git a/src/config.rs b/src/config.rs index 44c51d1..c8fc2e4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use std::ops::Deref; use std::str::FromStr; +use std::fmt::Debug; use serde::de::Deserialize; use error::*; @@ -8,6 +10,7 @@ use source::Source; use value::{Value, ValueWithKey}; use path; +#[derive(Clone, Debug)] enum ConfigKind { // A mutable configuration. This is the default. Mutable { @@ -34,7 +37,7 @@ impl Default for ConfigKind { /// 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)] +#[derive(Default, Clone, Debug)] pub struct Config { kind: ConfigKind, @@ -48,7 +51,7 @@ impl Config { } /// Merge in a configuration property source. - pub fn merge(&mut self, source: T) -> Result<()> + pub fn merge(&mut self, source: T) -> ConfigResult where T: 'static, T: Source + Send + Sync { @@ -58,7 +61,7 @@ impl Config { } ConfigKind::Frozen => { - return Err(ConfigError::Frozen); + return ConfigResult(Err(ConfigError::Frozen)); } } @@ -70,7 +73,7 @@ impl Config { /// /// Configuration is automatically refreshed after a mutation /// operation (`set`, `merge`, `set_default`, etc.). - pub fn refresh(&mut self) -> Result<()> { + pub fn refresh(&mut self) -> ConfigResult { self.cache = match self.kind { // TODO: We need to actually merge in all the stuff ConfigKind::Mutable { @@ -87,14 +90,23 @@ impl Config { // Add sources for source in sources { - let props = source.collect()?; + let props = match source.collect() { + Ok(props) => props, + Err(error) => { + return ConfigResult(Err(error)); + } + }; + for (key, val) in &props { match path::Expression::from_str(key) { // Set using the path Ok(expr) => expr.set(&mut cache, val.clone()), // Set diretly anyway - _ => path::Expression::Identifier(key.clone()).set(&mut cache, val.clone()) + _ => { + path::Expression::Identifier(key.clone()) + .set(&mut cache, val.clone()) + } } } } @@ -105,14 +117,14 @@ impl Config { } cache - }, + } ConfigKind::Frozen => { - return Err(ConfigError::Frozen); + return ConfigResult(Err(ConfigError::Frozen)); } }; - Ok(()) + ConfigResult(Ok(self)) } /// Deserialize the entire configuration. @@ -120,39 +132,41 @@ impl Config { T::deserialize(self.cache.clone()) } - pub fn set_default(&mut self, key: &str, value: T) -> Result<()> + pub fn set_default(&mut self, key: &str, value: T) -> ConfigResult where T: Into { match self.kind { - ConfigKind::Mutable { - ref mut defaults, - .. - } => { - defaults.insert(key.to_lowercase().parse()?, value.into()); + ConfigKind::Mutable { ref mut defaults, .. } => { + defaults.insert(match key.to_lowercase().parse() { + Ok(expr) => expr, + Err(error) => { + return ConfigResult(Err(error)); + } + }, + value.into()); } - ConfigKind::Frozen => { - return Err(ConfigError::Frozen) - } + ConfigKind::Frozen => return ConfigResult(Err(ConfigError::Frozen)), }; self.refresh() } - pub fn set(&mut self, key: &str, value: T) -> Result<()> + pub fn set(&mut self, key: &str, value: T) -> ConfigResult where T: Into { match self.kind { - ConfigKind::Mutable { - ref mut overrides, - .. - } => { - overrides.insert(key.to_lowercase().parse()?, value.into()); + ConfigKind::Mutable { ref mut overrides, .. } => { + overrides.insert(match key.to_lowercase().parse() { + Ok(expr) => expr, + Err(error) => { + return ConfigResult(Err(error)); + } + }, + value.into()); } - ConfigKind::Frozen => { - return Err(ConfigError::Frozen) - } + ConfigKind::Frozen => return ConfigResult(Err(ConfigError::Frozen)), }; self.refresh() @@ -199,3 +213,120 @@ impl Config { self.get(key).and_then(Value::into_array) } } + +pub struct ConfigResult<'a>(Result<&'a mut Config>); + +#[inline] +fn unwrap_failed(msg: &str, error: E) -> ! { + panic!("{}: {:?}", msg, error) +} + +impl<'a> ConfigResult<'a> { + pub fn merge(self, source: T) -> ConfigResult<'a> + where T: 'static, + T: Source + Send + Sync + { + match self.0 { + // If OK, Proceed to nested method + Ok(instance) => instance.merge(source), + + // Else, Forward the error + error => ConfigResult(error), + } + } + + pub fn set_default(self, key: &str, value: T) -> ConfigResult<'a> + where T: Into, + T: 'static + { + match self.0 { + // If OK, Proceed to nested method + Ok(instance) => instance.set_default(key, value), + + // Else, Forward the error + error => ConfigResult(error), + } + } + + pub fn set(self, key: &str, value: T) -> ConfigResult<'a> + where T: Into, + T: 'static + { + match self.0 { + // If OK, Proceed to nested method + Ok(instance) => instance.set(key, value), + + // Else, Forward the error + error => ConfigResult(error), + } + } + + /// Forwards `Result::is_ok` + #[inline] + pub fn is_ok(&self) -> bool { + match self.0 { + Ok(_) => true, + Err(_) => false, + } + } + + /// Forwards `Result::is_err` + #[inline] + pub fn is_err(&self) -> bool { + !self.is_ok() + } + + /// Forwards `Result::ok` + #[inline] + pub fn ok(self) -> Option { + match self.0 { + Ok(x) => Some(x.clone()), + Err(_) => None, + } + } + + /// Forwards `Result::err` + #[inline] + pub fn err(self) -> Option { + match self.0 { + Ok(_) => None, + Err(x) => Some(x), + } + } + + /// Forwards `Result::unwrap` + #[inline] + pub fn unwrap(self) -> Config { + match self.0 { + Ok(instance) => instance.clone(), + Err(error) => unwrap_failed("called `Result::unwrap()` on an `Err` value", error), + } + } + + /// Forwards `Result::expect` + #[inline] + pub fn expect(self, msg: &str) -> Config { + match self.0 { + Ok(instance) => instance.clone(), + Err(error) => unwrap_failed(msg, error), + } + } + + /// Forwards `Result::unwrap_err` + #[inline] + pub fn unwrap_err(self) -> ConfigError { + match self.0 { + Ok(t) => unwrap_failed("called `Result::unwrap_err()` on an `Ok` value", t), + Err(e) => e, + } + } + + /// Forwards `Result::expect_err` + #[inline] + pub fn expect_err(self, msg: &str) -> ConfigError { + match self.0 { + Ok(t) => unwrap_failed(msg, t), + Err(e) => e, + } + } +} diff --git a/src/env.rs b/src/env.rs index a0b190b..abadea2 100644 --- a/src/env.rs +++ b/src/env.rs @@ -4,6 +4,7 @@ use error::*; use source::Source; use value::{Value, ValueKind}; +#[derive(Clone, Debug)] pub struct Environment { /// Optional prefix that will limit access to the environment to only keys that /// begin with the defined prefix. @@ -29,7 +30,10 @@ impl Environment { } pub fn with_prefix(s: &str) -> Self { - Environment { prefix: Some(s.into()), ..Environment::default() } + Environment { + prefix: Some(s.into()), + ..Environment::default() + } } pub fn prefix(&mut self, s: String) -> &mut Self { @@ -53,6 +57,10 @@ impl Default for Environment { } impl Source for Environment { + fn clone_into_box(&self) -> Box { + Box::new((*self).clone()) + } + fn collect(&self) -> Result> { let mut m = HashMap::new(); let uri: String = "the environment".into(); @@ -80,7 +88,8 @@ impl Source for Environment { // Replace `separator` with `.` key = key.replace(&self.separator, "."); - m.insert(key.to_lowercase(), Value::new(Some(&uri), ValueKind::String(value))); + m.insert(key.to_lowercase(), + Value::new(Some(&uri), ValueKind::String(value))); } Ok(m) diff --git a/src/file/format/json.rs b/src/file/format/json.rs index 4a94943..290e17d 100644 --- a/src/file/format/json.rs +++ b/src/file/format/json.rs @@ -4,25 +4,27 @@ use std::collections::HashMap; use std::error::Error; use value::{Value, ValueKind}; -pub fn parse(uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result, Box> { +pub fn parse(uri: Option<&String>, + text: &str, + namespace: Option<&String>) + -> Result, Box> { // Parse a JSON object value from the text let mut root: serde_json::Value = serde_json::from_str(text)?; // Limit to namespace if let Some(namespace) = namespace { root = serde_json::Value::Object(match root { - serde_json::Value::Object(ref mut table) => { - if let Some(serde_json::Value::Object(table)) = table.remove(namespace) { - table - } else { - serde_json::Map::new() - } - } + serde_json::Value::Object(ref mut table) => { + if let Some(serde_json::Value::Object(table)) = + table.remove(namespace) { + table + } else { + serde_json::Map::new() + } + } - _ => { - serde_json::Map::new() - } - }); + _ => serde_json::Map::new(), + }); }; // TODO: Have a proper error fire if the root of a file is ever not a Table @@ -70,8 +72,6 @@ fn from_json_value(uri: Option<&String>, value: &serde_json::Value) -> Value { Value::new(uri, ValueKind::Array(l)) } - serde_json::Value::Null => { - Value::new(uri, ValueKind::Nil) - } + serde_json::Value::Null => Value::new(uri, ValueKind::Nil), } } diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs index 287b9b4..3fc15a6 100644 --- a/src/file/format/mod.rs +++ b/src/file/format/mod.rs @@ -55,7 +55,11 @@ impl FileFormat { // TODO: pub(crate) #[doc(hidden)] #[allow(unused_variables)] - pub fn parse(&self, uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result, Box> { + pub fn parse(&self, + uri: Option<&String>, + text: &str, + namespace: Option<&String>) + -> Result, Box> { match *self { #[cfg(feature = "toml")] FileFormat::Toml => toml::parse(uri, text, namespace), diff --git a/src/file/format/toml.rs b/src/file/format/toml.rs index 633f39e..2df1fca 100644 --- a/src/file/format/toml.rs +++ b/src/file/format/toml.rs @@ -4,25 +4,27 @@ use std::collections::{HashMap, BTreeMap}; use std::error::Error; use value::{Value, ValueKind}; -pub fn parse(uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result, Box> { +pub fn parse(uri: Option<&String>, + text: &str, + namespace: Option<&String>) + -> Result, Box> { // 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() - } - } + toml::Value::Table(ref mut table) => { + if let Some(toml::Value::Table(table)) = + table.remove(namespace) { + table + } else { + BTreeMap::new() + } + } - _ => { - BTreeMap::new() - } - }); + _ => BTreeMap::new(), + }); } // TODO: Have a proper error fire if the root of a file is ever not a Table diff --git a/src/file/format/yaml.rs b/src/file/format/yaml.rs index 7c3dd71..b1afce7 100644 --- a/src/file/format/yaml.rs +++ b/src/file/format/yaml.rs @@ -6,7 +6,10 @@ use std::collections::{BTreeMap, HashMap}; use std::mem; use value::{Value, ValueKind}; -pub fn parse(uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result, Box> { +pub fn parse(uri: Option<&String>, + text: &str, + namespace: Option<&String>) + -> Result, Box> { let mut docs = yaml::YamlLoader::load_from_str(text)?; // Designate root @@ -21,18 +24,17 @@ pub fn parse(uri: Option<&String>, text: &str, namespace: Option<&String>) -> Re // Limit to namespace if let Some(namespace) = namespace { root = yaml::Yaml::Hash(match root { - yaml::Yaml::Hash(ref mut table) => { - if let Some(yaml::Yaml::Hash(table)) = table.remove(&yaml::Yaml::String(namespace.clone())) { - table - } else { - BTreeMap::new() - } - } + yaml::Yaml::Hash(ref mut table) => { + if let Some(yaml::Yaml::Hash(table)) = + table.remove(&yaml::Yaml::String(namespace.clone())) { + table + } else { + BTreeMap::new() + } + } - _ => { - BTreeMap::new() - } - }); + _ => BTreeMap::new(), + }); }; // TODO: Have a proper error fire if the root of a file is ever not a Table @@ -47,7 +49,9 @@ pub fn parse(uri: Option<&String>, text: &str, namespace: Option<&String>) -> Re fn from_yaml_value(uri: Option<&String>, value: &yaml::Yaml) -> Value { match *value { yaml::Yaml::String(ref value) => Value::new(uri, ValueKind::String(value.clone())), - yaml::Yaml::Real(ref value) => Value::new(uri, ValueKind::Float(value.parse::().unwrap())), + yaml::Yaml::Real(ref value) => { + Value::new(uri, ValueKind::Float(value.parse::().unwrap())) + } yaml::Yaml::Integer(value) => Value::new(uri, ValueKind::Integer(value)), yaml::Yaml::Boolean(value) => Value::new(uri, ValueKind::Boolean(value)), yaml::Yaml::Hash(ref table) => { @@ -70,9 +74,7 @@ fn from_yaml_value(uri: Option<&String>, value: &yaml::Yaml) -> Value { Value::new(uri, ValueKind::Array(l)) } - yaml::Yaml::Null => { - Value::new(uri, ValueKind::Nil) - } + yaml::Yaml::Null => Value::new(uri, ValueKind::Nil), // TODO: how should we BadValue? _ => { diff --git a/src/file/mod.rs b/src/file/mod.rs index 7fc25c8..671f579 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -10,6 +10,7 @@ use std::path::Path; use self::source::FileSource; pub use self::format::FileFormat; +#[derive(Clone, Debug)] pub struct File where T: FileSource { @@ -70,12 +71,19 @@ impl File { } } -impl Source for File { +impl Source for File + where T: 'static, + T: Sync + Send +{ + fn clone_into_box(&self) -> Box { + Box::new((*self).clone()) + } + fn collect(&self) -> Result> { // Coerce the file contents to a string - let (uri, contents, format) = match self.source.resolve(self.format).map_err(|err| { - ConfigError::Foreign(err) - }) { + let (uri, contents, format) = match self.source + .resolve(self.format) + .map_err(|err| ConfigError::Foreign(err)) { Ok((uri, contents, format)) => (uri, contents, format), Err(error) => { @@ -88,13 +96,13 @@ impl Source for File { }; // Parse the string using the given format - let result = format.parse(uri.as_ref(), &contents, self.namespace.as_ref()).map_err(|cause| { - ConfigError::FileParse { - uri: uri, - cause: cause - } - }); - - result + format + .parse(uri.as_ref(), &contents, self.namespace.as_ref()) + .map_err(|cause| { + ConfigError::FileParse { + uri: uri, + cause: cause, + } + }) } } diff --git a/src/file/source/file.rs b/src/file/source/file.rs index 79f46bf..426c0b5 100644 --- a/src/file/source/file.rs +++ b/src/file/source/file.rs @@ -3,7 +3,7 @@ use std::result; use std::error::Error; use std::path::{PathBuf, Path}; -use ::file::format::ALL_EXTENSIONS; +use file::format::ALL_EXTENSIONS; use std::io::{self, Read}; use std::fs; use std::env; @@ -13,6 +13,7 @@ use source::Source; use super::{FileFormat, FileSource}; /// Describes a file sourced from a file +#[derive(Clone, Debug)] pub struct FileSourceFile { /// Basename of configuration file name: String, @@ -30,7 +31,9 @@ impl FileSourceFile { } } - fn find_file(&self, format_hint: Option) -> Result<(PathBuf, FileFormat), Box> { + fn find_file(&self, + format_hint: Option) + -> Result<(PathBuf, FileFormat), Box> { // Build expected configuration file let mut basename = PathBuf::new(); @@ -47,7 +50,11 @@ impl FileSourceFile { Some(format) => Ok((filename, format)), None => { for (format, extensions) in ALL_EXTENSIONS.iter() { - if extensions.contains(&filename.extension().unwrap_or_default().to_string_lossy().as_ref()) { + if extensions.contains(&filename + .extension() + .unwrap_or_default() + .to_string_lossy() + .as_ref()) { return Ok((filename, *format)); } } @@ -90,7 +97,9 @@ impl FileSourceFile { } impl FileSource for FileSourceFile { - fn resolve(&self, format_hint: Option) -> Result<(Option, String, FileFormat), Box> { + fn resolve(&self, + format_hint: Option) + -> Result<(Option, String, FileFormat), Box> { // Find file let (filename, format) = self.find_file(format_hint)?; diff --git a/src/file/source/mod.rs b/src/file/source/mod.rs index 89f1a3c..67e2cf1 100644 --- a/src/file/source/mod.rs +++ b/src/file/source/mod.rs @@ -1,12 +1,15 @@ pub mod file; pub mod string; +use std::fmt::Debug; 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, FileFormat), Box>; +pub trait FileSource: Debug + Clone { + fn resolve(&self, + format_hint: Option) + -> Result<(Option, String, FileFormat), Box>; } diff --git a/src/file/source/string.rs b/src/file/source/string.rs index 922e46c..70101d6 100644 --- a/src/file/source/string.rs +++ b/src/file/source/string.rs @@ -6,6 +6,7 @@ use source::Source; use super::{FileSource, FileFormat}; /// Describes a file sourced from a string +#[derive(Clone, Debug)] pub struct FileSourceString(String); impl<'a> From<&'a str> for FileSourceString { @@ -15,7 +16,9 @@ impl<'a> From<&'a str> for FileSourceString { } impl FileSource for FileSourceString { - fn resolve(&self, format_hint: Option) -> Result<(Option, String, FileFormat), Box> { + fn resolve(&self, + format_hint: Option) + -> Result<(Option, String, FileFormat), Box> { Ok((None, self.0.clone(), format_hint.expect("from_str requires a set file format"))) } } diff --git a/src/source.rs b/src/source.rs index 5db362e..c8a0e83 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,10 +1,19 @@ use error::*; +use std::fmt::Debug; use value::Value; use std::collections::HashMap; /// Describes a generic _source_ of configuration properties. -pub trait Source { +pub trait Source: Debug { + fn clone_into_box(&self) -> Box; + /// Collect all configuration properties available from this source and return /// a HashMap. fn collect(&self) -> Result>; } + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_into_box() + } +} -- cgit v1.2.3