summaryrefslogtreecommitdiffstats
path: root/src/file
diff options
context:
space:
mode:
authorRyan Leckey <leckey.ryan@gmail.com>2017-02-02 12:03:55 -0800
committerRyan Leckey <leckey.ryan@gmail.com>2017-02-02 12:03:55 -0800
commit115fe07e2c11aa72e91a5ce9b028ed1c1ff7d806 (patch)
tree48ccdb6417c3c7c9593a9c447157e3eba724f1a8 /src/file
parentd913d951f1ff040a543f9ac859a0f300ce4d8434 (diff)
Add support for Table/Array and deep merging of configuration values
Diffstat (limited to 'src/file')
-rw-r--r--src/file/json.rs65
-rw-r--r--src/file/mod.rs123
-rw-r--r--src/file/nil.rs6
-rw-r--r--src/file/toml.rs60
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)
}
}