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 | |
parent | 4357840e95f3646494ddeea4aae12425dfab2db8 (diff) |
Move things around and get some tests in place
Diffstat (limited to 'src')
-rw-r--r-- | src/config.rs | 104 | ||||
-rw-r--r-- | src/de.rs | 179 | ||||
-rw-r--r-- | src/error.rs | 155 | ||||
-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 | ||||
-rw-r--r-- | src/lib.rs | 25 | ||||
-rw-r--r-- | src/path/mod.rs | 41 | ||||
-rw-r--r-- | src/path/parser.rs | 120 | ||||
-rw-r--r-- | src/source.rs | 9 | ||||
-rw-r--r-- | src/value.rs | 328 |
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 |