diff options
author | Ryan Leckey <leckey.ryan@gmail.com> | 2017-01-26 19:02:13 -0800 |
---|---|---|
committer | Ryan Leckey <leckey.ryan@gmail.com> | 2017-01-26 19:02:13 -0800 |
commit | 589036c19409c7626a5bafda5af96c2fbc6a7de5 (patch) | |
tree | 9f4e09809df7c200c3d60b619c7eea9b91e6d70c /src/file | |
parent | d7ad51c8851fe3c845569760935fae1828e859b5 (diff) |
Refactor the file source to allow for N formats; implement JSON.
Diffstat (limited to 'src/file')
-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 |
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) + } +} |