summaryrefslogtreecommitdiffstats
path: root/src/file
diff options
context:
space:
mode:
authorRyan Leckey <leckey.ryan@gmail.com>2017-01-26 19:02:13 -0800
committerRyan Leckey <leckey.ryan@gmail.com>2017-01-26 19:02:13 -0800
commit589036c19409c7626a5bafda5af96c2fbc6a7de5 (patch)
tree9f4e09809df7c200c3d60b619c7eea9b91e6d70c /src/file
parentd7ad51c8851fe3c845569760935fae1828e859b5 (diff)
Refactor the file source to allow for N formats; implement JSON.
Diffstat (limited to 'src/file')
-rw-r--r--src/file/json.rs65
-rw-r--r--src/file/mod.rs157
-rw-r--r--src/file/nil.rs11
-rw-r--r--src/file/toml.rs57
4 files changed, 290 insertions, 0 deletions
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)
+ }
+}