diff options
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | examples/basic/Cargo.toml | 3 | ||||
-rw-r--r-- | examples/file-json/Cargo.toml | 6 | ||||
-rw-r--r-- | examples/file-json/Settings.json | 5 | ||||
-rw-r--r-- | examples/file-json/src/main.rs | 10 | ||||
-rw-r--r-- | examples/file-toml/Cargo.toml (renamed from examples/basic-file/Cargo.toml) | 3 | ||||
-rw-r--r-- | examples/file-toml/src/main.rs (renamed from examples/basic-file/src/main.rs) | 2 | ||||
-rw-r--r-- | src/config.rs | 46 | ||||
-rw-r--r-- | src/file.rs | 143 | ||||
-rw-r--r-- | src/file/json.rs | 65 | ||||
-rw-r--r-- | src/file/mod.rs | 157 | ||||
-rw-r--r-- | src/file/nil.rs | 11 | ||||
-rw-r--r-- | src/file/toml.rs | 57 | ||||
-rw-r--r-- | src/lib.rs | 19 | ||||
-rw-r--r-- | src/source.rs | 7 | ||||
-rw-r--r-- | src/value.rs | 53 |
16 files changed, 379 insertions, 215 deletions
@@ -3,5 +3,10 @@ name = "config" version = "0.1.0" authors = ["Ryan Leckey <leckey.ryan@gmail.com>"] +[features] +default = ["toml"] +json = ["serde_json"] + [dependencies] -toml = "0.2.1" +toml = { version = "0.2.1", optional = true } +serde_json = { version = "0.9", optional = true } diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index 2cb273b..7ede162 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "basic" version = "0.1.0" -authors = ["Ryan Leckey <leckey.ryan@gmail.com>"] [dependencies] -config = { path = "../.." } +config = { path = "../..", default-features = false } diff --git a/examples/file-json/Cargo.toml b/examples/file-json/Cargo.toml new file mode 100644 index 0000000..7223f35 --- /dev/null +++ b/examples/file-json/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "file-json" +version = "0.1.0" + +[dependencies] +config = { path = "../..", default-features = false, features = ["json"] } diff --git a/examples/file-json/Settings.json b/examples/file-json/Settings.json new file mode 100644 index 0000000..72b28e6 --- /dev/null +++ b/examples/file-json/Settings.json @@ -0,0 +1,5 @@ +{ + "debug": false, + "pi": 3.14159, + "weight": 150 +} diff --git a/examples/file-json/src/main.rs b/examples/file-json/src/main.rs new file mode 100644 index 0000000..9c636ac --- /dev/null +++ b/examples/file-json/src/main.rs @@ -0,0 +1,10 @@ +extern crate config; + +fn main() { + // Read configuration from "Settings.json" + config::merge(config::File::new("Settings", config::FileFormat::Json)).unwrap(); + + println!("debug = {:?}", config::get("debug")); + println!("pi = {:?}", config::get("pi")); + println!("weight = {:?}", config::get("weight")); +} diff --git a/examples/basic-file/Cargo.toml b/examples/file-toml/Cargo.toml index ffef864..6f45799 100644 --- a/examples/basic-file/Cargo.toml +++ b/examples/file-toml/Cargo.toml @@ -1,7 +1,6 @@ [package] -name = "basic-file" +name = "file-toml" version = "0.1.0" -authors = ["Ryan Leckey <leckey.ryan@gmail.com>"] [dependencies] config = { path = "../.." } diff --git a/examples/basic-file/src/main.rs b/examples/file-toml/src/main.rs index ae394f9..03ce61a 100644 --- a/examples/basic-file/src/main.rs +++ b/examples/file-toml/src/main.rs @@ -2,7 +2,7 @@ extern crate config; fn main() { // Read configuration from $(cwd)/Cargo.toml - config::merge(config::File::with_name("Cargo")).unwrap(); + config::merge(config::File::new("Cargo", config::FileFormat::Toml)).unwrap(); println!("package.name = {:?}", config::get_str("package.name")); println!("package.version = {:?}", config::get_str("package.version")); 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<String, Value>, overrides: HashMap<String, Value>, - environ: HashMap<String, Value>, - sources: Vec<HashMap<String, Value>>, + sources: Vec<Box<Source>>, } impl Config { @@ -22,8 +20,8 @@ impl Config { } /// Merge in configuration values from the given source. - pub fn merge<T>(&mut self, mut source: T) -> Result<(), Box<Error>> - where T: Source + pub fn merge<T>(&mut self, source: T) -> Result<(), Box<Error>> + 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<Value> { // 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<Cow<'a, str>> { + pub fn get_str(&self, key: &str) -> Option<String> { self.get(key).and_then(Value::as_str) } - pub fn get_int(&mut self, key: &str) -> Option<i64> { - if let Some(value) = self.get(key) { - value.as_int() - } else { - None - } + pub fn get_int(&self, key: &str) -> Option<i64> { + self.get(key).and_then(Value::as_int) } - pub fn get_float(&mut self, key: &str) -> Option<f64> { - if let Some(value) = self.get(key) { - value.as_float() - } else { - None - } + pub fn get_float(&self, key: &str) -> Option<f64> { + self.get(key).and_then(Value::as_float) } - pub fn get_bool(&mut self, key: &str) -> Option<bool> { - if let Some(value) = self.get(key) { - value.as_bool() - } else { - None - } + pub fn get_bool(&self, key: &str) -> Option<bool> { + 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<String>, - - /// Namespace to restrict configuration from the file - namespace: Option<String>, - - /// 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<String, Value>, - table: &toml::Table, - prefix: Option<String>) { - 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<HashMap<String, Value>, Box<Error>> { - 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<Source>, Box<Error>> { + // Parse + let root = serde_json::from_str(text)?; + + Ok(Box::new(Content { root: root })) + } +} + +fn from_json_value(value: &serde_json::Value) -> Option<Value> { + 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<Value> { + // 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<Source>, Box<Error>> { + 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<String>, + + /// Namespace to restrict configuration from the file + namespace: Option<String>, + + /// 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<PathBuf, Box<Error>> { + // 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<Source>, Box<Error>> { + // 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<Source>, Box<Error>> { + 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<Value> { + 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<Source>, Box<Error>> { + // 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<Value> { + 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<Value> { + // 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) + } +} @@ -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<T>(source: T) -> Result<(), Box<Error>> - where T: Source + where T: SourceBuilder { global().merge(source) } @@ -54,11 +59,11 @@ pub fn set<T>(key: &str, value: T) global().set(key, value) } -pub fn get<'a>(key: &str) -> Option<&'a Value> { +pub fn get(key: &str) -> Option<Value> { global().get(key) } -pub fn get_str<'a>(key: &str) -> Option<Cow<'a, str>> { +pub fn get_str(key: &str) -> Option<String> { 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<HashMap<String, Value>, Box<Error>>; + fn get(&self, key: &str) -> Option<Value>; +} + +pub trait SourceBuilder { + fn build(&self) -> Result<Box<Source>, Box<Error>>; } 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<String, Value>), + Array(Vec<Value>), } 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<Cow<'a, str>> { - 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<String> { + 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<bool> { - if let Value::Boolean(value) = *self { + pub fn as_bool(self) -> Option<bool> { + 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<i64> { - if let Value::Integer(value) = *self { + pub fn as_int(self) -> Option<i64> { + 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<f64> { - if let Value::Float(value) = *self { + pub fn as_float(self) -> Option<f64> { + 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 |