From 589036c19409c7626a5bafda5af96c2fbc6a7de5 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 26 Jan 2017 19:02:13 -0800 Subject: Refactor the file source to allow for N formats; implement JSON. --- src/config.rs | 46 ++++++---------- src/file.rs | 143 -------------------------------------------------- src/file/json.rs | 65 +++++++++++++++++++++++ src/file/mod.rs | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/file/nil.rs | 11 ++++ src/file/toml.rs | 57 ++++++++++++++++++++ src/lib.rs | 19 ++++--- src/source.rs | 7 ++- src/value.rs | 53 ++++++++++--------- 9 files changed, 349 insertions(+), 209 deletions(-) delete mode 100644 src/file.rs create mode 100644 src/file/json.rs create mode 100644 src/file/mod.rs create mode 100644 src/file/nil.rs create mode 100644 src/file/toml.rs (limited to 'src') diff --git a/src/config.rs b/src/config.rs index 22bf2c4..42e6c5c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,9 @@ use value::Value; -use source::Source; +use source::{Source, SourceBuilder}; use std::env; use std::error::Error; use std::collections::HashMap; -use std::borrow::Cow; #[derive(Default)] pub struct Config { @@ -12,8 +11,7 @@ pub struct Config { defaults: HashMap, overrides: HashMap, - environ: HashMap, - sources: Vec>, + sources: Vec>, } impl Config { @@ -22,8 +20,8 @@ impl Config { } /// Merge in configuration values from the given source. - pub fn merge(&mut self, mut source: T) -> Result<(), Box> - where T: Source + pub fn merge(&mut self, source: T) -> Result<(), Box> + where T: SourceBuilder { self.sources.push(source.build()?); @@ -54,11 +52,11 @@ impl Config { self.overrides.insert(key.to_lowercase(), value.into()); } - pub fn get<'a>(&'a mut self, key: &str) -> Option<&'a Value> { + pub fn get(&self, key: &str) -> Option { // Check explicit override if let Some(value) = self.overrides.get(key) { - return Some(value); + return Some(value.clone()); } // Check environment @@ -75,9 +73,7 @@ impl Config { env_key.push_str(&key.to_uppercase()); if let Ok(value) = env::var(env_key.clone()) { - // TODO: Find a better way to do this? - self.environ.insert(key.into(), value.into()); - return self.environ.get(key); + return Some(Value::from(value)); } // Check sources @@ -91,38 +87,26 @@ impl Config { // Check explicit defaults if let Some(value) = self.defaults.get(key) { - return Some(value); + return Some(value.clone()); } None } - pub fn get_str<'a>(&'a mut self, key: &str) -> Option> { + pub fn get_str(&self, key: &str) -> Option { self.get(key).and_then(Value::as_str) } - pub fn get_int(&mut self, key: &str) -> Option { - if let Some(value) = self.get(key) { - value.as_int() - } else { - None - } + pub fn get_int(&self, key: &str) -> Option { + self.get(key).and_then(Value::as_int) } - pub fn get_float(&mut self, key: &str) -> Option { - if let Some(value) = self.get(key) { - value.as_float() - } else { - None - } + pub fn get_float(&self, key: &str) -> Option { + self.get(key).and_then(Value::as_float) } - pub fn get_bool(&mut self, key: &str) -> Option { - if let Some(value) = self.get(key) { - value.as_bool() - } else { - None - } + pub fn get_bool(&self, key: &str) -> Option { + self.get(key).and_then(Value::as_bool) } } diff --git a/src/file.rs b/src/file.rs deleted file mode 100644 index 2ffc836..0000000 --- a/src/file.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::fs; -use std::env; -use std::error::Error; -use std::io::Read; -use std::collections::HashMap; - -use toml; - -use value::Value; -use source::Source; - -#[derive(Default)] -pub struct File { - /// Basename of configuration file - name: String, - - /// Directory where configuration file is found - /// When not specified, the current working directory (CWD) is considered - path: Option, - - /// Namespace to restrict configuration from the file - namespace: Option, - - /// A required File will error if it cannot be found - required: bool, -} - -impl File { - pub fn with_name(name: &str) -> File { - File { - name: name.into(), - required: true, - - ..Default::default() - } - } - - pub fn path(&mut self, path: &str) -> &mut File { - self.path = Some(path.into()); - self - } - - pub fn namespace(&mut self, namespace: &str) -> &mut File { - self.namespace = Some(namespace.into()); - self - } - - pub fn required(&mut self, required: bool) -> &mut File { - self.required = required; - self - } -} - -fn toml_collect(content: &mut HashMap, - table: &toml::Table, - prefix: Option) { - for (key, value) in table { - // Construct full key from prefix - let key = if let Some(ref prefix) = prefix { - prefix.clone() + "." + key - } else { - key.clone() - }; - - match *value { - // Recurse into nested table - toml::Value::Table(ref table) => toml_collect(content, table, Some(key)), - - toml::Value::String(ref value) => { - content.insert(key, value.clone().into()); - } - - toml::Value::Integer(value) => { - content.insert(key, value.into()); - } - - toml::Value::Float(value) => { - content.insert(key, value.into()); - } - - toml::Value::Boolean(value) => { - content.insert(key, value.into()); - } - - _ => { - // Unhandled - } - } - } -} - -impl Source for File { - fn build(&mut self) -> Result, Box> { - let mut content = HashMap::new(); - - // Find file - // TODO: Use a nearest algorithm rather than strictly CWD - let cwd = match env::current_dir() { - Ok(cwd) => cwd, - Err(err) => { - if self.required { - return Err(From::from(err)); - } else { - return Ok(content); - } - } - }; - - let filename = cwd.join(self.name.clone() + ".toml"); - - // Read contents from file - let mut file = match fs::File::open(filename) { - Ok(file) => file, - Err(err) => { - if self.required { - return Err(From::from(err)); - } else { - return Ok(content); - } - } - }; - - let mut buffer = String::new(); - let res = file.read_to_string(&mut buffer); - if res.is_err() { - if self.required { - return Err(From::from(res.err().unwrap())); - } else { - return Ok(content); - } - } - - // Parse - let mut parser = toml::Parser::new(&buffer); - // TODO: Get a solution to make this return an Error-able - let document = parser.parse().unwrap(); - - // Iterate through document and fill content - toml_collect(&mut content, &document, None); - - Ok(content) - } -} diff --git a/src/file/json.rs b/src/file/json.rs new file mode 100644 index 0000000..05af065 --- /dev/null +++ b/src/file/json.rs @@ -0,0 +1,65 @@ +use serde_json; + +use source::Source; +use std::error::Error; +use value::Value; + +pub struct Content { + // Root table of the TOML document + root: serde_json::Value, +} + +impl Content { + pub fn parse(text: &str) -> Result, Box> { + // Parse + let root = serde_json::from_str(text)?; + + Ok(Box::new(Content { root: root })) + } +} + +fn from_json_value(value: &serde_json::Value) -> Option { + match *value { + serde_json::Value::String(ref value) => Some(Value::String(value.clone())), + + serde_json::Value::Number(ref value) => { + if let Some(value) = value.as_i64() { + Some(Value::Integer(value)) + } else if let Some(value) = value.as_f64() { + Some(Value::Float(value)) + } else { + None + } + } + + serde_json::Value::Bool(value) => Some(Value::Boolean(value)), + + _ => None, + } +} + +impl Source for Content { + fn get(&self, key: &str) -> Option { + // TODO: Key segment iteration is not something that should be here directly + let key_delim = '.'; + let key_segments = key.split(key_delim); + let mut json_cursor = &self.root; + for segment in key_segments { + match *json_cursor { + serde_json::Value::Object(ref table) => { + if let Some(value) = table.get(segment) { + json_cursor = value; + } + } + + _ => { + // This is not a table or array + // Traversal is not possible + return None; + } + } + } + + from_json_value(json_cursor) + } +} diff --git a/src/file/mod.rs b/src/file/mod.rs new file mode 100644 index 0000000..2177a54 --- /dev/null +++ b/src/file/mod.rs @@ -0,0 +1,157 @@ +use std::env; +use std::error::Error; +use std::io::{self, Read}; +use std::fs; +use std::path::PathBuf; + +use source::{Source, SourceBuilder}; + +mod nil; + +#[cfg(feature = "toml")] +mod toml; + +#[cfg(feature = "json")] +mod json; + +pub enum FileFormat { + /// TOML (parsed with toml) + #[cfg(feature = "toml")] + Toml, + + /// JSON (parsed with serde_json) + #[cfg(feature = "json")] + Json, +} + +impl FileFormat { + fn extensions(&self) -> Vec<&'static str> { + match *self { + #[cfg(feature = "toml")] + FileFormat::Toml => vec!["toml"], + + #[cfg(feature = "json")] + FileFormat::Json => vec!["json"], + } + } + + #[allow(unused_variables)] + fn parse(&self, text: &str) -> Result, Box> { + match *self { + #[cfg(feature = "toml")] + FileFormat::Toml => toml::Content::parse(text), + + #[cfg(feature = "json")] + FileFormat::Json => json::Content::parse(text), + } + } +} + +pub struct File { + /// Basename of configuration file + name: String, + + /// Directory where configuration file is found + /// When not specified, the current working directory (CWD) is considered + path: Option, + + /// Namespace to restrict configuration from the file + namespace: Option, + + /// Format of file (which dictates what driver to use); Defauts to TOML. + format: FileFormat, + + /// A required File will error if it cannot be found + required: bool, +} + +impl File { + pub fn new(name: &str, format: FileFormat) -> File { + File { + name: name.into(), + format: format, + required: true, + path: None, + namespace: None, + } + } + + pub fn path(&mut self, path: &str) -> &mut File { + self.path = Some(path.into()); + self + } + + pub fn namespace(&mut self, namespace: &str) -> &mut File { + self.namespace = Some(namespace.into()); + self + } + + pub fn required(&mut self, required: bool) -> &mut File { + self.required = required; + self + } + + // Find configuration file + // Use algorithm similar to .git detection by git + fn find_file(&self) -> Result> { + // Build expected configuration file + let mut basename = PathBuf::new(); + let extensions = self.format.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()?; + + loop { + let mut filename = dir.as_path().join(basename.clone()); + 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(io::Error::new(io::ErrorKind::NotFound, + format!("configuration file \"{}\" not found", + basename.to_string_lossy())) + .into()); + } + } + } + + // Build normally and return error on failure + fn try_build(&self) -> Result, Box> { + // Find file + let filename = self.find_file()?; + + // Read contents from file + let mut file = fs::File::open(filename)?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + + // Parse the file + self.format.parse(&text) + } +} + +impl SourceBuilder for File { + // Use try_build but only pass an error through if this source + // is required + fn build(&self) -> Result, Box> { + if self.required { + self.try_build().or_else(|_| Ok(Box::new(nil::Nil {}))) + } else { + self.try_build() + } + } +} diff --git a/src/file/nil.rs b/src/file/nil.rs new file mode 100644 index 0000000..7666f8a --- /dev/null +++ b/src/file/nil.rs @@ -0,0 +1,11 @@ +use source::Source; +use value::Value; + +// Nil source that does nothing for optional files +pub struct Nil {} + +impl Source for Nil { + fn get(&self, _: &str) -> Option { + None + } +} diff --git a/src/file/toml.rs b/src/file/toml.rs new file mode 100644 index 0000000..9e4b170 --- /dev/null +++ b/src/file/toml.rs @@ -0,0 +1,57 @@ +use toml; +use source::Source; +use std::error::Error; +use value::Value; + +pub struct Content { + // Root table of the TOML document + root: toml::Value, +} + +impl Content { + pub fn parse(text: &str) -> Result, Box> { + // Parse + let mut parser = toml::Parser::new(text); + // TODO: Get a solution to make this return an Error-able + let root = parser.parse().unwrap(); + + Ok(Box::new(Content { root: toml::Value::Table(root) })) + } +} + +fn from_toml_value(value: &toml::Value) -> Option { + match *value { + toml::Value::String(ref value) => Some(Value::String(value.clone())), + toml::Value::Float(value) => Some(Value::Float(value)), + toml::Value::Integer(value) => Some(Value::Integer(value)), + toml::Value::Boolean(value) => Some(Value::Boolean(value)), + + _ => None, + } +} + +impl Source for Content { + fn get(&self, key: &str) -> Option { + // TODO: Key segment iteration is not something that should be here directly + let key_delim = '.'; + let key_segments = key.split(key_delim); + let mut toml_cursor = &self.root; + for segment in key_segments { + match *toml_cursor { + toml::Value::Table(ref table) => { + if let Some(value) = table.get(segment) { + toml_cursor = value; + } + } + + _ => { + // This is not a table or array + // Traversal is not possible + return None; + } + } + } + + from_toml_value(toml_cursor) + } +} diff --git a/src/lib.rs b/src/lib.rs index 426b526..99a6da3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,22 @@ #![feature(drop_types_in_const)] #![allow(unknown_lints)] +#[cfg(feature = "toml")] extern crate toml; +#[cfg(feature = "json")] +extern crate serde_json; + mod value; mod source; mod file; mod config; use std::error::Error; -use std::borrow::Cow; use std::sync::{Once, ONCE_INIT}; -pub use source::Source; -pub use file::File; +pub use source::{Source, SourceBuilder}; +pub use file::{File, FileFormat}; pub use value::Value; @@ -26,14 +29,16 @@ static CONFIG_INIT: Once = ONCE_INIT; // Get the global configuration instance fn global() -> &'static mut Config { unsafe { - CONFIG_INIT.call_once(|| { CONFIG = Some(Default::default()); }); + CONFIG_INIT.call_once(|| { + CONFIG = Some(Default::default()); + }); CONFIG.as_mut().unwrap() } } pub fn merge(source: T) -> Result<(), Box> - where T: Source + where T: SourceBuilder { global().merge(source) } @@ -54,11 +59,11 @@ pub fn set(key: &str, value: T) global().set(key, value) } -pub fn get<'a>(key: &str) -> Option<&'a Value> { +pub fn get(key: &str) -> Option { global().get(key) } -pub fn get_str<'a>(key: &str) -> Option> { +pub fn get_str(key: &str) -> Option { global().get_str(key) } diff --git a/src/source.rs b/src/source.rs index 95785a6..1a8d08f 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,8 +1,11 @@ use std::error::Error; -use std::collections::HashMap; use value::Value; pub trait Source { - fn build(&mut self) -> Result, Box>; + fn get(&self, key: &str) -> Option; +} + +pub trait SourceBuilder { + fn build(&self) -> Result, Box>; } diff --git a/src/value.rs b/src/value.rs index 52a2103..a887104 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,5 +1,5 @@ use std::convert::From; -use std::borrow::Cow; +use std::collections::HashMap; /// A configuration value. /// @@ -11,38 +11,39 @@ pub enum Value { Integer(i64), Float(f64), Boolean(bool), + Table(HashMap), + Array(Vec), } impl Value { /// Gets the underyling value as a string, performing a conversion only if neccessary. - #[allow(needless_lifetimes)] - pub fn as_str<'a>(&'a self) -> Option> { - if let Value::String(ref value) = *self { - Some(Cow::Borrowed(value)) - } else if let Value::Integer(value) = *self { - Some(Cow::Owned(value.to_string())) - } else if let Value::Float(value) = *self { - Some(Cow::Owned(value.to_string())) - } else if let Value::Boolean(value) = *self { - Some(Cow::Owned(value.to_string())) + pub fn as_str(self) -> Option { + if let Value::String(value) = self { + Some(value) + } else if let Value::Integer(value) = self { + Some(value.to_string()) + } else if let Value::Float(value) = self { + Some(value.to_string()) + } else if let Value::Boolean(value) = self { + Some(value.to_string()) } else { None } } /// Gets the underlying type as a boolean, performing a conversion only if neccessary. - pub fn as_bool(&self) -> Option { - if let Value::Boolean(value) = *self { + pub fn as_bool(self) -> Option { + if let Value::Boolean(value) = self { Some(value) - } else if let Value::String(ref value) = *self { + } else if let Value::String(ref value) = self { match value.to_lowercase().as_ref() { "1" | "true" | "on" | "yes" => Some(true), "0" | "false" | "off" | "no" => Some(false), _ => None, } - } else if let Value::Integer(value) = *self { + } else if let Value::Integer(value) = self { Some(value != 0) - } else if let Value::Float(value) = *self { + } else if let Value::Float(value) = self { Some(value != 0.0) } else { None @@ -50,14 +51,14 @@ impl Value { } /// Gets the underlying type as an integer, performing a conversion only if neccessary. - pub fn as_int(&self) -> Option { - if let Value::Integer(value) = *self { + pub fn as_int(self) -> Option { + if let Value::Integer(value) = self { Some(value) - } else if let Value::String(ref value) = *self { + } else if let Value::String(ref value) = self { value.parse().ok() - } else if let Value::Boolean(value) = *self { + } else if let Value::Boolean(value) = self { Some(if value { 1 } else { 0 }) - } else if let Value::Float(value) = *self { + } else if let Value::Float(value) = self { Some(value.round() as i64) } else { None @@ -65,14 +66,14 @@ impl Value { } /// Gets the underlying type as a floating-point, performing a conversion only if neccessary. - pub fn as_float(&self) -> Option { - if let Value::Float(value) = *self { + pub fn as_float(self) -> Option { + if let Value::Float(value) = self { Some(value) - } else if let Value::String(ref value) = *self { + } else if let Value::String(ref value) = self { value.parse().ok() - } else if let Value::Integer(value) = *self { + } else if let Value::Integer(value) = self { Some(value as f64) - } else if let Value::Boolean(value) = *self { + } else if let Value::Boolean(value) = self { Some(if value { 1.0 } else { 0.0 }) } else { None -- cgit v1.2.3