summaryrefslogtreecommitdiffstats
path: root/src
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
parent4357840e95f3646494ddeea4aae12425dfab2db8 (diff)
Move things around and get some tests in place
Diffstat (limited to 'src')
-rw-r--r--src/config.rs104
-rw-r--r--src/de.rs179
-rw-r--r--src/error.rs155
-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
-rw-r--r--src/lib.rs25
-rw-r--r--src/path/mod.rs41
-rw-r--r--src/path/parser.rs120
-rw-r--r--src/source.rs9
-rw-r--r--src/value.rs328
14 files changed, 1322 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..4d24a1d
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,104 @@
+use std::collections::HashMap;
+use serde::de::Deserialize;
+
+use error::*;
+use source::Source;
+use value::Value;
+use path;
+
+enum ConfigKind {
+ // A mutable configuration. This is the default.
+ Mutable {
+ defaults: HashMap<String, Value>,
+ overrides: HashMap<String, Value>,
+ sources: Vec<Box<Source + Send + Sync>>,
+ },
+
+ // A frozen configuration.
+ // Configuration can no longer be mutated.
+ Frozen,
+}
+
+impl Default for ConfigKind {
+ fn default() -> Self {
+ ConfigKind::Mutable {
+ defaults: HashMap::new(),
+ overrides: HashMap::new(),
+ sources: Vec::new(),
+ }
+ }
+}
+
+/// A prioritized configuration repository. It maintains a set of
+/// configuration sources, fetches values to populate those, and provides
+/// them according to the source's priority.
+#[derive(Default)]
+pub struct Config {
+ kind: ConfigKind,
+
+ /// Root of the cached configuration.
+ pub cache: Value,
+}
+
+impl Config {
+ /// Merge in a configuration property source.
+ pub fn merge<T>(&mut self, source: T) -> Result<()>
+ where T: 'static,
+ T: Source + Send + Sync
+ {
+ match self.kind {
+ ConfigKind::Mutable { ref mut sources, .. } => {
+ sources.push(Box::new(source));
+ }
+
+ ConfigKind::Frozen => {
+ return Err(ConfigError::Frozen);
+ }
+ }
+
+ self.refresh()
+ }
+
+ /// Refresh the configuration cache with fresh
+ /// data from added sources.
+ ///
+ /// Configuration is automatically refreshed after a mutation
+ /// operation (`set`, `merge`, `set_default`, etc.).
+ pub fn refresh(&mut self) -> Result<()> {
+ self.cache = match self.kind {
+ // TODO: We need to actually merge in all the stuff
+ ConfigKind::Mutable {
+ ref overrides,
+ ref sources,
+ ref defaults,
+ } => sources[0].collect()?,
+
+ ConfigKind::Frozen => {
+ return Err(ConfigError::Frozen);
+ }
+ };
+
+ Ok(())
+ }
+
+ pub fn deserialize<T: Deserialize>(&self) -> Result<T> {
+ return T::deserialize(self.cache.clone());
+ }
+
+ pub fn get<T: Deserialize>(&self, key: &str) -> Result<T> {
+ // Parse the key into a path expression
+ let expr: path::Expression = key.to_lowercase().parse()?;
+
+ // Traverse the cache using the path to (possibly) retrieve a value
+ let value = expr.get(&self.cache).cloned();
+
+ match value {
+ Some(value) => {
+ // Deserialize the received value into the requested type
+ T::deserialize(value)
+ }
+
+ None => Err(ConfigError::NotFound(key.into())),
+ }
+ }
+}
diff --git a/src/de.rs b/src/de.rs
new file mode 100644
index 0000000..9a9ef58
--- /dev/null
+++ b/src/de.rs
@@ -0,0 +1,179 @@
+use serde::de;
+use value::{Value, ValueKind};
+use error::*;
+use std::borrow::Cow;
+use std::iter::Peekable;
+use std::collections::HashMap;
+use std::collections::hash_map::Drain;
+
+impl de::Deserializer for Value {
+ type Error = ConfigError;
+
+ #[inline]
+ fn deserialize<V>(self, visitor: V) -> Result<V::Value>
+ where V: de::Visitor
+ {
+ // Deserialize based on the underlying type
+ match self.kind {
+ ValueKind::Integer(i) => visitor.visit_i64(i),
+ ValueKind::Boolean(b) => visitor.visit_bool(b),
+ ValueKind::Float(f) => visitor.visit_f64(f),
+ ValueKind::String(s) => visitor.visit_string(s),
+ ValueKind::Array(values) => unimplemented!(),
+ ValueKind::Table(map) => visitor.visit_map(MapVisitor::new(map)),
+ _ => {
+ unimplemented!();
+ }
+ }
+ }
+
+ #[inline]
+ fn deserialize_bool<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_bool(self.into_bool()?)
+ }
+
+ #[inline]
+ fn deserialize_i8<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_i8(self.into_int()? as i8)
+ }
+
+ #[inline]
+ fn deserialize_i16<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_i16(self.into_int()? as i16)
+ }
+
+ #[inline]
+ fn deserialize_i32<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_i32(self.into_int()? as i32)
+ }
+
+ #[inline]
+ fn deserialize_i64<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_i64(self.into_int()?)
+ }
+
+ #[inline]
+ fn deserialize_u8<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_u8(self.into_int()? as u8)
+ }
+
+ #[inline]
+ fn deserialize_u16<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_u16(self.into_int()? as u16)
+ }
+
+ #[inline]
+ fn deserialize_u32<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_u32(self.into_int()? as u32)
+ }
+
+ #[inline]
+ fn deserialize_u64<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_u64(self.into_int()? as u64)
+ }
+
+ #[inline]
+ fn deserialize_f32<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_f32(self.into_float()? as f32)
+ }
+
+ #[inline]
+ fn deserialize_f64<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_f64(self.into_float()?)
+ }
+
+ #[inline]
+ fn deserialize_str<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_string(self.into_str()?)
+ }
+
+ #[inline]
+ fn deserialize_string<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_string(self.into_str()?)
+ }
+
+ #[inline]
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
+ where V: de::Visitor
+ {
+ // Match an explicit nil as None and everything else as Some
+ match self.kind {
+ ValueKind::Nil => visitor.visit_none(),
+ _ => visitor.visit_some(self),
+ }
+ }
+
+ forward_to_deserialize! {
+ char seq
+ seq_fixed_size bytes byte_buf map struct unit enum newtype_struct
+ struct_field ignored_any unit_struct tuple_struct tuple
+ }
+}
+
+struct StrDeserializer<'a>(&'a str);
+
+impl<'a> StrDeserializer<'a> {
+ fn new(key: &'a str) -> Self {
+ StrDeserializer(key)
+ }
+}
+
+impl<'a> de::Deserializer for StrDeserializer<'a> {
+ type Error = ConfigError;
+
+ #[inline]
+ fn deserialize<V: de::Visitor>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_str(self.0)
+ }
+
+ forward_to_deserialize! {
+ bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
+ seq_fixed_size bytes byte_buf map struct unit enum newtype_struct
+ struct_field ignored_any unit_struct tuple_struct tuple option
+ }
+}
+
+struct MapVisitor {
+ elements: Vec<(String, Value)>,
+ index: usize,
+}
+
+impl MapVisitor {
+ fn new(mut table: HashMap<String, Value>) -> Self {
+ MapVisitor {
+ elements: table.drain().collect(),
+ index: 0,
+ }
+ }
+}
+
+impl de::MapVisitor for MapVisitor {
+ type Error = ConfigError;
+
+ fn visit_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
+ where K: de::DeserializeSeed
+ {
+ if self.index >= self.elements.len() {
+ return Ok(None);
+ }
+
+ let key_s = &self.elements[0].0;
+ let key_de = StrDeserializer(key_s);
+ let key = de::DeserializeSeed::deserialize(seed, key_de)?;
+
+ Ok(Some(key))
+ }
+
+ fn visit_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
+ where V: de::DeserializeSeed
+ {
+ de::DeserializeSeed::deserialize(seed, self.elements.remove(0).1)
+ }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..b97ebac
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,155 @@
+use std::error::Error;
+use std::borrow::Cow;
+use std::result;
+use std::fmt;
+use serde::de;
+use nom;
+
+#[derive(Debug)]
+pub enum Unexpected {
+ Bool(bool),
+ Integer(i64),
+ Float(f64),
+ Str(String),
+ Unit,
+ Seq,
+ Map
+}
+
+impl fmt::Display for Unexpected {
+ fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
+ match *self {
+ Unexpected::Bool(b) => write!(f, "boolean `{}`", b),
+ Unexpected::Integer(i) => write!(f, "integer `{}`", i),
+ Unexpected::Float(v) => write!(f, "floating point `{}`", v),
+ Unexpected::Str(ref s) => write!(f, "string {:?}", s),
+ Unexpected::Unit => write!(f, "unit value"),
+ Unexpected::Seq => write!(f, "sequence"),
+ Unexpected::Map => write!(f, "map"),
+ }
+ }
+}
+
+/// Represents all possible errors that can occur when working with
+/// configuration.
+pub enum ConfigError {
+ /// Configuration is frozen and no further mutations can be made.
+ Frozen,
+
+ /// Configuration property was not found
+ NotFound(String),
+
+ /// Configuration path could not be parsed.
+ PathParse(nom::ErrorKind),
+
+ /// Configuration could not be parsed from file.
+ FileParse { uri: Option<String>, cause: Box<Error> },
+
+ /// Value could not be converted into the requested type.
+ Type {
+ origin: Option<String>,
+ unexpected: Unexpected,
+ expected: &'static str,
+ },
+
+ /// Custom message
+ Message(String),
+
+ /// Unadorned error from a foreign source.
+ Foreign(Box<Error>),
+}
+
+impl ConfigError {
+ // FIXME: pub(crate)
+ #[doc(hidden)]
+ pub fn invalid_type(origin: Option<String>, unexpected: Unexpected, expected: &'static str) -> Self {
+ ConfigError::Type {
+ origin: origin,
+ unexpected: unexpected,
+ expected: expected
+ }
+ }
+}
+
+/// Alias for a `Result` with the error type set to `ConfigError`.
+pub type Result<T> = result::Result<T, ConfigError>;
+
+// Forward Debug to Display for readable panic! messages
+impl fmt::Debug for ConfigError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", *self)
+ }
+}
+
+impl fmt::Display for ConfigError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ ConfigError::Frozen | ConfigError::PathParse(_) => {
+ write!(f, "{}", self.description())
+ }
+
+ ConfigError::Message(ref s) => {
+ write!(f, "{}", s)
+ }
+
+ ConfigError::Foreign(ref cause) => {
+ write!(f, "{}", cause)
+ }
+
+ ConfigError::NotFound(ref key) => {
+ write!(f, "configuration property {:?} not found", key)
+ }
+
+ ConfigError::Type { ref origin, ref unexpected, expected } => {
+ write!(f, "invalid type: {}, expected {}",
+ unexpected, expected)?;
+
+ if let Some(ref origin) = *origin {
+ write!(f, " in {}", origin)?;
+ }
+
+ Ok(())
+ }
+
+ ConfigError::FileParse { ref cause, ref uri } => {
+ write!(f, "{}", cause)?;
+
+ if let Some(ref uri) = *uri {
+ write!(f, " in {}", uri)?;
+ }
+
+ Ok(())
+ }
+ }
+ }
+}
+
+impl Error for ConfigError {
+ fn description(&self) -> &str {
+ match *self {
+ ConfigError::Frozen => "configuration is frozen",
+ ConfigError::NotFound(_) => "configuration property not found",
+ ConfigError::Type { .. } => "invalid type",
+ ConfigError::Foreign(ref cause) => cause.description(),
+ ConfigError::FileParse { ref cause, .. } => cause.description(),
+ ConfigError::PathParse(ref kind) => kind.description(),
+
+ _ => "configuration error",
+ }
+ }
+
+ fn cause(&self) -> Option<&Error> {
+ match *self {
+ ConfigError::Foreign(ref cause) => Some(cause.as_ref()),
+ ConfigError::FileParse { ref cause, .. } => Some(cause.as_ref()),
+
+ _ => None
+ }
+ }
+}
+
+impl de::Error for ConfigError {
+ fn custom<T: fmt::Display>(msg: T) -> Self {
+ ConfigError::Message(msg.to_string())
+ }
+}
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()))
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..212e621
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,25 @@
+#![allow(dead_code)]
+#![allow(unused_imports)]
+#![allow(unused_variables)]
+
+#[macro_use]
+extern crate serde;
+
+extern crate nom;
+
+#[cfg(feature = "toml")]
+extern crate toml;
+
+mod error;
+mod value;
+mod de;
+mod path;
+mod source;
+mod config;
+mod file;
+
+pub use config::Config;
+pub use error::ConfigError;
+pub use value::Value;
+pub use source::Source;
+pub use file::{File, FileFormat};
diff --git a/src/path/mod.rs b/src/path/mod.rs
new file mode 100644
index 0000000..f889283
--- /dev/null
+++ b/src/path/mod.rs
@@ -0,0 +1,41 @@
+use std::str::FromStr;
+use nom::ErrorKind;
+use error::*;
+use value::{Value, ValueKind};
+
+mod parser;
+
+#[derive(Debug, Eq, PartialEq, Clone, Hash)]
+pub enum Expression {
+ Identifier(String),
+ Child(Box<Expression>, String),
+ Subscript(Box<Expression>, i32),
+}
+
+impl FromStr for Expression {
+ type Err = ConfigError;
+
+ fn from_str(s: &str) -> Result<Expression> {
+ parser::from_str(s).map_err(|kind| ConfigError::PathParse(kind))
+ }
+}
+
+impl Expression {
+ pub fn get<'a>(self, root: &'a Value) -> Option<&'a Value> {
+ match self {
+ Expression::Identifier(id) => {
+ match root.kind {
+ // `x` access on a table is equivalent to: map[x]
+ ValueKind::Table(ref map) => map.get(&id),
+
+ // all other variants return None
+ _ => None,
+ }
+ }
+
+ _ => {
+ unimplemented!();
+ }
+ }
+ }
+}
diff --git a/src/path/parser.rs b/src/path/parser.rs
new file mode 100644
index 0000000..eea4343
--- /dev/null
+++ b/src/path/parser.rs
@@ -0,0 +1,120 @@
+use nom::*;
+use std::str::{FromStr, from_utf8};
+use super::Expression;
+
+named!(ident_<String>,
+ map!(
+ map_res!(is_a!(
+ "abcdefghijklmnopqrstuvwxyz \
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ \
+ 0123456789 \
+ _-"
+ ), from_utf8),
+ |s: &str| {
+ s.to_string()
+ }
+ )
+);
+
+named!(integer <i32>,
+ map_res!(
+ map_res!(
+ ws!(digit),
+ from_utf8
+ ),
+ FromStr::from_str
+ )
+);
+
+named!(ident<Expression>, map!(ident_, Expression::Identifier));
+
+fn postfix(expr: Expression) -> Box<Fn(&[u8]) -> IResult<&[u8], Expression>> {
+ return Box::new(move |i: &[u8]| {
+ alt!(i,
+ do_parse!(
+ tag!(".") >>
+ id: ident_ >>
+ (Expression::Child(Box::new(expr.clone()), id))
+ ) |
+ delimited!(
+ char!('['),
+ do_parse!(
+ negative: opt!(tag!("-")) >>
+ num: integer >>
+ (Expression::Subscript(
+ Box::new(expr.clone()),
+ num * (if negative.is_none() { 1 } else { -1 })
+ ))
+ ),
+ char!(']')
+ )
+ )
+ });
+}
+
+pub fn from_str(input: &str) -> Result<Expression, ErrorKind> {
+ match ident(input.as_bytes()) {
+ IResult::Done(mut rem, mut expr) => {
+ while rem.len() > 0 {
+ match postfix(expr)(rem) {
+ IResult::Done(rem_, expr_) => {
+ rem = rem_;
+ expr = expr_;
+ }
+
+ // Forward Incomplete and Error
+ result @ _ => {
+ return result.to_result();
+ }
+ }
+ }
+
+ Ok(expr)
+ }
+
+ // Forward Incomplete and Error
+ result @ _ => result.to_result(),
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use super::Expression::*;
+
+ #[test]
+ fn test_id() {
+ let parsed: Expression = from_str("abcd").unwrap();
+ assert_eq!(parsed, Identifier("abcd".into()));
+ }
+
+ #[test]
+ fn test_id_dash() {
+ let parsed: Expression = from_str("abcd-efgh").unwrap();
+ assert_eq!(parsed, Identifier("abcd-efgh".into()));
+ }
+
+ #[test]
+ fn test_child() {
+ let parsed: Expression = from_str("abcd.efgh").unwrap();
+ let expected = Child(Box::new(Identifier("abcd".into())), "efgh".into());
+
+ assert_eq!(parsed, expected);
+ }
+
+ #[test]
+ fn test_subscript() {
+ let