diff options
author | Ryan Leckey <leckey.ryan@gmail.com> | 2017-02-02 12:03:55 -0800 |
---|---|---|
committer | Ryan Leckey <leckey.ryan@gmail.com> | 2017-02-02 12:03:55 -0800 |
commit | 115fe07e2c11aa72e91a5ce9b028ed1c1ff7d806 (patch) | |
tree | 48ccdb6417c3c7c9593a9c447157e3eba724f1a8 /src/file | |
parent | d913d951f1ff040a543f9ac859a0f300ce4d8434 (diff) |
Add support for Table/Array and deep merging of configuration values
Diffstat (limited to 'src/file')
-rw-r--r-- | src/file/json.rs | 65 | ||||
-rw-r--r-- | src/file/mod.rs | 123 | ||||
-rw-r--r-- | src/file/nil.rs | 6 | ||||
-rw-r--r-- | src/file/toml.rs | 60 |
4 files changed, 154 insertions, 100 deletions
diff --git a/src/file/json.rs b/src/file/json.rs index 5f1c3a2..d193da4 100644 --- a/src/file/json.rs +++ b/src/file/json.rs @@ -2,7 +2,7 @@ use serde_json; use source::Source; use std::error::Error; -use std::borrow::Cow; +use std::collections::HashMap; use value::Value; pub struct Content { @@ -19,50 +19,57 @@ impl Content { } } -fn from_json_value<'a>(value: &serde_json::Value) -> Option<Cow<'a, Value>> { +fn from_json_value(value: &serde_json::Value) -> Value { match *value { serde_json::Value::String(ref value) => { - Some(Cow::Owned(Value::String(Cow::Borrowed(value)))) + Value::String(value.clone()) } serde_json::Value::Number(ref value) => { if let Some(value) = value.as_i64() { - Some(Cow::Owned(Value::Integer(value))) + Value::Integer(value) } else if let Some(value) = value.as_f64() { - Some(Cow::Owned(Value::Float(value))) + Value::Float(value) } else { - None + unreachable!(); } } - serde_json::Value::Bool(value) => Some(Cow::Owned(Value::Boolean(value))), + serde_json::Value::Bool(value) => Value::Boolean(value), - _ => None, - } -} + serde_json::Value::Object(ref table) => { + let mut m = HashMap::new(); -impl Source for Content { - fn get<'a>(&self, key: &str) -> Option<Cow<'a, 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; - } - } + for (key, value) in table { + m.insert(key.clone(), from_json_value(value)); + } + + Value::Table(m) + } + + serde_json::Value::Array(ref array) => { + let mut l = Vec::new(); - _ => { - // This is not a table or array - // Traversal is not possible - return None; - } + for value in array { + l.push(from_json_value(value)); } + + Value::Array(l) } - from_json_value(json_cursor) + // TODO: What's left is JSON Null; how should we handle that? + _ => { unimplemented!(); } + } +} + +impl Source for Content { + fn collect(&self) -> HashMap<String, Value> { + if let Value::Table(table) = from_json_value(&self.root) { + table + } else { + // TODO: Better handle a non-object at root + // NOTE: I never want to support that but a panic is bad + panic!("expected object at JSON root"); + } } } diff --git a/src/file/mod.rs b/src/file/mod.rs index 6b9fd5c..e85d082 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -14,6 +14,7 @@ mod toml; #[cfg(feature = "json")] mod json; +#[derive(Clone, Copy)] pub enum FileFormat { /// TOML (parsed with toml) #[cfg(feature = "toml")] @@ -47,53 +48,34 @@ impl FileFormat { } } -pub struct File { +pub trait FileSource { + fn try_build(&self, format: FileFormat) -> Result<Box<Source>, Box<Error>>; +} + +pub struct FileSourceString(String); + +impl FileSource for FileSourceString { + fn try_build(&self, format: FileFormat) -> Result<Box<Source>, Box<Error>> { + format.parse(&self.0) + } +} + +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<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(self, path: &str) -> File { - File { path: Some(path.into()), ..self } - } - - pub fn namespace(self, namespace: &str) -> File { - File { namespace: Some(namespace.into()), ..self } - } - - pub fn required(self, required: bool) -> File { - File { required: required, ..self } - } - +impl FileSourceFile { // Find configuration file // Use algorithm similar to .git detection by git - fn find_file(&self) -> Result<PathBuf, Box<Error>> { + fn find_file(&self, format: FileFormat) -> Result<PathBuf, Box<Error>> { // Build expected configuration file let mut basename = PathBuf::new(); - let extensions = self.format.extensions(); + let extensions = format.extensions(); if let Some(ref path) = self.path { basename.push(path.clone()); @@ -125,11 +107,12 @@ impl File { } } } +} - // Build normally and return error on failure - fn try_build(&self) -> Result<Box<Source>, Box<Error>> { +impl FileSource for FileSourceFile { + fn try_build(&self, format: FileFormat) -> Result<Box<Source>, Box<Error>> { // Find file - let filename = self.find_file()?; + let filename = self.find_file(format)?; // Read contents from file let mut file = fs::File::open(filename)?; @@ -137,11 +120,71 @@ impl File { file.read_to_string(&mut text)?; // Parse the file - self.format.parse(&text) + format.parse(&text) + } +} + +pub struct File<T: FileSource> { + /// Source of the file + source: T, + + /// 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<FileSourceString> { + pub fn from_str(s: &str, format: FileFormat) -> File<FileSourceString> { + File { + format: format, + required: true, + namespace: None, + source: FileSourceString(s.into()), + } + } +} + +impl File<FileSourceFile> { + pub fn new(name: &str, format: FileFormat) -> File<FileSourceFile> { + File { + format: format, + required: true, + namespace: None, + source: FileSourceFile { + name: name.into(), + path: None, + } + } + } +} + +impl<T: FileSource> File<T> { + pub fn required(self, required: bool) -> File<T> { + File { required: required, ..self } + } + + // Build normally and return error on failure + fn try_build(&self) -> Result<Box<Source>, Box<Error>> { + self.source.try_build(self.format) + } +} + +impl File<FileSourceFile> { + pub fn path(self, path: &str) -> Self { + File { source: FileSourceFile { path: Some(path.into()), ..self.source } , ..self } + } + + pub fn namespace(self, namespace: &str) -> Self { + File { namespace: Some(namespace.into()), ..self } } } -impl SourceBuilder for File { +impl<T: FileSource> SourceBuilder for File<T> { // Use try_build but only pass an error through if this source // is required fn build(&self) -> Result<Box<Source>, Box<Error>> { diff --git a/src/file/nil.rs b/src/file/nil.rs index f494af4..f6d801a 100644 --- a/src/file/nil.rs +++ b/src/file/nil.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::collections::HashMap; use source::Source; use value::Value; @@ -7,7 +7,7 @@ use value::Value; pub struct Nil {} impl Source for Nil { - fn get<'a>(&self, _: &str) -> Option<Cow<'a, Value>> { - None + fn collect(&self) -> HashMap<String, Value> { + HashMap::new() } } diff --git a/src/file/toml.rs b/src/file/toml.rs index b2a8fe8..4de23ef 100644 --- a/src/file/toml.rs +++ b/src/file/toml.rs @@ -1,6 +1,6 @@ use toml; use source::Source; -use std::borrow::Cow; +use std::collections::HashMap; use std::error::Error; use value::Value; @@ -20,39 +20,43 @@ impl Content { } } -fn from_toml_value<'a>(value: &toml::Value) -> Option<Cow<'a, Value>> { +fn from_toml_value(value: &toml::Value) -> Value { match *value { - toml::Value::String(ref value) => Some(Cow::Owned(Value::String(Cow::Borrowed(value)))), - toml::Value::Float(value) => Some(Cow::Owned(Value::Float(value))), - toml::Value::Integer(value) => Some(Cow::Owned(Value::Integer(value))), - toml::Value::Boolean(value) => Some(Cow::Owned(Value::Boolean(value))), + toml::Value::String(ref value) => Value::String(value.clone()), + toml::Value::Float(value) => Value::Float(value), + toml::Value::Integer(value) => Value::Integer(value), + toml::Value::Boolean(value) => Value::Boolean(value), - _ => None, + toml::Value::Table(ref table) => { + let mut m = HashMap::new(); + + for (key, value) in table { + m.insert(key.clone(), from_toml_value(value)); + } + + Value::Table(m) + } + + toml::Value::Array(ref array) => { + let mut l = Vec::new(); + + for value in array { + l.push(from_toml_value(value)); + } + + Value::Array(l) + } + + _ => { unimplemented!(); } } } impl Source for Content { - fn get<'a>(&self, key: &str) -> Option<Cow<'a, 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; - } - } + fn collect(&self) -> HashMap<String, Value> { + if let Value::Table(table) = from_toml_value(&self.root) { + table + } else { + unreachable!(); } - - from_toml_value(toml_cursor) } } |