diff options
author | Ryan Leckey <ryan@launchbadge.com> | 2017-06-01 23:22:04 -0700 |
---|---|---|
committer | Ryan Leckey <ryan@launchbadge.com> | 2017-06-01 23:22:04 -0700 |
commit | bfc44c331a77d8c341c076e72df5ed0b56fbd422 (patch) | |
tree | c757723957be6b880d1e0d8d26ae2b1c9c606ed2 /src/file | |
parent | 4357840e95f3646494ddeea4aae12425dfab2db8 (diff) |
Move things around and get some tests in place
Diffstat (limited to 'src/file')
-rw-r--r-- | src/file/format/mod.rs | 60 | ||||
-rw-r--r-- | src/file/format/toml.rs | 64 | ||||
-rw-r--r-- | src/file/mod.rs | 75 | ||||
-rw-r--r-- | src/file/source/file.rs | 129 | ||||
-rw-r--r-- | src/file/source/mod.rs | 12 | ||||
-rw-r--r-- | src/file/source/string.rs | 21 |
6 files changed, 361 insertions, 0 deletions
diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs new file mode 100644 index 0000000..5c97a7f --- /dev/null +++ b/src/file/format/mod.rs @@ -0,0 +1,60 @@ +use source::Source; +use value::Value; +use std::error::Error; + +#[cfg(feature = "toml")] +mod toml; + +// #[cfg(feature = "json")] +// mod json; + +// #[cfg(feature = "yaml")] +// mod yaml; + +#[derive(Debug, Clone, Copy)] +pub enum FileFormat { + /// TOML (parsed with toml) + #[cfg(feature = "toml")] + Toml, + + // /// JSON (parsed with serde_json) + // #[cfg(feature = "json")] + // Json, + + // /// YAML (parsed with yaml_rust) + // #[cfg(feature = "yaml")] + // Yaml, +} + +impl FileFormat { + // TODO: pub(crate) + #[doc(hidden)] + pub fn extensions(&self) -> Vec<&'static str> { + match *self { + #[cfg(feature = "toml")] + FileFormat::Toml => vec!["toml"], + + // #[cfg(feature = "json")] + // FileFormat::Json => vec!["json"], + + // #[cfg(feature = "yaml")] + // FileFormat::Yaml => vec!["yaml", "yml"], + } + } + + // TODO: pub(crate) + #[doc(hidden)] + #[allow(unused_variables)] + pub fn parse(&self, uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result<Value, Box<Error>> { + match *self { + #[cfg(feature = "toml")] + FileFormat::Toml => toml::parse(uri, text, namespace), + + // #[cfg(feature = "json")] + // FileFormat::Json => json::Content::parse(text, namespace), + + // #[cfg(feature = "yaml")] + // FileFormat::Yaml => yaml::Content::parse(text, namespace), + } + } +} diff --git a/src/file/format/toml.rs b/src/file/format/toml.rs new file mode 100644 index 0000000..bbe6aa6 --- /dev/null +++ b/src/file/format/toml.rs @@ -0,0 +1,64 @@ +use toml; +use source::Source; +use std::collections::{HashMap, BTreeMap}; +use std::error::Error; +use value::Value; + +pub fn parse(uri: Option<&String>, text: &str, namespace: Option<&String>) -> Result<Value, Box<Error>> { + // Parse a TOML value from the provided text + let mut root: toml::Value = toml::from_str(text)?; + + // Limit to namespace + if let Some(namespace) = namespace { + root = toml::Value::Table(match root { + toml::Value::Table(ref mut table) => { + if let Some(toml::Value::Table(table)) = table.remove(namespace) { + table + } else { + BTreeMap::new() + } + } + + _ => { + BTreeMap::new() + } + }); + } + + Ok(from_toml_value(uri, &root)) +} + +// TODO: Extend value origin with line/column numbers when able +fn from_toml_value(uri: Option<&String>, value: &toml::Value) -> Value { + match *value { + toml::Value::String(ref value) => Value::new(uri, value.to_string()), + toml::Value::Float(value) => Value::new(uri, value), + toml::Value::Integer(value) => Value::new(uri, value), + toml::Value::Boolean(value) => Value::new(uri, value), + + toml::Value::Table(ref table) => { + let mut m = HashMap::new(); + + for (key, value) in table { + m.insert(key.clone(), from_toml_value(uri, value)); + } + + Value::new(uri, m) + } + + toml::Value::Array(ref array) => { + let mut l = Vec::new(); + + for value in array { + l.push(from_toml_value(uri, value)); + } + + Value::new(uri, l) + } + + _ => { + // TODO: DateTime + unimplemented!(); + } + } +} diff --git a/src/file/mod.rs b/src/file/mod.rs new file mode 100644 index 0000000..7534ddb --- /dev/null +++ b/src/file/mod.rs @@ -0,0 +1,75 @@ +mod format; +pub mod source; + +use source::Source; +use error::*; +use value::Value; + +use self::source::FileSource; +pub use self::format::FileFormat; + +pub struct File<T> + where T: FileSource +{ + source: T, + + /// Namespace to restrict configuration from the file + namespace: Option<String>, + + /// Format of file (which dictates what driver to use). + format: Option<FileFormat>, + + /// A required File will error if it cannot be found + required: bool, +} + +impl File<source::string::FileSourceString> { + pub fn from_str(s: &str, format: FileFormat) -> Self { + File { + format: Some(format), + required: true, + namespace: None, + source: s.into(), + } + } +} + +impl File<source::file::FileSourceFile> { + pub fn new(name: &str, format: FileFormat) -> Self { + File { + format: Some(format), + required: true, + namespace: None, + source: source::file::FileSourceFile::new(name), + } + } +} + +impl<T: FileSource> File<T> { + pub fn required(&mut self, required: bool) -> &mut Self { + self.required = required; + self + } + + pub fn namespace(&mut self, namespace: &str) -> &mut Self { + self.namespace = Some(namespace.into()); + self + } +} + +impl<T: FileSource> Source for File<T> { + fn collect(&self) -> Result<Value> { + // Coerce the file contents to a string + let (uri, contents) = self.source.resolve(self.format).map_err(|err| { + ConfigError::Foreign(err) + })?; + + // Parse the string using the given format + self.format.unwrap().parse(uri.as_ref(), &contents, self.namespace.as_ref()).map_err(|cause| { + ConfigError::FileParse { + uri: uri, + cause: cause + } + }) + } +} diff --git a/src/file/source/file.rs b/src/file/source/file.rs new file mode 100644 index 0000000..124b7dd --- /dev/null +++ b/src/file/source/file.rs @@ -0,0 +1,129 @@ +use std::str::FromStr; +use std::result; +use std::error::Error; + +use std::path::{PathBuf, Path}; +use std::io::{self, Read}; +use std::fs; +use std::env; + +use source::Source; +use super::{FileFormat, FileSource}; + +/// Describes a file sourced from a file +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>, +} + +impl FileSourceFile { + pub fn new(name: &str) -> FileSourceFile { + FileSourceFile { + name: name.into(), + path: None, + } + } + + fn find_file(&self, format_hint: Option<FileFormat>) -> Result<PathBuf, Box<Error>> { + // Build expected configuration file + let mut basename = PathBuf::new(); + let extensions = format_hint.unwrap().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()?; + let mut filename = dir.as_path().join(basename.clone()); + + loop { + 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(Box::new(io::Error::new(io::ErrorKind::NotFound, + format!("configuration file \"{}\" not found", + basename.to_string_lossy())) + )); + } + } + } +} + +impl FileSource for FileSourceFile { + fn resolve(&self, format_hint: Option<FileFormat>) -> Result<(Option<String>, String), Box<Error>> { + // Find file + let filename = self.find_file(format_hint)?; + + // Attempt to use a relative path for the URI + let base = env::current_dir()?; + let uri = match path_relative_from(&filename, &base) { + Some(value) => value, + None => filename.clone(), + }; + + // Read contents from file + let mut file = fs::File::open(filename.clone())?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + + Ok((Some(uri.to_string_lossy().into_owned()), text)) + } +} + +// TODO: This should probably be a crate +// https://github.com/rust-lang/rust/blob/master/src/librustc_trans/back/rpath.rs#L128 +fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> { + use std::path::Component; + + if path.is_absolute() != base.is_absolute() { + if path.is_absolute() { + Some(PathBuf::from(path)) + } else { + None + } + } else { + let mut ita = path.components(); + let mut itb = base.components(); + let mut comps: Vec<Component> = vec![]; + loop { + match (ita.next(), itb.next()) { + (None, None) => break, + (Some(a), None) => { + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + (None, _) => comps.push(Component::ParentDir), + (Some(a), Some(b)) if comps.is_empty() && a == b => (), + (Some(a), Some(b)) if b == Component::CurDir => comps.push(a), + (Some(_), Some(b)) if b == Component::ParentDir => return None, + (Some(a), Some(_)) => { + comps.push(Component::ParentDir); + for _ in itb { + comps.push(Component::ParentDir); + } + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + } + } + Some(comps.iter().map(|c| c.as_os_str()).collect()) + } +} diff --git a/src/file/source/mod.rs b/src/file/source/mod.rs new file mode 100644 index 0000000..4aeafa5 --- /dev/null +++ b/src/file/source/mod.rs @@ -0,0 +1,12 @@ +pub mod file; +pub mod string; + +use std::error::Error; + +use source::Source; +use super::FileFormat; + +/// Describes where the file is sourced +pub trait FileSource { + fn resolve(&self, format_hint: Option<FileFormat>) -> Result<(Option<String>, String), Box<Error>>; +} diff --git a/src/file/source/string.rs b/src/file/source/string.rs new file mode 100644 index 0000000..e1d9f64 --- /dev/null +++ b/src/file/source/string.rs @@ -0,0 +1,21 @@ +use std::str::FromStr; +use std::result; +use std::error::Error; + +use source::Source; +use super::{FileSource, FileFormat}; + +/// Describes a file sourced from a string +pub struct FileSourceString(String); + +impl<'a> From<&'a str> for FileSourceString { + fn from(s: &'a str) -> Self { + FileSourceString(s.into()) + } +} + +impl FileSource for FileSourceString { + fn resolve(&self, _: Option<FileFormat>) -> Result<(Option<String>, String), Box<Error>> { + Ok((None, self.0.clone())) + } +} |