summaryrefslogtreecommitdiffstats
path: root/src/file
diff options
context:
space:
mode:
authorRyan Leckey <ryan@launchbadge.com>2017-06-01 23:22:04 -0700
committerRyan Leckey <ryan@launchbadge.com>2017-06-01 23:22:04 -0700
commitbfc44c331a77d8c341c076e72df5ed0b56fbd422 (patch)
treec757723957be6b880d1e0d8d26ae2b1c9c606ed2 /src/file
parent4357840e95f3646494ddeea4aae12425dfab2db8 (diff)
Move things around and get some tests in place
Diffstat (limited to 'src/file')
-rw-r--r--src/file/format/mod.rs60
-rw-r--r--src/file/format/toml.rs64
-rw-r--r--src/file/mod.rs75
-rw-r--r--src/file/source/file.rs129
-rw-r--r--src/file/source/mod.rs12
-rw-r--r--src/file/source/string.rs21
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()))
+ }
+}