diff --git a/imag-store/src/retrieve.rs b/imag-store/src/retrieve.rs index 311f6bd1..2c0d19c5 100644 --- a/imag-store/src/retrieve.rs +++ b/imag-store/src/retrieve.rs @@ -50,7 +50,7 @@ fn print_entry(rt: &Runtime, scmd: &ArgMatches, e: FileLockEntry) { } else { debug!("Printing header as TOML..."); // We have to Value::Table() for Display - println!("{}", Value::Table(entry.get_header().toml().clone())) + println!("{}", Value::Table(entry.get_header().clone().into())) } } diff --git a/imag-store/src/util.rs b/imag-store/src/util.rs index 6ebb3a2d..987ab4f3 100644 --- a/imag-store/src/util.rs +++ b/imag-store/src/util.rs @@ -43,7 +43,7 @@ pub fn build_entry_path(rt: &Runtime, path_elem: &str) -> PathBuf { pub fn build_toml_header(matches: &ArgMatches, mut header: EntryHeader) -> EntryHeader { debug!("Building header from cli spec"); if let Some(headerspecs) = matches.values_of("header") { - let mut main = header.toml_mut(); + let mut main = header.into(); debug!("headerspec = {:?}", headerspecs); let kvs = headerspecs.into_iter() .filter_map(|hs| { @@ -61,9 +61,13 @@ pub fn build_toml_header(matches: &ArgMatches, mut header: EntryHeader) -> Entry insert_key_into(String::from(current.unwrap()), &mut split, value, &mut main); } } + + debug!("Header = {:?}", main); + EntryHeader::from(main) + } else { + debug!("Header = {:?}", header); + header } - debug!("Header = {:?}", header); - header } fn insert_key_into(current: String, diff --git a/libimagentryfilter/src/builtin/header/field_eq.rs b/libimagentryfilter/src/builtin/header/field_eq.rs index fc4e1e77..5617a42b 100644 --- a/libimagentryfilter/src/builtin/header/field_eq.rs +++ b/libimagentryfilter/src/builtin/header/field_eq.rs @@ -25,10 +25,9 @@ impl FieldEq { impl Filter for FieldEq { fn filter(&self, e: &Entry) -> bool { - let header = e.get_header(); - self.header_field_path - .walk(header) - .map(|v| self.expected_value == v.clone()) + e.get_header() + .read(&self.header_field_path[..]) + .map(|val| val.map(|v| v == self.expected_value).unwrap_or(false)) .unwrap_or(false) } diff --git a/libimagentryfilter/src/builtin/header/field_exists.rs b/libimagentryfilter/src/builtin/header/field_exists.rs index 45bc870a..ea964cb8 100644 --- a/libimagentryfilter/src/builtin/header/field_exists.rs +++ b/libimagentryfilter/src/builtin/header/field_exists.rs @@ -22,8 +22,7 @@ impl FieldExists { impl Filter for FieldExists { fn filter(&self, e: &Entry) -> bool { - let header = e.get_header(); - self.header_field_path.walk(header).is_some() + e.get_header().read(&self.header_field_path[..]).is_ok() } } diff --git a/libimagentryfilter/src/builtin/header/field_grep.rs b/libimagentryfilter/src/builtin/header/field_grep.rs index f1535602..2c28c9cf 100644 --- a/libimagentryfilter/src/builtin/header/field_grep.rs +++ b/libimagentryfilter/src/builtin/header/field_grep.rs @@ -26,13 +26,12 @@ impl FieldGrep { impl Filter for FieldGrep { fn filter(&self, e: &Entry) -> bool { - let header = e.get_header(); - self.header_field_path - .walk(header) + e.get_header() + .read(&self.header_field_path[..]) .map(|v| { match v { - Value::String(s) => self.grep.captures(&s[..]).is_some(), - _ => false, + Some(Value::String(s)) => self.grep.captures(&s[..]).is_some(), + _ => false, } }) .unwrap_or(false) diff --git a/libimagentryfilter/src/builtin/header/field_isempty.rs b/libimagentryfilter/src/builtin/header/field_isempty.rs index 83f74984..a6a5b3d3 100644 --- a/libimagentryfilter/src/builtin/header/field_isempty.rs +++ b/libimagentryfilter/src/builtin/header/field_isempty.rs @@ -22,18 +22,17 @@ impl FieldIsEmpty { impl Filter for FieldIsEmpty { fn filter(&self, e: &Entry) -> bool { - let header = e.get_header(); - self.header_field_path - .walk(header) + e.get_header() + .read(&self.header_field_path[..]) .map(|v| { match v { - Value::Array(a) => a.is_empty(), - Value::Boolean(_) => false, - Value::Float(_) => false, - Value::Integer(_) => false, - Value::String(_) => false, - Value::Table(t) => t.is_empty(), - _ => true, + Some(Value::Array(a)) => a.is_empty(), + Some(Value::Boolean(_)) => false, + Some(Value::Float(_)) => false, + Some(Value::Integer(_)) => false, + Some(Value::String(_)) => false, + Some(Value::Table(t)) => t.is_empty(), + _ => true, } }) .unwrap_or(false) diff --git a/libimagentryfilter/src/builtin/header/field_istype.rs b/libimagentryfilter/src/builtin/header/field_istype.rs index 23191611..6ff6d511 100644 --- a/libimagentryfilter/src/builtin/header/field_istype.rs +++ b/libimagentryfilter/src/builtin/header/field_istype.rs @@ -50,10 +50,9 @@ impl FieldIsType { impl Filter for FieldIsType { fn filter(&self, e: &Entry) -> bool { - let header = e.get_header(); - self.header_field_path - .walk(header) - .map(|v| self.expected_type.matches(&v)) + e.get_header() + .read(&self.header_field_path[..]) + .map(|val| val.map(|v| self.expected_type.matches(&v)).unwrap_or(false)) .unwrap_or(false) } diff --git a/libimagentryfilter/src/builtin/header/field_path.rs b/libimagentryfilter/src/builtin/header/field_path.rs new file mode 100644 index 00000000..b61f32b9 --- /dev/null +++ b/libimagentryfilter/src/builtin/header/field_path.rs @@ -0,0 +1 @@ +pub type FieldPath = String; diff --git a/libimagentryfilter/src/builtin/header/field_path/element.rs b/libimagentryfilter/src/builtin/header/field_path/element.rs deleted file mode 100644 index d6ad599a..00000000 --- a/libimagentryfilter/src/builtin/header/field_path/element.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::fmt::{Display, Formatter}; -use std::fmt::Error as FmtError; - -use toml::Value; - -#[derive(Clone, Debug)] -pub struct FieldPathElement { - name: String -} - -impl FieldPathElement { - - pub fn new(name: String) -> FieldPathElement { - FieldPathElement { name: name } - } - - pub fn apply(&self, value: Value) -> Option { - use std::str::FromStr; - use std::ops::Index; - - match value { - Value::Table(t) => { - t.get(&self.name).map(|a| a.clone()) - }, - - Value::Array(a) => { - usize::from_str(&self.name[..]) - .ok() - .and_then(|i| Some(a[i].clone())) - }, - - _ => None, - } - } - -} - -impl Display for FieldPathElement { - - fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { - try!(write!(fmt, "{}", self.name)); - Ok(()) - } - -} - - diff --git a/libimagentryfilter/src/builtin/header/field_path/error.rs b/libimagentryfilter/src/builtin/header/field_path/error.rs deleted file mode 100644 index 69703cbf..00000000 --- a/libimagentryfilter/src/builtin/header/field_path/error.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::fmt::{Display, Formatter}; -use std::fmt::Error as FmtError; -use std::error::Error; - -use builtin::header::field_path::element::FieldPathElement; - -#[derive(Debug)] -pub struct FieldPathParsingError { - source: String, - token: FieldPathElement -} - -impl FieldPathParsingError { - - pub fn new(source: String, token: FieldPathElement) -> FieldPathParsingError { - FieldPathParsingError { source: source, token: token } - } -} - -impl Display for FieldPathParsingError { - - fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { - try!(write!(fmt, "Failed to compile '{}', failed at: '{}'", self.source, self.token)); - Ok(()) - } - -} - -impl Error for FieldPathParsingError { - - fn description(&self) -> &str { - &self.source[..] - } - - fn cause(&self) -> Option<&Error> { - None - } - -} diff --git a/libimagentryfilter/src/builtin/header/field_path/mod.rs b/libimagentryfilter/src/builtin/header/field_path/mod.rs deleted file mode 100644 index 0760c596..00000000 --- a/libimagentryfilter/src/builtin/header/field_path/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::fmt::{Display, Formatter}; -use std::fmt::Error as FmtError; -use std::error::Error; - -use toml::Value; - -pub mod element; -pub mod error; - -use libimagstore::store::Entry; -use libimagstore::store::EntryHeader; - -use builtin::header::field_path::element::FieldPathElement; -use builtin::header::field_path::error::FieldPathParsingError; - -pub struct FieldPath { - elements: Vec, -} - -impl FieldPath { - - pub fn new(elements: Vec) -> FieldPath { - FieldPath { - elements: elements, - } - } - - pub fn compile(source: String) -> Result { - unimplemented!() - } - - pub fn walk(&self, e: &EntryHeader) -> Option { - let init_val : Value = Value::Table(e.toml().clone()); - - self.elements - .clone() - .into_iter() - .fold(Some(init_val), |acc: Option, token: FieldPathElement| { - acc.and_then(|element| token.apply(element)) - }) - } - -} - diff --git a/libimagstore/Cargo.toml b/libimagstore/Cargo.toml index acfa7e80..624e129e 100644 --- a/libimagstore/Cargo.toml +++ b/libimagstore/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" authors = ["Matthias Beyer "] [dependencies] -log = "0.3.5" fs2 = "0.2.2" glob = "0.2.10" +log = "0.3.5" regex = "0.1.47" semver = "0.2" toml = "0.1.25" @@ -14,3 +14,5 @@ version = "2.0.1" [dev-dependencies] tempdir = "0.3.4" +env_logger = "0.3" + diff --git a/libimagstore/src/error.rs b/libimagstore/src/error.rs index 1bbb6961..be1375f0 100644 --- a/libimagstore/src/error.rs +++ b/libimagstore/src/error.rs @@ -24,6 +24,10 @@ pub enum StoreErrorKind { EntryAlreadyBorrowed, EntryAlreadyExists, MalformedEntry, + HeaderPathSyntaxError, + HeaderPathTypeFailure, + HeaderKeyNotFound, + HeaderTypeFailure, // maybe more } @@ -44,6 +48,10 @@ fn store_error_type_as_str(e: &StoreErrorKind) -> &'static str { &StoreErrorKind::EntryAlreadyBorrowed => "Entry is already borrowed", &StoreErrorKind::EntryAlreadyExists => "Entry already exists", &StoreErrorKind::MalformedEntry => "Entry has invalid formatting, missing header", + &StoreErrorKind::HeaderPathSyntaxError => "Syntax error in accessor string", + &StoreErrorKind::HeaderPathTypeFailure => "Header has wrong type for path", + &StoreErrorKind::HeaderKeyNotFound => "Header Key not found", + &StoreErrorKind::HeaderTypeFailure => "Header type is wrong", } } diff --git a/libimagstore/src/lib.rs b/libimagstore/src/lib.rs index b0fb2286..97b7a52c 100644 --- a/libimagstore/src/lib.rs +++ b/libimagstore/src/lib.rs @@ -1,5 +1,5 @@ -#[macro_use] extern crate version; #[macro_use] extern crate log; +#[macro_use] extern crate version; extern crate fs2; extern crate glob; extern crate regex; diff --git a/libimagstore/src/store.rs b/libimagstore/src/store.rs index 30b6d808..39c3ccb3 100644 --- a/libimagstore/src/store.rs +++ b/libimagstore/src/store.rs @@ -7,6 +7,8 @@ use std::sync::Arc; use std::sync::RwLock; use std::collections::BTreeMap; use std::io::{Seek, SeekFrom}; +use std::convert::From; +use std::convert::Into; use toml::{Table, Value}; use regex::Regex; @@ -327,11 +329,17 @@ pub type EntryContent = String; */ #[derive(Debug, Clone)] pub struct EntryHeader { - toml: Table, + header: Value, } pub type EntryResult = RResult; +#[derive(Debug, Clone, PartialEq, Eq)] +enum Token { + Key(String), + Index(usize), +} + /** * Wrapper type around file header (TOML) object */ @@ -339,27 +347,16 @@ impl EntryHeader { pub fn new() -> EntryHeader { EntryHeader { - toml: build_default_header() + header: build_default_header() } } fn from_table(t: Table) -> EntryHeader { EntryHeader { - toml: t + header: Value::Table(t) } } - /** - * Get the table which lives in the background - */ - pub fn toml(&self) -> &Table { - &self.toml - } - - pub fn toml_mut(&mut self) -> &mut Table { - &mut self.toml - } - pub fn parse(s: &str) -> EntryResult { use toml::Parser; @@ -371,12 +368,405 @@ impl EntryHeader { } pub fn verify(&self) -> Result<()> { - verify_header(&self.toml) + match &self.header { + &Value::Table(ref t) => verify_header(&t), + _ => Err(StoreError::new(StoreErrorKind::HeaderTypeFailure, None)), + } + } + + /** + * Insert a header field by a string-spec + * + * ```ignore + * insert("something.in.a.field", Boolean(true)); + * ``` + * + * If an array field was accessed which is _out of bounds_ of the array available, the element + * is appended to the array. + * + * Inserts a Boolean in the section "something" -> "in" -> "a" -> "field" + * A JSON equivalent would be + * + * { + * something: { + * in: { + * a: { + * field: true + * } + * } + * } + * } + * + * Returns true if header field was set, false if there is already a value + */ + pub fn insert(&mut self, spec: &str, v: Value) -> Result { + self.insert_with_sep(spec, '.', v) + } + + pub fn insert_with_sep(&mut self, spec: &str, sep: char, v: Value) -> Result { + let tokens = EntryHeader::tokenize(spec, sep); + if tokens.is_err() { // return parser error if any + return tokens.map(|_| false); + } + let tokens = tokens.unwrap(); + + let destination = tokens.iter().last(); + if destination.is_none() { + return Err(StoreError::new(StoreErrorKind::HeaderPathSyntaxError, None)); + } + let destination = destination.unwrap(); + + let path_to_dest = tokens[..(tokens.len() - 1)].into(); // N - 1 tokens + let mut value = EntryHeader::walk_header(&mut self.header, path_to_dest); // walk N-1 tokens + if value.is_err() { + return value.map(|_| false); + } + let mut value = value.unwrap(); + + // There is already an value at this place + if EntryHeader::extract(value, destination).is_ok() { + return Ok(false); + } + + match destination { + &Token::Key(ref s) => { // if the destination shall be an map key + match value { + /* + * Put it in there if we have a map + */ + &mut Value::Table(ref mut t) => { + t.insert(s.clone(), v); + } + + /* + * Fail if there is no map here + */ + _ => return Err(StoreError::new(StoreErrorKind::HeaderPathTypeFailure, None)), + } + }, + + &Token::Index(i) => { // if the destination shall be an array + match value { + + /* + * Put it in there if we have an array + */ + &mut Value::Array(ref mut a) => { + a.push(v); // push to the end of the array + + // if the index is inside the array, we swap-remove the element at this + // index + if a.len() < i { + a.swap_remove(i); + } + }, + + /* + * Fail if there is no array here + */ + _ => return Err(StoreError::new(StoreErrorKind::HeaderPathTypeFailure, None)), + } + }, + } + + Ok(true) + } + + /** + * Set a header field by a string-spec + * + * ```ignore + * set("something.in.a.field", Boolean(true)); + * ``` + * + * Sets a Boolean in the section "something" -> "in" -> "a" -> "field" + * A JSON equivalent would be + * + * { + * something: { + * in: { + * a: { + * field: true + * } + * } + * } + * } + * + * If there is already a value at this place, this value will be overridden and the old value + * will be returned + */ + pub fn set(&mut self, spec: &str, v: Value) -> Result> { + self.set_with_sep(spec, '.', v) + } + + pub fn set_with_sep(&mut self, spec: &str, sep: char, v: Value) -> Result> { + let tokens = EntryHeader::tokenize(spec, sep); + if tokens.is_err() { // return parser error if any + return Err(tokens.err().unwrap()); + } + let tokens = tokens.unwrap(); + debug!("tokens = {:?}", tokens); + + let destination = tokens.iter().last(); + if destination.is_none() { + return Err(StoreError::new(StoreErrorKind::HeaderPathSyntaxError, None)); + } + let destination = destination.unwrap(); + debug!("destination = {:?}", destination); + + let path_to_dest = tokens[..(tokens.len() - 1)].into(); // N - 1 tokens + let mut value = EntryHeader::walk_header(&mut self.header, path_to_dest); // walk N-1 tokens + if value.is_err() { + return Err(value.err().unwrap()); + } + let mut value = value.unwrap(); + debug!("walked value = {:?}", value); + + match destination { + &Token::Key(ref s) => { // if the destination shall be an map key->value + match value { + /* + * Put it in there if we have a map + */ + &mut Value::Table(ref mut t) => { + debug!("Matched Key->Table"); + return Ok(t.insert(s.clone(), v)); + } + + /* + * Fail if there is no map here + */ + _ => { + debug!("Matched Key->NON-Table"); + return Err(StoreError::new(StoreErrorKind::HeaderPathTypeFailure, None)); + } + } + }, + + &Token::Index(i) => { // if the destination shall be an array + match value { + + /* + * Put it in there if we have an array + */ + &mut Value::Array(ref mut a) => { + debug!("Matched Index->Array"); + a.push(v); // push to the end of the array + + // if the index is inside the array, we swap-remove the element at this + // index + if a.len() > i { + debug!("Swap-Removing in Array {:?}[{:?}] <- {:?}", a, i, a[a.len()-1]); + return Ok(Some(a.swap_remove(i))); + } + + debug!("Appended"); + return Ok(None); + }, + + /* + * Fail if there is no array here + */ + _ => { + debug!("Matched Index->NON-Array"); + return Err(StoreError::new(StoreErrorKind::HeaderPathTypeFailure, None)); + }, + } + }, + } + + Ok(None) + } + + /** + * Read a header field by a string-spec + * + * ```ignore + * let value = read("something.in.a.field"); + * ``` + * + * Reads a Value in the section "something" -> "in" -> "a" -> "field" + * A JSON equivalent would be + * + * { + * something: { + * in: { + * a: { + * field: true + * } + * } + * } + * } + * + * If there is no a value at this place, None will be returned. This also holds true for Arrays + * which are accessed at an index which is not yet there, even if the accessed index is much + * larger than the array length. + */ + pub fn read(&self, spec: &str) -> Result> { + self.read_with_sep(spec, '.') + } + + pub fn read_with_sep(&self, spec: &str, splitchr: char) -> Result> { + let tokens = EntryHeader::tokenize(spec, splitchr); + if tokens.is_err() { // return parser error if any + return Err(tokens.err().unwrap()); + } + let tokens = tokens.unwrap(); + + let mut header_clone = self.header.clone(); // we clone as READing is simpler this way + let mut value = EntryHeader::walk_header(&mut header_clone, tokens); // walk N-1 tokens + if value.is_err() { + let e = value.err().unwrap(); + match e.err_type() { + // We cannot find the header key, as there is no path to it + StoreErrorKind::HeaderKeyNotFound => return Ok(None), + _ => return Err(e), + } + return Err(e); + } + Ok(Some(value.unwrap().clone())) + } + + pub fn delete(&mut self, spec: &str) -> Result> { + let tokens = EntryHeader::tokenize(spec, '.'); + if tokens.is_err() { // return parser error if any + return Err(tokens.err().unwrap()); + } + let tokens = tokens.unwrap(); + + let destination = tokens.iter().last(); + if destination.is_none() { + return Err(StoreError::new(StoreErrorKind::HeaderPathSyntaxError, None)); + } + let destination = destination.unwrap(); + debug!("destination = {:?}", destination); + + let path_to_dest = tokens[..(tokens.len() - 1)].into(); // N - 1 tokens + let mut value = EntryHeader::walk_header(&mut self.header, path_to_dest); // walk N-1 tokens + if value.is_err() { + return Err(value.err().unwrap()); + } + let mut value = value.unwrap(); + debug!("walked value = {:?}", value); + + match destination { + &Token::Key(ref s) => { // if the destination shall be an map key->value + match value { + &mut Value::Table(ref mut t) => { + debug!("Matched Key->Table, removing {:?}", s); + return Ok(t.remove(s)); + }, + _ => { + debug!("Matched Key->NON-Table"); + return Err(StoreError::new(StoreErrorKind::HeaderPathTypeFailure, None)); + } + } + }, + + &Token::Index(i) => { // if the destination shall be an array + match value { + &mut Value::Array(ref mut a) => { + // if the index is inside the array, we swap-remove the element at this + // index + if a.len() > i { + debug!("Removing in Array {:?}[{:?}]", a, i); + return Ok(Some(a.remove(i))); + } else { + return Ok(None); + } + }, + _ => { + debug!("Matched Index->NON-Array"); + return Err(StoreError::new(StoreErrorKind::HeaderPathTypeFailure, None)); + }, + } + }, + } + + Ok(None) + } + + fn tokenize(spec: &str, splitchr: char) -> Result> { + use std::str::FromStr; + + spec.split(splitchr) + .map(|s| { + usize::from_str(s) + .map(Token::Index) + .or_else(|_| Ok(Token::Key(String::from(s)))) + }) + .collect() + } + + fn walk_header(v: &mut Value, tokens: Vec) -> Result<&mut Value> { + use std::vec::IntoIter; + + fn walk_iter<'a>(v: Result<&'a mut Value>, i: &mut IntoIter) -> Result<&'a mut Value> { + let next = i.next(); + v.and_then(move |value| { + if let Some(token) = next { + walk_iter(EntryHeader::extract(value, &token), i) + } else { + Ok(value) + } + }) + } + + walk_iter(Ok(v), &mut tokens.into_iter()) + } + + fn extract_from_table<'a>(v: &'a mut Value, s: &String) -> Result<&'a mut Value> { + match v { + &mut Value::Table(ref mut t) => { + t.get_mut(&s[..]) + .ok_or(StoreError::new(StoreErrorKind::HeaderKeyNotFound, None)) + }, + _ => Err(StoreError::new(StoreErrorKind::HeaderPathTypeFailure, None)), + } + } + + fn extract_from_array(v: &mut Value, i: usize) -> Result<&mut Value> { + match v { + &mut Value::Array(ref mut a) => { + if a.len() < i { + Err(StoreError::new(StoreErrorKind::HeaderKeyNotFound, None)) + } else { + Ok(&mut a[i]) + } + }, + _ => Err(StoreError::new(StoreErrorKind::HeaderPathTypeFailure, None)), + } + } + + fn extract<'a>(v: &'a mut Value, token: &Token) -> Result<&'a mut Value> { + match token { + &Token::Key(ref s) => EntryHeader::extract_from_table(v, s), + &Token::Index(i) => EntryHeader::extract_from_array(v, i), + } } } -fn build_default_header() -> BTreeMap { +impl Into for EntryHeader { + + fn into(self) -> Table { + match self.header { + Value::Table(t) => t, + _ => panic!("EntryHeader is not a table!"), + } + } + +} + +impl From
for EntryHeader { + + fn from(t: Table) -> EntryHeader { + EntryHeader { header: Value::Table(t) } + } + +} + +fn build_default_header() -> Value { // BTreeMap let mut m = BTreeMap::new(); m.insert(String::from("imag"), { @@ -388,7 +778,7 @@ fn build_default_header() -> BTreeMap { Value::Table(imag_map) }); - m + Value::Table(m) } fn verify_header(t: &Table) -> Result<()> { if !has_main_section(t) { @@ -512,7 +902,7 @@ impl Entry { pub fn to_str(&self) -> String { format!("---{header}---\n{content}", - header = ::toml::encode_str(&self.header.toml), + header = ::toml::encode_str(&self.header.header), content = self.content) } @@ -545,7 +935,11 @@ impl Entry { #[cfg(test)] mod test { + extern crate env_logger; + use std::collections::BTreeMap; + use super::EntryHeader; + use super::Token; use toml::Value; @@ -696,7 +1090,455 @@ Hai"; assert_eq!(TEST_ENTRY, string); } + #[test] + fn test_walk_header_simple() { + let tokens = EntryHeader::tokenize("a", '.').unwrap(); + assert!(tokens.len() == 1, "1 token was expected, {} were parsed", tokens.len()); + assert!(tokens.iter().next().unwrap() == &Token::Key(String::from("a")), + "'a' token was expected, {:?} was parsed", tokens.iter().next()); + + let mut header = BTreeMap::new(); + header.insert(String::from("a"), Value::Integer(1)); + + let mut v_header = Value::Table(header); + let res = EntryHeader::walk_header(&mut v_header, tokens); + assert_eq!(&mut Value::Integer(1), res.unwrap()); + } + + #[test] + fn test_walk_header_with_array() { + let tokens = EntryHeader::tokenize("a.0", '.').unwrap(); + assert!(tokens.len() == 2, "2 token was expected, {} were parsed", tokens.len()); + assert!(tokens.iter().next().unwrap() == &Token::Key(String::from("a")), + "'a' token was expected, {:?} was parsed", tokens.iter().next()); + + let mut header = BTreeMap::new(); + let ary = Value::Array(vec![Value::Integer(1)]); + header.insert(String::from("a"), ary); + + + let mut v_header = Value::Table(header); + let res = EntryHeader::walk_header(&mut v_header, tokens); + assert_eq!(&mut Value::Integer(1), res.unwrap()); + } + + #[test] + fn test_walk_header_extract_array() { + let tokens = EntryHeader::tokenize("a", '.').unwrap(); + assert!(tokens.len() == 1, "1 token was expected, {} were parsed", tokens.len()); + assert!(tokens.iter().next().unwrap() == &Token::Key(String::from("a")), + "'a' token was expected, {:?} was parsed", tokens.iter().next()); + + let mut header = BTreeMap::new(); + let ary = Value::Array(vec![Value::Integer(1)]); + header.insert(String::from("a"), ary); + + let mut v_header = Value::Table(header); + let res = EntryHeader::walk_header(&mut v_header, tokens); + assert_eq!(&mut Value::Array(vec![Value::Integer(1)]), res.unwrap()); + } + + /** + * Creates a big testing header. + * + * JSON equivalent: + * + * ```json + * { + * "a": { + * "array": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] + * }, + * "b": { + * "array": [ "string1", "string2", "string3", "string4" ] + * }, + * "c": { + * "array": [ 1, "string2", 3, "string4" ] + * }, + * "d": { + * "array": [ + * { + * "d1": 1 + * }, + * { + * "d2": 2 + * }, + * { + * "d3": 3 + * }, + * ], + * + * "something": "else", + * + * "and": { + * "something": { + * "totally": "different" + * } + * } + * } + * } + * ``` + * + * The sections "a", "b", "c", "d" are created in the respective helper functions + * create_header_section_a, create_header_section_b, create_header_section_c and + * create_header_section_d. + * + * These functions can also be used for testing. + * + */ + fn create_header() -> Value { + let a = create_header_section_a(); + let b = create_header_section_b(); + let c = create_header_section_c(); + let d = create_header_section_d(); + + let mut header = BTreeMap::new(); + header.insert(String::from("a"), a); + header.insert(String::from("b"), b); + header.insert(String::from("c"), c); + header.insert(String::from("d"), d); + + Value::Table(header) + } + + fn create_header_section_a() -> Value { + // 0..10 is exclusive 10 + let a_ary = Value::Array((0..10).map(|x| Value::Integer(x)).collect()); + + let mut a_obj = BTreeMap::new(); + a_obj.insert(String::from("array"), a_ary); + Value::Table(a_obj) + } + + fn create_header_section_b() -> Value { + let b_ary = Value::Array((0..9) + .map(|x| Value::String(format!("string{}", x))) + .collect()); + + let mut b_obj = BTreeMap::new(); + b_obj.insert(String::from("array"), b_ary); + Value::Table(b_obj) + } + + fn create_header_section_c() -> Value { + let c_ary = Value::Array( + vec![ + Value::Integer(1), + Value::String(String::from("string2")), + Value::Integer(3), + Value::String(String::from("string4")) + ]); + + let mut c_obj = BTreeMap::new(); + c_obj.insert(String::from("array"), c_ary); + Value::Table(c_obj) + } + + fn create_header_section_d() -> Value { + let d_ary = Value::Array( + vec![ + { + let mut tab = BTreeMap::new(); + tab.insert(String::from("d1"), Value::Integer(1)); + tab + }, + { + let mut tab = BTreeMap::new(); + tab.insert(String::from("d2"), Value::Integer(2)); + tab + }, + { + let mut tab = BTreeMap::new(); + tab.insert(String::from("d3"), Value::Integer(3)); + tab + }, + ].into_iter().map(Value::Table).collect()); + + let and_obj = Value::Table({ + let mut tab = BTreeMap::new(); + let something_tab = Value::Table({ + let mut tab = BTreeMap::new(); + tab.insert(String::from("totally"), Value::String(String::from("different"))); + tab + }); + tab.insert(String::from("something"), something_tab); + tab + }); + + let mut d_obj = BTreeMap::new(); + d_obj.insert(String::from("array"), d_ary); + d_obj.insert(String::from("something"), Value::String(String::from("else"))); + d_obj.insert(String::from("and"), and_obj); + Value::Table(d_obj) + } + + #[test] + fn test_walk_header_big_a() { + test_walk_header_extract_section("a", &create_header_section_a()); + } + + #[test] + fn test_walk_header_big_b() { + test_walk_header_extract_section("b", &create_header_section_b()); + } + + #[test] + fn test_walk_header_big_c() { + test_walk_header_extract_section("c", &create_header_section_c()); + } + + #[test] + fn test_walk_header_big_d() { + test_walk_header_extract_section("d", &create_header_section_d()); + } + + fn test_walk_header_extract_section(secname: &str, expected: &Value) { + let tokens = EntryHeader::tokenize(secname, '.').unwrap(); + assert!(tokens.len() == 1, "1 token was expected, {} were parsed", tokens.len()); + assert!(tokens.iter().next().unwrap() == &Token::Key(String::from(secname)), + "'{}' token was expected, {:?} was parsed", secname, tokens.iter().next()); + + let mut header = create_header(); + let res = EntryHeader::walk_header(&mut header, tokens); + assert_eq!(expected, res.unwrap()); + } + + #[test] + fn test_walk_header_extract_numbers() { + test_extract_number("a", 0, 0); + test_extract_number("a", 1, 1); + test_extract_number("a", 2, 2); + test_extract_number("a", 3, 3); + test_extract_number("a", 4, 4); + test_extract_number("a", 5, 5); + test_extract_number("a", 6, 6); + test_extract_number("a", 7, 7); + test_extract_number("a", 8, 8); + test_extract_number("a", 9, 9); + + test_extract_number("c", 0, 1); + test_extract_number("c", 2, 3); + } + + fn test_extract_number(sec: &str, idx: usize, exp: i64) { + let tokens = EntryHeader::tokenize(&format!("{}.array.{}", sec, idx)[..], '.').unwrap(); + assert!(tokens.len() == 3, "3 token was expected, {} were parsed", tokens.len()); + { + let mut iter = tokens.iter(); + + let tok = iter.next().unwrap(); + let exp = Token::Key(String::from(sec)); + assert!(tok == &exp, "'{}' token was expected, {:?} was parsed", sec, tok); + + let tok = iter.next().unwrap(); + let exp = Token::Key(String::from("array")); + assert!(tok == &exp, "'array' token was expected, {:?} was parsed", tok); + + let tok = iter.next().unwrap(); + let exp = Token::Index(idx); + assert!(tok == &exp, "'{}' token was expected, {:?} was parsed", idx, tok); + } + + let mut header = create_header(); + let res = EntryHeader::walk_header(&mut header, tokens); + assert_eq!(&mut Value::Integer(exp), res.unwrap()); + } + + #[test] + fn test_header_read() { + let v = create_header(); + let h = match v { + Value::Table(t) => EntryHeader::from_table(t), + _ => panic!("create_header() doesn't return a table!"), + }; + + assert!(if let Ok(Some(Value::Table(_))) = h.read("a") { true } else { false }); + assert!(if let Ok(Some(Value::Array(_))) = h.read("a.array") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("a.array.1") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("a.array.9") { true } else { false }); + + assert!(if let Ok(Some(Value::Table(_))) = h.read("c") { true } else { false }); + assert!(if let Ok(Some(Value::Array(_))) = h.read("c.array") { true } else { false }); + assert!(if let Ok(Some(Value::String(_))) = h.read("c.array.1") { true } else { false }); + assert!(if let Ok(None) = h.read("c.array.9") { true } else { false }); + + assert!(if let Ok(Some(Value::Integer(_))) = h.read("d.array.0.d1") { true } else { false }); + assert!(if let Ok(None) = h.read("d.array.0.d2") { true } else { false }); + assert!(if let Ok(None) = h.read("d.array.0.d3") { true } else { false }); + + assert!(if let Ok(None) = h.read("d.array.1.d1") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("d.array.1.d2") { true } else { false }); + assert!(if let Ok(None) = h.read("d.array.1.d3") { true } else { false }); + + assert!(if let Ok(None) = h.read("d.array.2.d1") { true } else { false }); + assert!(if let Ok(None) = h.read("d.array.2.d2") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("d.array.2.d3") { true } else { false }); + + assert!(if let Ok(Some(Value::String(_))) = h.read("d.something") { true } else { false }); + assert!(if let Ok(Some(Value::Table(_))) = h.read("d.and") { true } else { false }); + assert!(if let Ok(Some(Value::Table(_))) = h.read("d.and.something") { true } else { false }); + assert!(if let Ok(Some(Value::String(_))) = h.read("d.and.something.totally") { true } else { false }); + } + + #[test] + fn test_header_set_override() { + let _ = env_logger::init(); + let v = create_header(); + let mut h = match v { + Value::Table(t) => EntryHeader::from_table(t), + _ => panic!("create_header() doesn't return a table!"), + }; + + println!("Testing index 0"); + assert_eq!(h.read("a.array.0").unwrap().unwrap(), Value::Integer(0)); + + println!("Altering index 0"); + assert_eq!(h.set("a.array.0", Value::Integer(42)).unwrap().unwrap(), Value::Integer(0)); + + println!("Values now: {:?}", h); + + println!("Testing all indexes"); + assert_eq!(h.read("a.array.0").unwrap().unwrap(), Value::Integer(42)); + assert_eq!(h.read("a.array.1").unwrap().unwrap(), Value::Integer(1)); + assert_eq!(h.read("a.array.2").unwrap().unwrap(), Value::Integer(2)); + assert_eq!(h.read("a.array.3").unwrap().unwrap(), Value::Integer(3)); + assert_eq!(h.read("a.array.4").unwrap().unwrap(), Value::Integer(4)); + assert_eq!(h.read("a.array.5").unwrap().unwrap(), Value::Integer(5)); + assert_eq!(h.read("a.array.6").unwrap().unwrap(), Value::Integer(6)); + assert_eq!(h.read("a.array.7").unwrap().unwrap(), Value::Integer(7)); + assert_eq!(h.read("a.array.8").unwrap().unwrap(), Value::Integer(8)); + assert_eq!(h.read("a.array.9").unwrap().unwrap(), Value::Integer(9)); + } + + #[test] + fn test_header_set_new() { + let _ = env_logger::init(); + let v = create_header(); + let mut h = match v { + Value::Table(t) => EntryHeader::from_table(t), + _ => panic!("create_header() doesn't return a table!"), + }; + + assert!(h.read("a.foo").is_ok()); + assert!(h.read("a.foo").unwrap().is_none()); + + { + let v = h.set("a.foo", Value::Integer(42)); + assert!(v.is_ok()); + assert!(v.unwrap().is_none()); + + assert!(if let Ok(Some(Value::Table(_))) = h.read("a") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("a.foo") { true } else { false }); + } + + { + let v = h.set("new", Value::Table(BTreeMap::new())); + assert!(v.is_ok()); + assert!(v.unwrap().is_none()); + + let v = h.set("new.subset", Value::Table(BTreeMap::new())); + assert!(v.is_ok()); + assert!(v.unwrap().is_none()); + + let v = h.set("new.subset.dest", Value::Integer(1337)); + assert!(v.is_ok()); + assert!(v.unwrap().is_none()); + + assert!(if let Ok(Some(Value::Table(_))) = h.read("new") { true } else { false }); + assert!(if let Ok(Some(Value::Table(_))) = h.read("new.subset") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("new.subset.dest") { true } else { false }); + } + } + + + #[test] + fn test_header_insert_override() { + let _ = env_logger::init(); + let v = create_header(); + let mut h = match v { + Value::Table(t) => EntryHeader::from_table(t), + _ => panic!("create_header() doesn't return a table!"), + }; + + println!("Testing index 0"); + assert_eq!(h.read("a.array.0").unwrap().unwrap(), Value::Integer(0)); + + println!("Altering index 0"); + assert_eq!(h.insert("a.array.0", Value::Integer(42)).unwrap(), false); + println!("...should have failed"); + + println!("Testing all indexes"); + assert_eq!(h.read("a.array.0").unwrap().unwrap(), Value::Integer(0)); + assert_eq!(h.read("a.array.1").unwrap().unwrap(), Value::Integer(1)); + assert_eq!(h.read("a.array.2").unwrap().unwrap(), Value::Integer(2)); + assert_eq!(h.read("a.array.3").unwrap().unwrap(), Value::Integer(3)); + assert_eq!(h.read("a.array.4").unwrap().unwrap(), Value::Integer(4)); + assert_eq!(h.read("a.array.5").unwrap().unwrap(), Value::Integer(5)); + assert_eq!(h.read("a.array.6").unwrap().unwrap(), Value::Integer(6)); + assert_eq!(h.read("a.array.7").unwrap().unwrap(), Value::Integer(7)); + assert_eq!(h.read("a.array.8").unwrap().unwrap(), Value::Integer(8)); + assert_eq!(h.read("a.array.9").unwrap().unwrap(), Value::Integer(9)); + } + + #[test] + fn test_header_insert_new() { + let _ = env_logger::init(); + let v = create_header(); + let mut h = match v { + Value::Table(t) => EntryHeader::from_table(t), + _ => panic!("create_header() doesn't return a table!"), + }; + + assert!(h.read("a.foo").is_ok()); + assert!(h.read("a.foo").unwrap().is_none()); + + { + let v = h.insert("a.foo", Value::Integer(42)); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), true); + + assert!(if let Ok(Some(Value::Table(_))) = h.read("a") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("a.foo") { true } else { false }); + } + + { + let v = h.insert("new", Value::Table(BTreeMap::new())); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), true); + + let v = h.insert("new.subset", Value::Table(BTreeMap::new())); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), true); + + let v = h.insert("new.subset.dest", Value::Integer(1337)); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), true); + + assert!(if let Ok(Some(Value::Table(_))) = h.read("new") { true } else { false }); + assert!(if let Ok(Some(Value::Table(_))) = h.read("new.subset") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("new.subset.dest") { true } else { false }); + } + } + + #[test] + fn test_header_delete() { + let _ = env_logger::init(); + let v = create_header(); + let mut h = match v { + Value::Table(t) => EntryHeader::from_table(t), + _ => panic!("create_header() doesn't return a table!"), + }; + + assert!(if let Ok(Some(Value::Table(_))) = h.read("a") { true } else { false }); + assert!(if let Ok(Some(Value::Array(_))) = h.read("a.array") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("a.array.1") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(_))) = h.read("a.array.9") { true } else { false }); + + assert!(if let Ok(Some(Value::Integer(1))) = h.delete("a.array.1") { true } else { false }); + assert!(if let Ok(Some(Value::Integer(9))) = h.delete("a.array.8") { true } else { false }); + assert!(if let Ok(Some(Value::Array(_))) = h.delete("a.array") { true } else { false }); + assert!(if let Ok(Some(Value::Table(_))) = h.delete("a") { true } else { false }); + + } + } - -