From 6ed76e485ef53c886f510a992263ec0b28cc5e00 Mon Sep 17 00:00:00 2001 From: Lionel Sambuc Date: Wed, 19 Jun 2019 14:30:08 +0200 Subject: [PATCH] Parser implementation * Adaptation of the grammar to make more regular w.r.t reference space names placement. * Implemented Type check * Implemented prediction of the result set cardinality. This assumes: + A space is not infinite, this is required to compute complementary sets as we can compute the whole space volume. + Density is mostly uniform, this allows us to assume the number of results is related with a constant factor to the volume in space of the selection. + The prediction is approximated by using the most pessimistic resulting cardinality for each operator, in order to keep the operation simple, and fast, at the expense of the precision. * Implemented execution, which calls into a DB abstraction layer. That layer is currently mostly empty. Execution is also implemented in a naive way, and should most likely be optimised. --- Cargo.toml | 1 + Grammars/filters.g4 | 17 +- Grammars/queries.g4 | 4 +- src/ast.rs | 113 --- src/database/mod.rs | 57 ++ src/database/space.rs | 86 ++ src/executors.rs | 314 +++++++ src/expression.rs | 15 + src/lib.rs | 19 +- src/main.rs | 46 +- src/predictors.rs | 48 ++ src/queries.lalrpop | 286 ++++--- src/symbols.rs | 319 +++++++ src/tests.rs | 1835 +++++++++++++++++++++-------------------- src/types.rs | 59 ++ src/validators.rs | 171 ++++ 16 files changed, 2237 insertions(+), 1153 deletions(-) delete mode 100644 src/ast.rs create mode 100644 src/database/mod.rs create mode 100644 src/database/space.rs create mode 100644 src/executors.rs create mode 100644 src/expression.rs create mode 100644 src/predictors.rs create mode 100644 src/symbols.rs create mode 100644 src/types.rs create mode 100644 src/validators.rs diff --git a/Cargo.toml b/Cargo.toml index aeae3bc..6301b11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" lalrpop-util = "0.17.0" regex = "0.2.1" measure_time = "0.6" # To mesure parsing time, only required by binary +lazy_static = "1.3.0" # for space.rs, might be removed once the full data model is provided by the use of the crate # Logging macros API log = { version = "0.4", features = ["max_level_trace", "release_max_level_debug"] } diff --git a/Grammars/filters.g4 b/Grammars/filters.g4 index 1e85c64..bc18eee 100644 --- a/Grammars/filters.g4 +++ b/Grammars/filters.g4 @@ -134,33 +134,38 @@ hyperrectangle : 'hyperrectangle' '{' position ',' position ( ',' position ',' position )* + ( ',' STRING )? '}' ; /* A hypersphere is defined by its center and a radius, independantly * of the number of dimensions of the space. */ hypersphere - : 'hypersphere' '{' position ( ',' positive_number ) '}' + : 'hypersphere' '{' + position + ',' positive_number + ( ',' STRING )? + '}' ; point - : 'point' '{' position '}' + : 'point' '{' position ( ',' STRING )? '}' ; /* Define a shape as the non-zero values in a NIfTI object, defined by * nifti{ - * spaceId: string, * lower_corner: position, // Optional, default to the origin * rotation: [ position+ ], // Optional, no rotation by default - * bytes: uri(STRING) // uri to the NIfTI object + * bytes: uri(STRING), // uri to the NIfTI object + * spaceId: string * } */ nifti : 'nifti' '{' - STRING ',' (position ',' )? ( '[' position ( ',' position )* ']' ',' )? - byte_provider + byte_provider ',' + STRING '}' ; diff --git a/Grammars/queries.g4 b/Grammars/queries.g4 index 1b7509e..792a262 100644 --- a/Grammars/queries.g4 +++ b/Grammars/queries.g4 @@ -18,11 +18,11 @@ projection_operators * * If it is provided, it MUST resolve to a NUMBER. */ nifti_operator - : 'nifti' '(' ( selector ',' )? bag_expression ')' + : 'nifti' '(' ( STRING ',' )? ( selector ',' )? bag_expression ')' ; json_operator - : 'json' '(' jslt ',' bag_expression ')' + : 'json' '(' jslt ',' bag_expression ( ',' STRING )? ')' ; jslt diff --git a/src/ast.rs b/src/ast.rs deleted file mode 100644 index 61c13cd..0000000 --- a/src/ast.rs +++ /dev/null @@ -1,113 +0,0 @@ -trait Expression { - fn check_type(); - fn predict(); - fn execute(); -} - -/**********************************************************************/ -/* FORMATTING DATA */ -/**********************************************************************/ -#[derive(Clone, Debug)] -pub enum Projection { - Nifti(LiteralSelector, Bag), - JSON(JsonValue, Bag), -} - -// JSON FORMAT -#[derive(Clone, Debug)] -pub enum JsonValue { - String(String), - JsonNumber(LiteralNumber), - Bool(bool), - Null, - Object(Vec<(String, JsonValue)>), - Array(Vec), - Selector(LiteralSelector), - Aggregation(Aggregation), -} - -#[derive(Clone, Debug)] -pub enum Aggregation { - Count(bool, LiteralSelector), - Sum(LiteralSelector), - Min(LiteralSelector), - Max(LiteralSelector), -} - -// NIFTI -#[derive(Clone, Debug)] -struct Transform { - reference: String, - offset: Vec, - rotation: Vec>, -} - -/**********************************************************************/ -/* SELECTING / FILTERING DATA */ -/**********************************************************************/ -#[derive(Clone, Debug)] -pub enum Bag { - Distinct(Box), - Filter(Option, Option>), - Complement(Box), - Intersection(Box, Box), - Union(Box, Box), - Bag(Vec), - Inside(Shape), - Outside(Shape), -} - -/**********************************************************************/ -/* BAG OPERATORS */ -/**********************************************************************/ -#[derive(Clone, Debug)] -pub enum Predicate { - Less(Position, LiteralPosition), - Greater(Position, LiteralPosition), - Equal(Position, LiteralPosition), - Not(Box), - And(Box, Box), - Or(Box, Box), -} - -/**********************************************************************/ -/* SPATIAL OPERATORS */ -/**********************************************************************/ - -/**********************************************************************/ -/* SHAPES */ -/**********************************************************************/ -#[derive(Clone, Debug)] -pub enum Shape { - Point(LiteralPosition), - HyperRectangle(Vec), - HyperSphere(LiteralPosition, LiteralNumber), - Nifti(), -} - -/**********************************************************************/ -/* POSITIONS */ -/**********************************************************************/ -#[derive(Clone, Debug)] -pub enum Position { - StrCmpICase(LiteralSelector, String), - StrCmp(LiteralSelector, String), - Selector(LiteralSelector), - LiteralPosition(LiteralPosition), -} - -/**********************************************************************/ -/* Literals / TOKENS */ -/**********************************************************************/ - -#[derive(Clone, Debug)] -pub struct Field(pub String, pub Option); - -#[derive(Clone, Debug)] -pub enum LiteralNumber { - Int(i64), - Float(f64), -} - -pub type LiteralPosition = Vec; -pub type LiteralSelector = Vec; diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..9bd9a97 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,57 @@ +use crate::executors::ResultSet; +use crate::symbols::*; + +//use ironsea_index::*; + +pub fn get_all(_space_id: &String) -> ResultSet { + //space::get_all(space_id) + Err("not yet implemented".to_string()) +} + +pub fn get_by_bounding_box(_space_id: &String, _bounding_box: &Vec) -> ResultSet { + Err("not yet implemented".to_string()) +} + +pub fn get_by_position(_space_id: &String, _position: &LiteralPosition) -> ResultSet { + Err("not yet implemented".to_string()) +} + +pub fn get(_point: &Object, _fields: &Vec) -> Result { + Err("not yet implemented".to_string()) +} + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +pub struct Object { + id: u32, +} + +impl Object { + pub fn length(&self) -> f64 { + 0.0 + } + + pub fn eval(&self, _predicate: &Predicate) -> bool { + false + } +} + +impl Position { + pub fn eval(&self, point: &Object) -> LiteralPosition { + match self { + Position::StrCmpICase(_selector, _string) => LiteralPosition(vec![]), //TODO + Position::StrCmp(_selector, _string) => LiteralPosition(vec![]), //TODO + Position::Selector(selector) => selector.eval(point), + Position::Literal(position) => position.clone(), + } + } +} + +impl LiteralSelector { + pub fn eval(&self, point: &Object) -> LiteralPosition { + let LiteralSelector(fields) = self; + match get(point, fields) { + Err(_) => LiteralPosition(vec![]), + Ok(p @ LiteralPosition(_)) => p, + } + } +} diff --git a/src/database/space.rs b/src/database/space.rs new file mode 100644 index 0000000..4ce8080 --- /dev/null +++ b/src/database/space.rs @@ -0,0 +1,86 @@ +use std::collections::HashMap; + +use crate::types::LiteralTypes; + +//FIXME: Improve, as this should not be static, but coming from DB. +lazy_static! { + static ref UNIVERSE: String = "Universe".to_string(); + static ref SPACES: HashMap<&'static str, Space> = { + let mut m = HashMap::new(); + m.insert( + "Universe", + Space { + space_type: LiteralTypes::Vector(vec![ + LiteralTypes::Float, + LiteralTypes::Float, + LiteralTypes::Float, + ]), + bounding_box: vec![ + vec![0, 0, 0], + vec![ + std::u32::MAX as i64, + std::u32::MAX as i64, + std::u32::MAX as i64, + ], + ], + }, + ); + + m + }; +} + +struct Space { + space_type: LiteralTypes, + bounding_box: Vec>, +} + +impl Space { + pub fn max_volume(&self) -> f64 { + let mut volume = 1.0; + for max in &self.bounding_box[1] { + volume *= *max as f64; + } + + volume + } +} + +pub fn name() -> &'static String { + lazy_static! { + static ref UNIVERSE: String = "Universe".to_string(); + }; + + &UNIVERSE +} + +#[inline] +pub fn get_type(space_id: &String) -> &LiteralTypes { + lazy_static! { + static ref EMPTY_TYPE: LiteralTypes = LiteralTypes::Vector(Vec::new()); + }; + + match SPACES.get(space_id.as_str()) { + None => &EMPTY_TYPE, + Some(space) => &space.space_type, + } +} + +#[inline] +pub fn bounding_box(space_id: &String) -> &Vec> { + lazy_static! { + static ref EMPTY_BOX: Vec> = Vec::new(); + }; + match SPACES.get(space_id.as_str()) { + None => &EMPTY_BOX, + Some(space) => &space.bounding_box, + } +} + +#[inline] +pub fn max_volume(space_id: &String) -> f64 { + match SPACES.get(space_id.as_str()) { + None => 0.0, + Some(space) => space.max_volume(), + } +} diff --git a/src/executors.rs b/src/executors.rs new file mode 100644 index 0000000..cba48ac --- /dev/null +++ b/src/executors.rs @@ -0,0 +1,314 @@ +use std::collections::HashSet; + +use super::database; +use super::expression::*; +use super::symbols::*; + +pub type ResultSet = Result, String>; + +impl Executor for Projection { + type ResultSet = self::ResultSet; + + fn execute(&self) -> ResultSet { + match self { + Projection::Nifti(_, _, _bag) => Err("not yet implemented".to_string()), + Projection::JSON(_, _format, bag) => bag.execute(), // FIXME: Add projections here + } + } +} + +impl Executor for Bag { + type ResultSet = self::ResultSet; + + fn execute(&self) -> ResultSet { + fn get_bounding_box( + position: &LiteralPosition, + radius: &LiteralNumber, + ) -> Result, String> { + let LiteralPosition(position) = position; + let mut low = vec![]; + let mut high = vec![]; + match radius { + LiteralNumber::Int(r) => { + for x in position { + match x { + LiteralNumber::Int(x) => { + low.push(LiteralNumber::Int(x - r)); + high.push(LiteralNumber::Int(x + r)); + } + LiteralNumber::Float(x) => { + low.push(LiteralNumber::Float(x - (*r as f64))); + high.push(LiteralNumber::Float(x + (*r as f64))); + } + }; + } + } + LiteralNumber::Float(r) => { + for x in position { + match x { + LiteralNumber::Int(_) => { + return Err(format!("The radius provided is a floating point value, which is incompatible with integer coordinates components: radius {:?}, coordinates {:?}", radius, position)); + } + LiteralNumber::Float(x) => { + low.push(LiteralNumber::Float(x - r)); + high.push(LiteralNumber::Float(x + r)); + } + }; + } + } + } + + Ok(vec![LiteralPosition(low), LiteralPosition(high)]) + }; + + match self { + Bag::Distinct(bag) => match bag.execute() { + e @ Err(_) => e, + Ok(mut v) => { + let set: HashSet<_> = v.drain(..).collect(); // dedup + v.extend(set.into_iter()); + + Ok(v) + } + }, + Bag::Filter(predicate, bag) => match predicate { + None => bag.execute(), + Some(predicate) => match bag.execute() { + e @ Err(_) => e, + Ok(source) => { + let mut filtered = Vec::new(); + + for point in source { + if point.eval(predicate) { + filtered.push(point); + } + } + + Ok(filtered) + } + }, + }, + Bag::Complement(bag) => match bag.execute() { + // The complement of a set is computed within its definition space. + e @ Err(_) => e, + Ok(inside) => { + let mut outside = Vec::new(); + match database::get_all(bag.space()) { + e @ Err(_) => e, + Ok(points) => { + for point in points { + if !inside.contains(&point) { + outside.push(point) + } + } + + Ok(outside) + } + } + } + }, + Bag::Intersection(lh, rh) => { + let l = lh.execute(); + if let Ok(l) = l { + let r = rh.execute(); + if let Ok(r) = r { + let mut v = vec![]; + + if rh.predict() < lh.predict() { + for o in r { + if l.contains(&o) { + v.push(o); + } + } + } else { + for o in l { + if r.contains(&o) { + v.push(o); + } + } + } + Ok(v) + } else { + r + } + } else { + l + } + } + Bag::Union(lh, rh) => { + let l = lh.execute(); + if let Ok(mut l) = l { + let r = rh.execute(); + if let Ok(mut r) = r { + if rh.predict() < lh.predict() { + l.append(&mut r); + Ok(l) + } else { + r.append(&mut l); + Ok(r) + } + } else { + r + } + } else { + l + } + } + Bag::Bag(bags) => { + let mut v = vec![]; + for bag in bags { + let b = bag.execute(); + match b { + e @ Err(_) => { + return e; + } + Ok(mut b) => { + //TODO: SPACE CONVERSIONS IF NOT THE SAME SPACES? + v.append(&mut b); + } + } + } + + Ok(v) + } + Bag::Inside(shape) => match shape { + Shape::Point(space_id, position) => database::get_by_position(space_id, position), + Shape::HyperRectangle(space_id, bounding_box) => { + database::get_by_bounding_box(space_id, bounding_box) + } + Shape::HyperSphere(space_id, position, radius) => { + let length = match radius { + LiteralNumber::Int(x) => *x as f64, + LiteralNumber::Float(x) => *x, + }; + + match get_bounding_box(position, radius) { + Err(e) => Err(e), + Ok(inside) => match database::get_by_bounding_box(space_id, &inside) { + e @ Err(_) => e, + Ok(source) => { + let mut filtered = vec![]; + + for point in source { + // Include the surface of the sphere + if point.length() <= length { + filtered.push(point); + } + } + Ok(filtered) + } + }, + } + } + Shape::Nifti(_space_id) => Err("not yet implemented".to_string()), + }, + Bag::Outside(shape) => { + fn outside_set(space_id: &String, inside: Vec) -> ResultSet { + let mut outside = Vec::new(); + match database::get_all(space_id) { + e @ Err(_) => e, + Ok(points) => { + for point in points { + if !inside.contains(&point) { + outside.push(point) + } + } + + Ok(outside) + } + } + } + + match shape { + Shape::Point(space_id, position) => { + match database::get_by_position(space_id, position) { + e @ Err(_) => e, + Ok(inside) => outside_set(space_id, inside), + } + } + Shape::HyperRectangle(space_id, bounding_box) => { + // We need to adapt the bounding_box to ensure the + // surface will not hit as part of the inside set, so we + // compute the biggest bounding box contained within the + // given box. + + // Smallest increment possible + let mut low: Vec = vec![]; + let LiteralPosition(coordinates) = &bounding_box[0]; + for coordinate in coordinates { + match coordinate { + LiteralNumber::Int(x) => low.push(LiteralNumber::Int(x + 1)), + LiteralNumber::Float(x) => { + low.push(LiteralNumber::Float(x + std::f64::EPSILON)) + } + }; + } + let low = LiteralPosition(low); + + // Smallest decrement possible + let mut high: Vec = vec![]; + let LiteralPosition(coordinates) = &bounding_box[1]; + for coordinate in coordinates { + match coordinate { + LiteralNumber::Int(x) => high.push(LiteralNumber::Int(x - 1)), + LiteralNumber::Float(x) => { + high.push(LiteralNumber::Float(x - std::f64::EPSILON)) + } + }; + } + let high = LiteralPosition(high); + + match database::get_by_bounding_box(space_id, &vec![low, high]) { + e @ Err(_) => e, + Ok(inside) => outside_set(space_id, inside), + } + } + Shape::HyperSphere(space_id, position, radius) => { + let length = match radius { + LiteralNumber::Int(x) => *x as f64, + LiteralNumber::Float(x) => *x, + }; + + match get_bounding_box(position, radius) { + Err(e) => Err(e), + Ok(inside) => match database::get_by_bounding_box(space_id, &inside) { + Err(e) => Err(e), + Ok(source) => { + let mut filtered = vec![]; + + for point in source { + // Exclude the surface of the sphere, so + // that it is included in the + // complement. + if point.length() < length { + filtered.push(point); + } + } + + outside_set(space_id, filtered) + } + }, + } + } + Shape::Nifti(_space_id) => Err("not yet implemented".to_string()), + } + } + } + } +} + +impl Predicate { + pub fn eval(&self, point: &database::Object) -> bool { + match self { + Predicate::Not(predicate) => !predicate.eval(point), + Predicate::And(lh, rh) => lh.eval(point) && rh.eval(point), + Predicate::Or(lh, rh) => lh.eval(point) || rh.eval(point), + // I don't know how to evaluate these at this point, so let the DB object take care of that. + // Predicate::Less(selector, literal) => &selector.eval(point) < literal, + // Predicate::Greater(selector, literal) => &selector.eval(point) > literal, + // Predicate::Equal(selector, literal) => &selector.eval(point) == literal, + // Redirect to the DB Objet the evaluation of the remaining predicate operators + predicate => point.eval(predicate), + } + } +} diff --git a/src/expression.rs b/src/expression.rs new file mode 100644 index 0000000..d92ed18 --- /dev/null +++ b/src/expression.rs @@ -0,0 +1,15 @@ +pub trait Validator { + type ValidationResult; + + fn validate(&self) -> Self::ValidationResult; +} + +pub trait Predictor { + fn predict(&self) -> f64; +} + +pub trait Executor { + type ResultSet; + + fn execute(&self) -> Self::ResultSet; +} diff --git a/src/lib.rs b/src/lib.rs index 0a2ef0c..867c4ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,25 @@ +#[macro_use] +extern crate lazy_static; + #[macro_use] extern crate lalrpop_util; lalrpop_mod!(pub queries); // synthesized by LALRPOP -pub mod ast; -pub use ast::*; +mod database; +mod executors; +mod expression; +mod predictors; +mod validators; + +mod symbols; +mod types; + +pub use expression::Executor; +pub use expression::Predictor; +pub use expression::Validator; +pub use queries::FiltersParser; +pub use queries::QueryParser; #[cfg(test)] mod tests; diff --git a/src/main.rs b/src/main.rs index 30b27b0..01a9d9a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,8 @@ extern crate measure_time; extern crate parser; -use parser::queries; +use parser::QueryParser; +use parser::{Executor, Predictor, Validator}; use std::io; @@ -15,13 +16,13 @@ fn main() { pretty_env_logger::init(); //let parser = queries::FiltersParser::new(); - let parser = queries::QueryParser::new(); + let parser = QueryParser::new(); loop { + println!(); info!("Expression to parse (type `quit` to exit): "); let mut input = String::new(); - match io::stdin().read_line(&mut input) { Ok(0) => break, // Catch ^D Ok(1) => continue, // Catch \n @@ -34,11 +35,42 @@ fn main() { } let input = input.as_str(); - let mut out; { - debug_time!("Parsing"); - out = parser.parse(input); + debug_time!("Interpretation"); + let mut parse; + { + trace_time!("Parsing"); + parse = parser.parse(input); + } + trace!("Tree: \n{:?}", parse); + + match parse { + Ok(Some(t)) => { + let validate; + { + trace_time!("Type check"); + validate = t.validate(); + } + info!("Type: \n{:?}", validate); + + if let Ok(_) = validate { + let predict; + { + trace_time!("Prediction"); + predict = t.predict(); + } + info!("Predict: \n{:?}", predict); + + let execute; + { + trace_time!("Exectution"); + execute = t.execute(); + } + info!("Execution: \n{:?}", execute); + } + } + _ => (), + } } - info!("Tree: \n{:?}", out); } } diff --git a/src/predictors.rs b/src/predictors.rs new file mode 100644 index 0000000..d72e3c8 --- /dev/null +++ b/src/predictors.rs @@ -0,0 +1,48 @@ +use super::expression::Predictor; +use super::symbols::*; + +use super::database::space; + +impl Predictor for Projection { + fn predict(&self) -> f64 { + match self { + Projection::Nifti(_, _, bag) => bag.predict(), + Projection::JSON(_, _, bag) => bag.predict(), + } + } +} + +impl Predictor for Bag { + fn predict(&self) -> f64 { + match self { + Bag::Distinct(bag) => bag.predict(), + Bag::Filter(_, bag) => bag.predict(), + Bag::Complement(bag) => space::max_volume(bag.space()) - bag.predict(), + Bag::Intersection(lh, rh) => { + let l = lh.predict(); + let r = rh.predict(); + if l < r { + l + } else { + r + } + } + Bag::Union(lh, rh) => lh.predict() + rh.predict(), + Bag::Bag(bags) => { + let mut s = 0.0; + for bag in bags { + s += bag.predict(); + } + s + } + Bag::Inside(shape) => shape.predict(), + Bag::Outside(shape) => space::max_volume(shape.space()) - shape.predict(), + } + } +} + +impl Predictor for Shape { + fn predict(&self) -> f64 { + self.volume() + } +} diff --git a/src/queries.lalrpop b/src/queries.lalrpop index 29017cb..5334f24 100644 --- a/src/queries.lalrpop +++ b/src/queries.lalrpop @@ -1,7 +1,7 @@ use std::str::FromStr; -use crate::ast; -use crate::ast::*; +use crate::symbols; +use crate::database::space; grammar; @@ -10,7 +10,7 @@ grammar; //*********************************************************************/ pub Query = { Projections? }; -Projections: ast::Projection = { +Projections: symbols::Projection = { NiftiOperator, JsonOperator }; @@ -19,18 +19,39 @@ Projections: ast::Projection = { // each position where there is a point in bag_expression. // // If it is provided, it MUST resolve to a NUMBER. -NiftiOperator: ast::Projection = { - "nifti" "(" ")" => +NiftiOperator: symbols::Projection = { + "nifti" "(" + + + )?> + ")" => { + let space_id = match rs { + Some(id) => id, + None => space::name().clone(), + }; + if let Some((sel, _)) = s { - Projection::Nifti(sel, b) + symbols::Projection::Nifti(space_id, sel, b) } else { - Projection::Nifti(Vec::new(), b) + symbols::Projection::Nifti(space_id, symbols::LiteralSelector(Vec::new()), b) } + + } }; -JsonOperator: ast::Projection = { - "json" "(" "," ")" => - Projection::JSON(f, b) +JsonOperator: symbols::Projection = { + "json" "(" + "," + + )?> + ")" => { + let space_id = match rs { + Some(id) => id, + None => space::name().clone(), + }; + + symbols::Projection::JSON(space_id, f, b) + } }; //*********************************************************************/ @@ -41,73 +62,73 @@ JsonOperator: ast::Projection = { // https://github.com/antlr/grammars-v4/blob/master/json/JSON.g4 // // Some of the parser / lexer rules are in the imported grammar as well. -JsonValues: ast::JsonValue = { - String => JsonValue::String(<>), +JsonValues: symbols::JsonValue = { + String => symbols::JsonValue::String(<>), JsonNumber => <>, JsonObj => <>, JsonArray => <>, - "true" => JsonValue::Bool(true), - "false" => JsonValue::Bool(false), - "null" => JsonValue::Null, + "true" => symbols::JsonValue::Bool(true), + "false" => symbols::JsonValue::Bool(false), + "null" => symbols::JsonValue::Null, // Support reference to values from the selected bag. - Selector => JsonValue::Selector(<>), - Aggregations => JsonValue::Aggregation(<>) + Selector => symbols::JsonValue::Selector(<>), + Aggregations => symbols::JsonValue::Aggregation(<>) }; -JsonObj: ast::JsonValue = { +JsonObj: symbols::JsonValue = { "{" "}" => { if let Some((elem, list)) = exp { - let mut values: Vec<(String, JsonValue)> = vec![elem]; + let mut values = vec![elem]; for v in list { let (_, pair) = v; values.push(pair.clone()); } - JsonValue::Object(values) + symbols::JsonValue::Object(values) } else { - JsonValue::Object(Vec::new()) + symbols::JsonValue::Object(Vec::new()) } } }; -JsonPair: (String, ast::JsonValue) = { +JsonPair: (String, symbols::JsonValue) = { ":" => (s, v) }; -JsonArray: ast::JsonValue = { +JsonArray: symbols::JsonValue = { "[" "]" => { if let Some((elem, list)) = exp { - let mut values: Vec = vec![elem]; + let mut values = vec![elem]; for v in list.iter() { let (_, val) = v; values.push(val.clone()); } - JsonValue::Array(values) + symbols::JsonValue::Array(values) } else { - JsonValue::Array(Vec::new()) + symbols::JsonValue::Array(Vec::new()) } } }; // The bag expression is implicit here, as this is te // second argument to the json operator -Aggregations: ast::Aggregation = { +Aggregations: symbols::Aggregation = { "count" "(" ")" => { if let Some(_) = d { - Aggregation::Count(true, s) + symbols::Aggregation::Count(true, s) } else { - Aggregation::Count(false, s) + symbols::Aggregation::Count(false, s) } }, "sum" "(" ")" => - Aggregation::Sum(<>), + symbols::Aggregation::Sum(<>), "min" "(" ")" => - Aggregation::Min(<>), + symbols::Aggregation::Min(<>), "max" "(" ")" => - Aggregation::Max(<>), + symbols::Aggregation::Max(<>), }; //*********************************************************************/ @@ -116,7 +137,7 @@ Aggregations: ast::Aggregation = { pub Filters = { Bags }; // All these expressions generate bags. -Bags: ast::Bag = { +Bags: symbols::Bag = { // Bag Operators Distinct, Filter, @@ -129,49 +150,65 @@ Bags: ast::Bag = { Outside, // When used directly here, the inside() operation on the shape is // implied. - Shapes => Bag::Inside(<>) + Shapes => symbols::Bag::Inside(<>) }; //*********************************************************************/ // BAG OPERATORS */ //*********************************************************************/ -Distinct: ast::Bag = { +Distinct: symbols::Bag = { "distinct" "(" ")" => - Bag::Distinct(Box::new(<>)) + symbols::Bag::Distinct(Box::new(<>)) }; // Returns all the points which are NOT part of the bag. -Complement: ast::Bag = { +Complement: symbols::Bag = { "complement" "(" ")" => - Bag::Complement(Box::new(<>)) + symbols::Bag::Complement(Box::new(<>)) }; // Returns points which are part of both left and right sets. -Intersection: ast::Bag = { +Intersection: symbols::Bag = { "intersection" "(" "," ")" => - Bag::Intersection(Box::new(lh), Box::new(rh)) + symbols::Bag::Intersection(Box::new(lh), Box::new(rh)) }; // Returns points which are either part of left or right sets // (or both). -Union: ast::Bag = { +Union: symbols::Bag = { "union" "(" "," ")" => - Bag::Union(Box::new(lh), Box::new(rh)) + symbols::Bag::Union(Box::new(lh), Box::new(rh)) }; // Filters point so that points part of the resulting bag respect // the predicate. -Filter: ast::Bag = { +Filter: symbols::Bag = { // "filter" "(" "," ")" => "filter" "(" ")" => - Bag::Filter(None, Some(Box::new(b))), + symbols::Bag::Filter(None, Box::new(b)), "filter" "(" )?> ")" => match b { - Some(b) => Bag::Filter(Some(p), Some(Box::new(b))), - None => Bag::Filter(Some(p), None), + Some(b) => symbols::Bag::Filter(Some(p), Box::new(b)), + None => { + let v = space::bounding_box(space::name()); + let mut positions = Vec::new(); + for point in v { + let mut position = Vec::new(); + for c in point { + position.push(symbols::LiteralNumber::Int(*c)) + } + positions.push(symbols::LiteralPosition(position)); + } + let bb = symbols::Shape::HyperRectangle( + space::name().clone(), + positions + ); + + symbols::Bag::Filter(Some(p), Box::new(symbols::Bag::Inside(bb))) + } } }; -Predicates: ast::Predicate = { +Predicates: symbols::Predicate = { Less, Greater, Equal, @@ -180,41 +217,41 @@ Predicates: ast::Predicate = { Or }; -Less: ast::Predicate = { +Less: symbols::Predicate = { "<" "(" "," ")" => { - Predicate::Less(v, literal) + symbols::Predicate::Less(v, literal) } }; -Greater: ast::Predicate = { +Greater: symbols::Predicate = { ">" "(" "," ")" => { - Predicate::Greater(v, literal) + symbols::Predicate::Greater(v, literal) } }; -Equal: ast::Predicate = { +Equal: symbols::Predicate = { "=" "(" "," ")" => { - Predicate::Equal(v, literal) + symbols::Predicate::Equal(v, literal) } }; -Not: ast::Predicate = { +Not: symbols::Predicate = { "!" "(" ")" => - Predicate::Not(Box::new(p)) + symbols::Predicate::Not(Box::new(p)) }; -And: ast::Predicate = { +And: symbols::Predicate = { "&" "(" "," ")" => - Predicate::And(Box::new(lh), Box::new(rh)) + symbols::Predicate::And(Box::new(lh), Box::new(rh)) }; -Or: ast::Predicate = { +Or: symbols::Predicate = { "|" "(" "," ")" => - Predicate::Or(Box::new(lh), Box::new(rh)) + symbols::Predicate::Or(Box::new(lh), Box::new(rh)) }; // Arbitrary bag of positions. -Bag: ast::Bag = { +Bag: symbols::Bag = { "bag" "{" "}" => { let mut bags = vec![elem]; @@ -222,7 +259,7 @@ Bag: ast::Bag = { bags.push(b); } - Bag::Bag(bags) + symbols::Bag::Bag(bags) } }; @@ -240,15 +277,15 @@ Bag: ast::Bag = { // of the cube [0,0], [1,1]. // Returns the set of points outside the shape, (face included) -Outside: ast::Bag = { +Outside: symbols::Bag = { "outside" "(" ")" => - Bag::Outside(<>) + symbols::Bag::Outside(<>) }; // Returns the set of points inside the shape, (face included) -Inside: ast::Bag = { +Inside: symbols::Bag = { "inside" "(" ")" => - Bag::Inside(<>) + symbols::Bag::Inside(<>) }; //*********************************************************************/ @@ -257,7 +294,7 @@ Inside: ast::Bag = { // Shapes are defined in terms of POSITION, a.k.a a LiteralPosition // value, which is not a POSITIONS, which might be a filter for example. -Shapes: ast::Shape = { +Shapes: symbols::Shape = { Point, HyperRectangle, HyperSphere, @@ -266,30 +303,50 @@ Shapes: ast::Shape = { // If the hyperrectangle is aligned with the axes, then two points are // enough, if not we need all the points to be specified. -HyperRectangle: ast::Shape = { +HyperRectangle: symbols::Shape = { "hyperrectangle" "{" "," + )?> "}" => { + let space_id = match rs { + Some(id) => id, + None => space::name().clone(), + }; let mut pos = vec![l, h]; for (_, lh, _, rh) in list.iter() { pos.push(lh.clone()); pos.push(rh.clone()); } - Shape::HyperRectangle(pos) + symbols::Shape::HyperRectangle(space_id, pos) } }; // A hypersphere is defined by its center and a radius, independantly // of the number of dimensions of the space. -HyperSphere: ast::Shape = { - "hypersphere" "{" "," "}" => - Shape::HyperSphere(c, r) +HyperSphere: symbols::Shape = { + "hypersphere" "{" + "," + )?> + "}" => { + let space_id = match rs { + Some(id) => id, + None => space::name().clone(), + }; + + symbols::Shape::HyperSphere(space_id, c, r) + } }; -Point: ast::Shape = { - "point" "{" "}" => - Shape::Point(<>) +Point: symbols::Shape = { + "point" "{" )?> "}" => { + let space_id = match rs { + Some(id) => id, + None => space::name().clone(), + }; + + symbols::Shape::Point(space_id, pos) + } }; // Define a shape as the non-zero values in a NIfTI object, defined by @@ -299,14 +356,18 @@ Point: ast::Shape = { // rotation: [ position+ ], // Optional, no rotation by default // bytes: uri(STRING) // uri to the NIfTI object // } -Nifti: ast::Shape = { +Nifti: symbols::Shape = { "nifti" "{" - String "," - ( Position "," )? - ( "[" Position ( "," Position)* "]" "," )? - ByteProvider + + + + )?> "}" => { - Shape::Nifti() + let space_id = match rs { + Some(id) => id, + None => space::name().clone(), + }; + symbols::Shape::Nifti(space_id) } }; @@ -321,44 +382,44 @@ ByteProvider = { "uri" "(" String ")" }; // Always returns a vector of numbers, a.k.a a position (a scalar will // be represented as a vector of one element) -Positions: ast::Position = { +Positions: symbols::Position = { StrCmpICase, StrCmp, - Selector => Position::Selector(<>), - Position => Position::LiteralPosition(<>) + Selector => symbols::Position::Selector(<>), + Position => symbols::Position::Literal(<>) }; // Compare lexicographically two strings, and returns a `position`: // [-1] : String is lexicographically before, // [ 0] : is equal, // [ 1] : is after. -StrCmp: ast::Position = { +StrCmp: symbols::Position = { "str_cmp" "(" "," ")" => { - Position::StrCmp(s, v) + symbols::Position::StrCmp(s, v) } }; // Same, but case insensitive. -StrCmpICase: ast::Position = { +StrCmpICase: symbols::Position = { "str_cmp_ignore_case" "(" "," ")" => { - Position::StrCmpICase(s, v) + symbols::Position::StrCmpICase(s, v) } }; // FIXME: FIELDS are expected to be exisiting in the data model. Root Object is assumed to be the type of the ressource on which the POST call was done. -Selector = { - ( )+ +Selector: symbols::LiteralSelector = { + ( )+ => symbols::LiteralSelector(<>) }; -Position: ast::LiteralPosition = { +Position: symbols::LiteralPosition = { "[" )*> "]" => { - let mut pos: LiteralPosition = vec![element]; + let mut pos = vec![element]; for e in list.iter() { pos.push(e.clone()); } - pos + symbols::LiteralPosition(pos) } }; @@ -370,72 +431,75 @@ Position: ast::LiteralPosition = { // 1. start with a dot ('.') // 2. optionnally followed by a field name consisting of a letter or // underscore, followed by letters, numbers or underscore, -// 3. optionnally followed by brakets enclosing an natural number +// 3. optionnally followed by brakets enclosing a natural number // denoting an offset in a list or array. -Field: Field = { +Field: symbols::Field = { => { if let Some(pos) = n.rfind('[') { let name = &n[1..pos]; let index = &n[(pos+1)..(n.len()-1)]; let index = usize::from_str(index).unwrap(); - Field(String::from(name), Some(index)) + symbols::Field(String::from(name), Some(index)) } else { let name = &n[1..]; - Field(String::from(name), None) + symbols::Field(String::from(name), None) } } }; String: String = { - r#"["]([\\](["\\/bfnrt]|u[0-9a-fA-F]{4})|[^"\\\u0000-\u001F])*["]"# => - String::from(<>) + r#"["]([\\](["\\/bfnrt]|u[0-9a-fA-F]{4})|[^"\\\u0000-\u001F])*["]"# => { + let s = <>; + let l = s.len() - 1; + s[1..l].to_string() + } }; //*********************************************************************/ // TOKENS - NUMBERS */ //*********************************************************************/ // We define 3 kinds of number, to avoid ambiguities in the rules. -JsonNumber: ast::JsonValue = { +JsonNumber: symbols::JsonValue = { => match s { - None => JsonValue::JsonNumber(v), + None => symbols::JsonValue::JsonNumber(v), Some(_) => match v { - LiteralNumber::Int(x) => JsonValue::JsonNumber(LiteralNumber::Int(-x)), - LiteralNumber::Float(x) => JsonValue::JsonNumber(LiteralNumber::Float(-x)) + symbols::LiteralNumber::Int(x) => symbols::JsonValue::JsonNumber(symbols::LiteralNumber::Int(-x)), + symbols::LiteralNumber::Float(x) => symbols::JsonValue::JsonNumber(symbols::LiteralNumber::Float(-x)) } } }; -PositiveNumber: ast::LiteralNumber = { "+"? => v }; +PositiveNumber: symbols::LiteralNumber = { "+"? => v }; -Number: ast::LiteralNumber = { +Number: symbols::LiteralNumber = { "+" => v, "-" => match v { - LiteralNumber::Int(x) => LiteralNumber::Int(-x), - LiteralNumber::Float(x) => LiteralNumber::Float(-x) + symbols::LiteralNumber::Int(x) => symbols::LiteralNumber::Int(-x), + symbols::LiteralNumber::Float(x) => symbols::LiteralNumber::Float(-x) }, => v }; -Num: ast::LiteralNumber = { +Num: symbols::LiteralNumber = { r"0([.][0-9]+([eE][+\-]?(0|[1-9][0-9]*))?)?" => { if let Ok(v) = i64::from_str(<>) { - LiteralNumber::Int(v) + symbols::LiteralNumber::Int(v) } else { // Either parsing as a float succeed or we pass along // the error - LiteralNumber::Float(f64::from_str(<>).unwrap()) + symbols::LiteralNumber::Float(f64::from_str(<>).unwrap()) } }, r"[1-9][0-9]*([.][0-9]+)?([eE][+\-]?(0|[1-9][0-9]*))?" => { if let Ok(v) = i64::from_str(<>) { - LiteralNumber::Int(v) + symbols::LiteralNumber::Int(v) } else { // Either parsing as a float succeed or we pass along // the error - LiteralNumber::Float(f64::from_str(<>).unwrap()) + symbols::LiteralNumber::Float(f64::from_str(<>).unwrap()) } } }; diff --git a/src/symbols.rs b/src/symbols.rs new file mode 100644 index 0000000..3ea0cbe --- /dev/null +++ b/src/symbols.rs @@ -0,0 +1,319 @@ +use std::cmp::Ordering; + +use super::database::space; +pub use super::types::*; + +/**********************************************************************/ +/* FORMATTING DATA */ +/**********************************************************************/ +#[derive(Clone, Debug)] +pub enum Projection { + Nifti(String, LiteralSelector, Bag), + JSON(String, JsonValue, Bag), +} + +impl Projection { + pub fn space(&self) -> &String { + match self { + Projection::Nifti(space, _, _) => &space, + Projection::JSON(space, _, _) => &space, + } + } +} + +// JSON FORMAT +#[derive(Clone, Debug)] +pub enum JsonValue { + String(String), + JsonNumber(LiteralNumber), + Bool(bool), + Null, + Object(Vec<(String, JsonValue)>), + Array(Vec), + Selector(LiteralSelector), + Aggregation(Aggregation), +} + +#[derive(Clone, Debug)] +pub enum Aggregation { + Count(bool, LiteralSelector), + Sum(LiteralSelector), + Min(LiteralSelector), + Max(LiteralSelector), +} + +// NIFTI +#[derive(Clone, Debug)] +struct Transform { + reference: String, + offset: Vec, + rotation: Vec>, +} + +/**********************************************************************/ +/* SELECTING / FILTERING DATA */ +/**********************************************************************/ +#[derive(Clone, Debug)] +pub enum Bag { + Distinct(Box), + Filter(Option, Box), + Complement(Box), + Intersection(Box, Box), + Union(Box, Box), + Bag(Vec), + Inside(Shape), + Outside(Shape), +} + +impl Bag { + pub fn space(&self) -> &String { + match self { + Bag::Distinct(bag) => bag.space(), + Bag::Filter(_, bag) => bag.space(), + Bag::Complement(bag) => bag.space(), + Bag::Intersection(lh, _) => { + // We are assuming lh and rh are in the same space. + // Checked as part of the validation. + lh.space() + } + Bag::Union(lh, _) => { + // We are assuming lh and rh are in the same space. + // Checked as part of the validation. + lh.space() + } + Bag::Bag(_) => { + // Bags can be defined in different spaces, thus the output is + // always in the universe space. + space::name() + } + Bag::Inside(shape) => shape.space(), + Bag::Outside(shape) => shape.space(), + } + } +} +/**********************************************************************/ +/* BAG OPERATORS */ +/**********************************************************************/ +#[derive(Clone, Debug)] +pub enum Predicate { + Less(Position, LiteralPosition), + Greater(Position, LiteralPosition), + Equal(Position, LiteralPosition), + Not(Box), + And(Box, Box), + Or(Box, Box), +} + +/**********************************************************************/ +/* SPATIAL OPERATORS */ +/**********************************************************************/ + +/**********************************************************************/ +/* SHAPES */ +/**********************************************************************/ +#[derive(Clone, Debug)] +pub enum Shape { + Point(String, LiteralPosition), + HyperRectangle(String, Vec), + HyperSphere(String, LiteralPosition, LiteralNumber), + Nifti(String), +} + +impl Shape { + pub fn space(&self) -> &String { + match self { + Shape::Point(space, _) => space, + Shape::HyperRectangle(space, _) => space, + Shape::HyperSphere(space, _, _) => space, + Shape::Nifti(space) => space, + } + } + + pub fn volume(&self) -> f64 { + match self { + Shape::Point(_, _) => 1.0, // This is the smallest non-zero volume possible //TODO DOUBLE CHECK IT IS TRUE + Shape::HyperRectangle(_space, pos) => { + //TODO: At this time, only aligned to the axes, defined by two points, hyperrectangles are supported. + assert_eq!(pos.len(), 2); + + // We assume the first position is the low point, the second is + // the high point, this being true for each dimension. As we add + // an even number of points per extra dimension, we assume the + // last group is the high end, and the last position is the + // highest point. + let mut volume = 1.0; + let LiteralPosition(low) = &pos[0]; + let LiteralPosition(high) = &pos[pos.len() - 1]; + + // For each dimension, multiply by the length in that dimension + for i in 0..low.len() { + let l = match low[i] { + LiteralNumber::Int(x) => x as f64, + LiteralNumber::Float(x) => x, + }; + + let h = match high[i] { + LiteralNumber::Int(x) => x as f64, + LiteralNumber::Float(x) => x, + }; + + let length = if h > l { h - l } else { l - h }; + volume *= length; + } + + volume + } + Shape::HyperSphere(_space, pos, r) => { + // Formula from https://en.wikipedia.org/wiki/N-sphere#/media/File:N_SpheresVolumeAndSurfaceArea.png + let LiteralPosition(p) = pos; + let k = p.len(); // Number of dimensions. + + let r = match *r { + LiteralNumber::Int(x) => x as f64, + LiteralNumber::Float(x) => x, + }; + + let pi = std::f64::consts::PI; + let factor = 2.0 * pi; + + // Set starting values for the coefficient + let mut a = 2.0; + let mut i = 1; + + if (k % 2) == 0 { + a = pi; + i = 2; + } + + while i < k { + i += 2; + a *= factor; + a /= i as f64; + } + + a * r.powi(i as i32) + } + Shape::Nifti(_) => unimplemented!(), + } + } +} + +/**********************************************************************/ +/* POSITIONS */ +/**********************************************************************/ +#[derive(Clone, Debug)] +pub enum Position { + StrCmpICase(LiteralSelector, String), + StrCmp(LiteralSelector, String), + Selector(LiteralSelector), + Literal(LiteralPosition), +} + +/**********************************************************************/ +/* Literals / TOKENS */ +/**********************************************************************/ + +#[derive(Clone, Debug)] +pub struct Field(pub String, pub Option); + +#[derive(Clone, Debug)] +pub enum LiteralNumber { + Int(i64), + Float(f64), +} + +impl PartialEq for LiteralNumber { + fn eq(&self, other: &LiteralNumber) -> bool { + match self { + LiteralNumber::Int(l) => match other { + LiteralNumber::Int(r) => l == r, + LiteralNumber::Float(_) => false, + }, + LiteralNumber::Float(l) => match other { + LiteralNumber::Int(r) => l == &(*r as f64), + LiteralNumber::Float(r) => l == r, + }, + } + } +} + +#[derive(Clone, Debug)] +pub struct LiteralPosition(pub Vec); + +impl LiteralPosition { + pub fn get_type(&self) -> LiteralTypes { + let Self(v) = self; + let mut t = Vec::new(); + + for n in v { + t.push(match n { + LiteralNumber::Int(_) => LiteralTypes::Int, + LiteralNumber::Float(_) => LiteralTypes::Float, + }); + } + + LiteralTypes::Vector(t) + } + + pub fn length(&self) -> f64 { + let LiteralPosition(v) = self; + let mut a = 0.0; + + for x in v { + let x = match x { + LiteralNumber::Int(x) => (*x) as f64, + LiteralNumber::Float(x) => *x, + }; + + a += x * x; + } + + a + } +} + +impl PartialOrd for LiteralPosition { + fn partial_cmp(&self, other: &LiteralPosition) -> Option { + let LiteralPosition(lh) = self; + let LiteralPosition(rh) = other; + + if lh.len() != rh.len() { + None + } else { + // Order is defined by the geometric length of the vector between the Origin and the point. + let l = self.length(); + let r = other.length(); + + l.partial_cmp(&r) + } + } +} + +impl PartialEq for LiteralPosition { + fn eq(&self, other: &LiteralPosition) -> bool { + let LiteralPosition(lh) = self; + let LiteralPosition(rh) = other; + + if lh.len() == rh.len() { + for i in 0..lh.len() { + if lh[i] != rh[i] { + return false; + } + } + + true + } else { + false + } + } +} + +#[derive(Clone, Debug)] +pub struct LiteralSelector(pub Vec); + +impl LiteralSelector { + pub fn get_type(&self) -> LiteralTypes { + // FIXME: Pretend for now that everything is a number, needs to be actually looked up in data model. + LiteralTypes::Int + } +} diff --git a/src/tests.rs b/src/tests.rs index 8e0614a..2d226d2 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,944 +1,955 @@ -//use super::ast; - -/******************************************************************/ -/* FORMATTING DATA */ -/******************************************************************/ #[cfg(test)] -mod query { - use crate::queries; - - fn query_parser() -> queries::QueryParser { - queries::QueryParser::new() - } - - #[test] - fn query() { - let p = query_parser(); - - let nifti = "nifti(point{[0]})"; - - // Option is Empty - assert!(p.parse("").is_ok()); - - // Option is there - assert!(p.parse(nifti).is_ok()); - - // Too many element - assert!(p.parse(format!("{} {}", nifti, nifti).as_str()).is_err()); - } - - /* Not useful to test this rule - #[test] - fn projections() { - let p = query_parser(); - - let nifti = "nifti(point{[0]})"; - let json = "json(., point{[0]})"; - - // Each alternative - assert!(p.parse(nifti).is_ok()); - assert!(p.parse(json).is_ok()); - } - */ - - #[test] - fn nifti_operator() { - let p = query_parser(); - - // Check allowed forms of the operator - assert!(p.parse("nifti(point{[0]})").is_ok()); - assert!(p.parse("nifti(.properties.id, point{[0]})").is_ok()); - - //FIXME: THIS SHOULD BE ALLOWED - assert!(p.parse("nifti(2, point{[0]})").is_ok()); - assert!(p.parse("nifti(2.23, point{[0]})").is_ok()); - - //FIXME: SYNTAX OK, TYPE NOT - assert!(p.parse("nifti(\"asd\", point{[0]})").is_err()); - } - - #[test] - fn json_operator() { - let p = query_parser(); - - assert!(p.parse("json(true, point{[0]})").is_ok()); - assert!(p.parse("json(23, point{[0]})").is_ok()); - assert!(p.parse("json([23, 24], point{[0]})").is_ok()); - assert!(p.parse("json([23, count(.)], point{[0]})").is_ok()); - - assert!(p.parse("json(true)").is_err()); - assert!(p.parse("json(true,)").is_err()); - - assert!(p.parse("json(, point{[0]})").is_err()); - assert!(p.parse("json(point{[0]})").is_err()); - - assert!(p.parse("json(true, point)").is_err()); - } - - #[test] - fn json_values() { - let p = query_parser(); - - assert!(p - .parse(format!("json({}, point{{[0]}})", "true").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "false").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "null").as_str()) - .is_ok()); - - // Incorrect capitalisation - assert!(p - .parse(format!("json({}, point{{[0]}})", "True").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "False").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "Null").as_str()) - .is_err()); - } - - #[test] - fn json_obj() { - let p = query_parser(); - - assert!(p - .parse(format!("json({}, point{{[0]}})", "{}").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{\"field\": 0}").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{\"field\": 0, \"field1\": 1}").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{\"field\": [0, 1]}").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{\"field\": {\"field1\": 0}}").as_str()) - .is_ok()); - assert!(p - .parse( - format!( - "json({}, point{{[0]}})", - "{\"field\": [{\"field1\": 0}, {\"field1\": 1}]}" - ) - .as_str() - ) - .is_ok()); - } - - #[test] - fn json_pair() { - let p = query_parser(); - - assert!(p - .parse(format!("json({}, point{{[0]}})", "{:}").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{field: 0}").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{0: 0}").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{\"0\": }").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{\"0\": 0 }").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{\"field\": 0 }").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "{\"field\": \"0\" }").as_str()) - .is_ok()); - } - - #[test] - fn json_array() { - let p = query_parser(); - - assert!(p - .parse(format!("json({}, point{{[0]}})", "[, 0]").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "[]").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "[0]").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "[0, 1]").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "[{\"field\": 0}, {\"field\": 1}]").as_str()) - .is_ok()); - } - - #[test] - fn aggregations() { - let p = query_parser(); - - //FIXME: ADD STUFF - - // count () - assert!(p - .parse(format!("json({}, point{{[0]}})", "count()").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "count(distinct)").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "count(.)").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "count(distinct .)").as_str()) - .is_ok()); - - // sum () - assert!(p - .parse(format!("json({}, point{{[0]}})", "sum()").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "sum(.)").as_str()) - .is_ok()); - - // min () - assert!(p - .parse(format!("json({}, point{{[0]}})", "min()").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "min(.)").as_str()) - .is_ok()); - - // max () - assert!(p - .parse(format!("json({}, point{{[0]}})", "max()").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "max(.)").as_str()) - .is_ok()); - } - - #[test] - fn json_numbers() { - let p = query_parser(); - - // Integers - assert!(p - .parse(format!("json({}, point{{[0]}})", "0").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "+0").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "-0").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "1").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "+1").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "-1").as_str()) - .is_ok()); - - // Floating point values - assert!(p - .parse(format!("json({}, point{{[0]}})", "0.0").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "+0.0").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "-0.0").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "0.1").as_str()) - .is_ok()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "+0.01").as_str()) - .is_err()); - assert!(p - .parse(format!("json({}, point{{[0]}})", "-0.01").as_str()) - .is_ok()); - } -} - -#[cfg(test)] -mod filters { - use crate::queries; +mod parsing { /******************************************************************/ - /* SELECTING / FILTERING DATA */ + /* FORMATTING DATA */ /******************************************************************/ - fn filters_parser() -> queries::FiltersParser { - queries::FiltersParser::new() - } + #[cfg(test)] + mod query { + use crate::queries; - #[test] - fn filters() { - let p = filters_parser(); - - assert!(p.parse("").is_err()); - - assert!(p.parse("point{[0]}").is_ok()); - } - - /* Not useful to test this rule - #[test] - fn bags() { - let p = filters_parser(); - } */ - - #[test] - fn distinct() { - let p = filters_parser(); - - assert!(p.parse("distinct()").is_err()); - - assert!(p.parse("distinct(point{[0]})").is_ok()); - } - - #[test] - fn complement() { - let p = filters_parser(); - - assert!(p.parse("complement()").is_err()); - - assert!(p.parse("complement(point{[0]})").is_ok()); - } - - #[test] - fn intersection() { - let p = filters_parser(); - - assert!(p.parse("intersection()").is_err()); - assert!(p.parse("intersection(point{[0]})").is_err()); - assert!(p - .parse("intersection(point{[0]}, point{[0]}, point{[0]})") - .is_err()); - - assert!(p.parse("intersection(point{[0]}, point{[0]})").is_ok()); - } - - #[test] - fn union() { - let p = filters_parser(); - - assert!(p.parse("union()").is_err()); - assert!(p.parse("union(point{[0]})").is_err()); - assert!(p - .parse("union(point{[0]}, point{[0]}, point{[0]})") - .is_err()); - - assert!(p.parse("union(point{[0]}, point{[0]})").is_ok()); - } - - #[test] - fn filter() { - let p = filters_parser(); - - assert!(p.parse("filter()").is_err()); - assert!(p.parse("filter(point{[0]})").is_ok()); - assert!(p.parse("filter(=(., [0]))").is_ok()); - - assert!(p.parse("filter(=(., [0]), point{[0]})").is_ok()); - } - - /* Not useful to test this rule - #[test] - fn predicates() { - let p = filters_parser(); - }*/ - - #[test] - fn less() { - let p = filters_parser(); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "<(., [0])").as_str()) - .is_ok()); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "<(, [0])").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", "<(.)").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", "<()").as_str()) - .is_err()); - } - - #[test] - fn greater() { - let p = filters_parser(); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", ">(., [0])").as_str()) - .is_ok()); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", ">(, [0])").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", ">(.)").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", ">()").as_str()) - .is_err()); - } - - #[test] - fn equal() { - let p = filters_parser(); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "=(., [0])").as_str()) - .is_ok()); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "=(, [0])").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", "=(.)").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", "=()").as_str()) - .is_err()); - } - - #[test] - fn not() { - let p = filters_parser(); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "!(=(., [0]))").as_str()) - .is_ok()); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "!()").as_str()) - .is_err()); - } - - #[test] - fn and() { - let p = filters_parser(); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "&(=(., [0]), =(., [0]))").as_str()) - .is_ok()); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "&(, =(., [0]))").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", "&(|(=(., [0])))").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", "&()").as_str()) - .is_err()); - } - - #[test] - fn or() { - let p = filters_parser(); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "|(=(., [0]), =(., [0]))").as_str()) - .is_ok()); - - assert!(p - .parse(format!("filter({}, point{{[0]}})", "|(, =(., [0]))").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", "|(|(=(., [0])))").as_str()) - .is_err()); - assert!(p - .parse(format!("filter({}, point{{[0]}})", "|()").as_str()) - .is_err()); - } - - #[test] - fn bag() { - let p = filters_parser(); - - assert!(p.parse("bag{}").is_err()); - - assert!(p.parse("bag{point{[0]}}").is_ok()); - assert!(p.parse("bag{point{[0]}, point{[0]}}").is_ok()); - assert!(p.parse("bag{point{[0]}, point{[0]}, point{[0]}}").is_ok()); - assert!(p - .parse("bag{point{[0]}, hypersphere{[0], 1}, hyperrectangle{[0], [1]}}") - .is_ok()); - } - - #[test] - fn outside() { - let p = filters_parser(); - - assert!(p.parse("outside()").is_err()); - - assert!(p.parse("outside(point{[0]})").is_ok()); - } - - #[test] - fn inside() { - let p = filters_parser(); - - assert!(p.parse("inside()").is_err()); - - assert!(p.parse("inside(point{[0]})").is_ok()); - } - - /* Not useful to test this rule - #[test] - fn shapes() { - let p = filters_parser(); - - assert!(p.parse("point{[0]}").is_ok()); - assert!(p.parse("hyperrectangle{[0], [1]}").is_ok()); - assert!(p.parse("hypersphere{[0], 1}").is_ok()); - assert!(p.parse("nifti{\"\", uri(\"\")}").is_ok()); - }*/ - - #[test] - fn hyperrectangle() { - let p = filters_parser(); - - // At least two positions when it is aligned with the axis, otherwise an even number - // of positions, as the number of vertices follows the rule 2**k, where k is the number - // of dimensions of the space containing the hyperrectangle. - assert!(p.parse("hyperrectangle{}").is_err()); - assert!(p.parse("hyperrectangle{[]}").is_err()); - assert!(p.parse("hyperrectangle{[0]}").is_err()); - assert!(p.parse("hyperrectangle{[0], [1], [2]}").is_err()); - assert!(p.parse("hyperrectangle{[0], [1], [2], [3], [4]}").is_err()); - - assert!(p.parse("hyperrectangle{[0], [1]}").is_ok()); - assert!(p.parse("hyperrectangle{[0], [1], [2], [3]}").is_ok()); - assert!(p - .parse("hyperrectangle{[0], [1], [2], [3], [4], [5]}") - .is_ok()); - } - - #[test] - fn hyperrsphere() { - let p = filters_parser(); - - assert!(p.parse("hypersphere{}").is_err()); - assert!(p.parse("hypersphere{[]}").is_err()); - assert!(p.parse("hypersphere{[0]}").is_err()); - - assert!(p.parse("hypersphere{[0], 23}").is_ok()); - } - - #[test] - fn point() { - let p = filters_parser(); - - assert!(p.parse("point{}").is_err()); - assert!(p.parse("point{[]}").is_err()); - - assert!(p.parse("point{[0]}").is_ok()); - } - - #[test] - fn nifti() { - let p = filters_parser(); - //FIXME: ADD STUFF - assert!(false); - } - - #[test] - fn byte_provider() { - let p = filters_parser(); - //FIXME: ADD STUFF - assert!(false); - } - - /* Not useful to test this rule - #[test] - fn positions() { - let p = filters_parser(); - - assert!(p - .parse( - format!( - "filter(=({}, [1]), point{{[0]}})", - "str_cmp_ignore_case(.field, \"\")" - ) - .as_str() - ) - .is_ok()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", "str_cmp(.field, \"\")").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", "[0]").as_str()) - .is_ok()); - - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", "point{[0]}").as_str()) - .is_err()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", "{0}").as_str()) - .is_err()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", "").as_str()) - .is_err()); - }*/ - - #[test] - fn str_cmp() { - let p = filters_parser(); - - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", "str_cmp(.field, \"\")").as_str()) - .is_ok()); - - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", "str_cmp(.field)").as_str()) - .is_err()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", "str_cmp(\"\")").as_str()) - .is_err()); - } - - #[test] - fn str_cmp_icase() { - let p = filters_parser(); - - assert!(p - .parse( - format!( - "filter(=({}, [1]), point{{[0]}})", - "str_cmp_ignore_case(.field, \"\")" - ) - .as_str() - ) - .is_ok()); - - assert!(p - .parse( - format!( - "filter(=({}, [1]), point{{[0]}})", - "str_cmp_ignore_case(.field)" - ) - .as_str() - ) - .is_err()); - assert!(p - .parse( - format!( - "filter(=({}, [1]), point{{[0]}})", - "str_cmp_ignore_case(\"\")" - ) - .as_str() - ) - .is_err()); - } - - #[test] - fn selector() { - let p = filters_parser(); - - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", ".").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field.field").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field[1].field").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field.field[1]").as_str()) - .is_ok()); - } - - #[test] - fn position() { - let p = filters_parser(); - - // Empty - assert!(p.parse(format!("point{{{}}}", "[]").as_str()).is_err()); - - // Non-numerical coordinate: - assert!(p.parse(format!("point{{{}}}", "[aa]").as_str()).is_err()); - - assert!(p - .parse(format!("point{{{}}}", "[\"aa\"]").as_str()) - .is_err()); - - // One or more coordinates - assert!(p.parse(format!("point{{{}}}", "[0]").as_str()).is_ok()); - assert!(p.parse(format!("point{{{}}}", "[0, 0]").as_str()).is_ok()); - assert!(p - .parse(format!("point{{{}}}", "[0, 0, 0]").as_str()) - .is_ok()); - assert!(p - .parse(format!("point{{{}}}", "[0, 0, 0, 0]").as_str()) - .is_ok()); - assert!(p - .parse(format!("point{{{}}}", "[0,0,0,0]").as_str()) - .is_ok()); - } - - #[test] - fn field() { - let p = filters_parser(); - - // Single dot - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".").as_str()) - .is_ok()); - - // Check first character is within allowed characters - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".a").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", "._").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".2").as_str()) - .is_err()); - - // Check second character is within allowed characters - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".fa").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f2").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f_").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f2").as_str()) - .is_ok()); - - // Check we can add subscript - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".[23]").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[0]").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[2]").as_str()) - .is_ok()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[23]").as_str()) - .is_ok()); - - // Invalid index values - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[2.3]").as_str()) - .is_err()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[02]").as_str()) - .is_err()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[-2]").as_str()) - .is_err()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[2e2]").as_str()) - .is_err()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[2E2]").as_str()) - .is_err()); - assert!(p - .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[+2]").as_str()) - .is_err()); - } - - #[test] - fn string() { - fn test_str_ok(p: &queries::FiltersParser, string: &str) { - let n = format!( - "{}{}{}", - "nifti{", string, ", uri(\"http://a.nifti.file\") }" - ); - let n = n.as_str(); - - assert!(p.parse(n).is_ok()); + fn query_parser() -> queries::QueryParser { + queries::QueryParser::new() } - fn test_str_err(p: &queries::FiltersParser, string: &str) { - let n = format!( - "{}{}{}", - "nifti{", string, ", uri(\"http://a.nifti.file\") }" - ); - let n = n.as_str(); + #[test] + fn query() { + let p = query_parser(); - assert!(p.parse(n).is_err()); + let nifti = "nifti(point{[0]})"; + + // Option is Empty + assert!(p.parse("").is_ok()); + + // Option is there + assert!(p.parse(nifti).is_ok()); + + // Too many element + assert!(p.parse(format!("{} {}", nifti, nifti).as_str()).is_err()); } - let p = &filters_parser(); + /* Not useful to test this rule + #[test] + fn projections() { + let p = query_parser(); - // Empty String - test_str_ok(p, r#""""#); + let nifti = "nifti(point{[0]})"; + let json = "json(., point{[0]})"; - // Usual escapes - test_str_ok(p, r#""\"""#); - test_str_ok(p, r#""\\""#); - test_str_ok(p, r#""\/""#); - test_str_ok(p, r#""\b""#); - test_str_ok(p, r#""\f""#); - test_str_ok(p, r#""\n""#); - test_str_ok(p, r#""\r""#); - test_str_ok(p, r#""\t""#); + // Each alternative + assert!(p.parse(nifti).is_ok()); + assert!(p.parse(json).is_ok()); + } + */ - // Unicode Escape - test_str_ok(p, r#""\u0012""#); - test_str_ok(p, r#""\u001F""#); - test_str_ok(p, r#""\u001a""#); + #[test] + fn nifti_operator() { + let p = query_parser(); - // ASCI Letters & digit - test_str_ok(p, r#""abcdefghijklmnopqrstuvwxyz""#); - test_str_ok(p, r#""ABCDEFGHIJKLMNOPQRSTUVWXYZ""#); - test_str_ok(p, r#""0123456789""#); + // Check allowed forms of the operator + assert!(p.parse("nifti(point{[0]})").is_ok()); + assert!(p.parse("nifti(.properties.id, point{[0]})").is_ok()); - // Space and some non-white characters - test_str_ok(p, r#"" ,.-;:!?'^&|§°+*ç%_""#); + unimplemented!(); // TO REMEMBER SOME WORK IS DUE HERE. - // Invalid - test_str_err(p, "\"\u{0010}\""); // rust requires \u{..}, while JSON does not. + //FIXME: THIS SHOULD BE ALLOWED + assert!(p.parse("nifti(2, point{[0]})").is_ok()); + assert!(p.parse("nifti(2.23, point{[0]})").is_ok()); + + //FIXME: SYNTAX OK, TYPE NOT + assert!(p.parse("nifti(point{[0], \"space\"})").is_err()); + } + + #[test] + fn json_operator() { + let p = query_parser(); + + assert!(p.parse("json(true, point{[0]})").is_ok()); + assert!(p.parse("json(23, point{[0]})").is_ok()); + assert!(p.parse("json([23, 24], point{[0]})").is_ok()); + assert!(p.parse("json([23, count(.)], point{[0]})").is_ok()); + + assert!(p.parse("json(true)").is_err()); + assert!(p.parse("json(true,)").is_err()); + + assert!(p.parse("json(, point{[0]})").is_err()); + assert!(p.parse("json(point{[0]})").is_err()); + + assert!(p.parse("json(true, point)").is_err()); + } + + #[test] + fn json_values() { + let p = query_parser(); + + assert!(p + .parse(format!("json({}, point{{[0]}})", "true").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "false").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "null").as_str()) + .is_ok()); + + // Incorrect capitalisation + assert!(p + .parse(format!("json({}, point{{[0]}})", "True").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "False").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "Null").as_str()) + .is_err()); + } + + #[test] + fn json_obj() { + let p = query_parser(); + + assert!(p + .parse(format!("json({}, point{{[0]}})", "{}").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{\"field\": 0}").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{\"field\": 0, \"field1\": 1}").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{\"field\": [0, 1]}").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{\"field\": {\"field1\": 0}}").as_str()) + .is_ok()); + assert!(p + .parse( + format!( + "json({}, point{{[0]}})", + "{\"field\": [{\"field1\": 0}, {\"field1\": 1}]}" + ) + .as_str() + ) + .is_ok()); + } + + #[test] + fn json_pair() { + let p = query_parser(); + + assert!(p + .parse(format!("json({}, point{{[0]}})", "{:}").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{field: 0}").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{0: 0}").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{\"0\": }").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{\"0\": 0 }").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{\"field\": 0 }").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "{\"field\": \"0\" }").as_str()) + .is_ok()); + } + + #[test] + fn json_array() { + let p = query_parser(); + + assert!(p + .parse(format!("json({}, point{{[0]}})", "[, 0]").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "[]").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "[0]").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "[0, 1]").as_str()) + .is_ok()); + assert!(p + .parse( + format!("json({}, point{{[0]}})", "[{\"field\": 0}, {\"field\": 1}]").as_str() + ) + .is_ok()); + } + + #[test] + fn aggregations() { + let p = query_parser(); + + // count () + assert!(p + .parse(format!("json({}, point{{[0]}})", "count()").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "count(distinct)").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "count(.)").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "count(distinct .)").as_str()) + .is_ok()); + + // sum () + assert!(p + .parse(format!("json({}, point{{[0]}})", "sum()").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "sum(.)").as_str()) + .is_ok()); + + // min () + assert!(p + .parse(format!("json({}, point{{[0]}})", "min()").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "min(.)").as_str()) + .is_ok()); + + // max () + assert!(p + .parse(format!("json({}, point{{[0]}})", "max()").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "max(.)").as_str()) + .is_ok()); + } + + #[test] + fn json_numbers() { + let p = query_parser(); + + // Integers + assert!(p + .parse(format!("json({}, point{{[0]}})", "0").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "+0").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "-0").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "1").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "+1").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "-1").as_str()) + .is_ok()); + + // Floating point values + assert!(p + .parse(format!("json({}, point{{[0]}})", "0.0").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "+0.0").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "-0.0").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "0.1").as_str()) + .is_ok()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "+0.01").as_str()) + .is_err()); + assert!(p + .parse(format!("json({}, point{{[0]}})", "-0.01").as_str()) + .is_ok()); + } } - #[test] - fn positive_numbers() { - let p = filters_parser(); + #[cfg(test)] + mod filters { + use crate::queries; - // Integers - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "0").as_str()) - .is_ok()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "+0").as_str()) - .is_ok()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "-0").as_str()) - .is_err()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "1").as_str()) - .is_ok()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "+1").as_str()) - .is_ok()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "-1").as_str()) - .is_err()); + /******************************************************************/ + /* SELECTING / FILTERING DATA */ + /******************************************************************/ + fn filters_parser() -> queries::FiltersParser { + queries::FiltersParser::new() + } - // Floating point values - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "0.0").as_str()) - .is_ok()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "+0.0").as_str()) - .is_ok()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "-0.0").as_str()) - .is_err()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "0.1").as_str()) - .is_ok()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "+0.01").as_str()) - .is_ok()); - assert!(p - .parse(format!("hypersphere{{[0],{}}}", "-0.01").as_str()) - .is_err()); - } + #[test] + fn filters() { + let p = filters_parser(); - #[test] - fn numbers() { - let p = filters_parser(); + assert!(p.parse("").is_err()); - // Integers - assert!(p.parse(format!("point{{[{}]}}", "0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "+0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "-0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "+1").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "-1").as_str()).is_ok()); + assert!(p.parse("point{[0]}").is_ok()); + } - // Floating point values - assert!(p.parse(format!("point{{[{}]}}", "0.0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "+0.0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "-0.0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "0.1").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "+0.01").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "-0.01").as_str()).is_ok()); - } + /* Not useful to test this rule + #[test] + fn bags() { + let p = filters_parser(); + } */ - #[test] - fn num() { - let p = filters_parser(); + #[test] + fn distinct() { + let p = filters_parser(); - // Integers - assert!(p.parse(format!("point{{[{}]}}", "0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1e2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1e+2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1e-2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1E2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "100").as_str()).is_ok()); + assert!(p.parse("distinct()").is_err()); - assert!(p.parse(format!("point{{[{}]}}", "010").as_str()).is_err()); + assert!(p.parse("distinct(point{[0]})").is_ok()); + } - // Floating point values (normalized) - assert!(p.parse(format!("point{{[{}]}}", "0.0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "0.1").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "0.1e0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "0.1e2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "0.1e+2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "0.1e-2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "0.1E2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "0.1E23").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "0.01").as_str()).is_ok()); + #[test] + fn complement() { + let p = filters_parser(); - assert!(p.parse(format!("point{{[{}]}}", "0.").as_str()).is_err()); - assert!(p - .parse(format!("point{{[{}]}}", "0.1E03").as_str()) - .is_err()); - assert!(p - .parse(format!("point{{[{}]}}", "0.1E0.3").as_str()) - .is_err()); + assert!(p.parse("complement()").is_err()); - // Floating point values (denormalized) - assert!(p.parse(format!("point{{[{}]}}", "1.0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1.1").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1.1e0").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1.1e2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1.1e+2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1.1e-2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1.1E2").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1.1E23").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "1.01").as_str()).is_ok()); - assert!(p.parse(format!("point{{[{}]}}", "10.1").as_str()).is_ok()); + assert!(p.parse("complement(point{[0]})").is_ok()); + } - assert!(p.parse(format!("point{{[{}]}}", "1.").as_str()).is_err()); - assert!(p.parse(format!("point{{[{}]}}", "01.1").as_str()).is_err()); - assert!(p - .parse(format!("point{{[{}]}}", "1.1E03").as_str()) - .is_err()); - assert!(p - .parse(format!("point{{[{}]}}", "1.1E0.3").as_str()) - .is_err()); + #[test] + fn intersection() { + let p = filters_parser(); + + assert!(p.parse("intersection()").is_err()); + assert!(p.parse("intersection(point{[0]})").is_err()); + assert!(p + .parse("intersection(point{[0]}, point{[0]}, point{[0]})") + .is_err()); + + assert!(p.parse("intersection(point{[0]}, point{[0]})").is_ok()); + } + + #[test] + fn union() { + let p = filters_parser(); + + assert!(p.parse("union()").is_err()); + assert!(p.parse("union(point{[0]})").is_err()); + assert!(p + .parse("union(point{[0]}, point{[0]}, point{[0]})") + .is_err()); + + assert!(p.parse("union(point{[0]}, point{[0]})").is_ok()); + } + + #[test] + fn filter() { + let p = filters_parser(); + + assert!(p.parse("filter()").is_err()); + assert!(p.parse("filter(point{[0]})").is_ok()); + assert!(p.parse("filter(=(., [0]))").is_ok()); + + assert!(p.parse("filter(=(., [0]), point{[0]})").is_ok()); + } + + /* Not useful to test this rule + #[test] + fn predicates() { + let p = filters_parser(); + }*/ + + #[test] + fn less() { + let p = filters_parser(); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "<(., [0])").as_str()) + .is_ok()); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "<(, [0])").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", "<(.)").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", "<()").as_str()) + .is_err()); + } + + #[test] + fn greater() { + let p = filters_parser(); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", ">(., [0])").as_str()) + .is_ok()); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", ">(, [0])").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", ">(.)").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", ">()").as_str()) + .is_err()); + } + + #[test] + fn equal() { + let p = filters_parser(); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "=(., [0])").as_str()) + .is_ok()); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "=(, [0])").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", "=(.)").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", "=()").as_str()) + .is_err()); + } + + #[test] + fn not() { + let p = filters_parser(); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "!(=(., [0]))").as_str()) + .is_ok()); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "!()").as_str()) + .is_err()); + } + + #[test] + fn and() { + let p = filters_parser(); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "&(=(., [0]), =(., [0]))").as_str()) + .is_ok()); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "&(, =(., [0]))").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", "&(|(=(., [0])))").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", "&()").as_str()) + .is_err()); + } + + #[test] + fn or() { + let p = filters_parser(); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "|(=(., [0]), =(., [0]))").as_str()) + .is_ok()); + + assert!(p + .parse(format!("filter({}, point{{[0]}})", "|(, =(., [0]))").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", "|(|(=(., [0])))").as_str()) + .is_err()); + assert!(p + .parse(format!("filter({}, point{{[0]}})", "|()").as_str()) + .is_err()); + } + + #[test] + fn bag() { + let p = filters_parser(); + + assert!(p.parse("bag{}").is_err()); + + assert!(p.parse("bag{point{[0]}}").is_ok()); + assert!(p.parse("bag{point{[0]}, point{[0]}}").is_ok()); + assert!(p.parse("bag{point{[0]}, point{[0]}, point{[0]}}").is_ok()); + assert!(p + .parse("bag{point{[0]}, hypersphere{[0], 1}, hyperrectangle{[0], [1]}}") + .is_ok()); + } + + #[test] + fn outside() { + let p = filters_parser(); + + assert!(p.parse("outside()").is_err()); + + assert!(p.parse("outside(point{[0]})").is_ok()); + } + + #[test] + fn inside() { + let p = filters_parser(); + + assert!(p.parse("inside()").is_err()); + + assert!(p.parse("inside(point{[0]})").is_ok()); + } + + /* Not useful to test this rule + #[test] + fn shapes() { + let p = filters_parser(); + + assert!(p.parse("point{[0]}").is_ok()); + assert!(p.parse("hyperrectangle{[0], [1]}").is_ok()); + assert!(p.parse("hypersphere{[0], 1}").is_ok()); + assert!(p.parse("nifti{\"\", uri(\"\")}").is_ok()); + }*/ + + #[test] + fn hyperrectangle() { + let p = filters_parser(); + + // At least two positions when it is aligned with the axis, otherwise an even number + // of positions, as the number of vertices follows the rule 2**k, where k is the number + // of dimensions of the space containing the hyperrectangle. + assert!(p.parse("hyperrectangle{}").is_err()); + assert!(p.parse("hyperrectangle{[]}").is_err()); + assert!(p.parse("hyperrectangle{[0]}").is_err()); + assert!(p.parse("hyperrectangle{[0], [1], [2]}").is_err()); + assert!(p.parse("hyperrectangle{[0], [1], [2], [3], [4]}").is_err()); + + assert!(p.parse("hyperrectangle{[0], [1]}").is_ok()); + assert!(p.parse("hyperrectangle{[0], [1], \"space\"}").is_ok()); + assert!(p.parse("hyperrectangle{[0], [1], [2], [3]}").is_ok()); + assert!(p.parse("hyperrectangle{[0], [1], [2], [3]}").is_ok()); + assert!(p + .parse("hyperrectangle{[0], [1], [2], [3], [4], [5]}") + .is_ok()); + assert!(p + .parse("hyperrectangle{[0], [1], [2], [3], [4], [5], \"space\"}") + .is_ok()); + } + + #[test] + fn hyperrsphere() { + let p = filters_parser(); + + assert!(p.parse("hypersphere{}").is_err()); + assert!(p.parse("hypersphere{[]}").is_err()); + assert!(p.parse("hypersphere{[0]}").is_err()); + + assert!(p.parse("hypersphere{[0], 23}").is_ok()); + assert!(p.parse("hypersphere{[0], 23, \"space\"}").is_ok()); + } + + #[test] + fn point() { + let p = filters_parser(); + + assert!(p.parse("point{}").is_err()); + assert!(p.parse("point{[]}").is_err()); + + assert!(p.parse("point{[0]}").is_ok()); + assert!(p.parse("point{[0], \"space\"}").is_ok()); + } + + #[test] + fn nifti() { + let _p = filters_parser(); + unimplemented!(); + } + + #[test] + fn byte_provider() { + let _p = filters_parser(); + unimplemented!(); + } + + /* Not useful to test this rule + #[test] + fn positions() { + let p = filters_parser(); + + assert!(p + .parse( + format!( + "filter(=({}, [1]), point{{[0]}})", + "str_cmp_ignore_case(.field, \"\")" + ) + .as_str() + ) + .is_ok()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", "str_cmp(.field, \"\")").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", "[0]").as_str()) + .is_ok()); + + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", "point{[0]}").as_str()) + .is_err()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", "{0}").as_str()) + .is_err()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", "").as_str()) + .is_err()); + }*/ + + #[test] + fn str_cmp() { + let p = filters_parser(); + + assert!(p + .parse( + format!("filter(=({}, [1]), point{{[0]}})", "str_cmp(.field, \"\")").as_str() + ) + .is_ok()); + + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", "str_cmp(.field)").as_str()) + .is_err()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", "str_cmp(\"\")").as_str()) + .is_err()); + } + + #[test] + fn str_cmp_icase() { + let p = filters_parser(); + + assert!(p + .parse( + format!( + "filter(=({}, [1]), point{{[0]}})", + "str_cmp_ignore_case(.field, \"\")" + ) + .as_str() + ) + .is_ok()); + + assert!(p + .parse( + format!( + "filter(=({}, [1]), point{{[0]}})", + "str_cmp_ignore_case(.field)" + ) + .as_str() + ) + .is_err()); + assert!(p + .parse( + format!( + "filter(=({}, [1]), point{{[0]}})", + "str_cmp_ignore_case(\"\")" + ) + .as_str() + ) + .is_err()); + } + + #[test] + fn selector() { + let p = filters_parser(); + + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", ".").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field.field").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field[1].field").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(=({}, [1]), point{{[0]}})", ".field.field[1]").as_str()) + .is_ok()); + } + + #[test] + fn position() { + let p = filters_parser(); + + // Empty + assert!(p.parse(format!("point{{{}}}", "[]").as_str()).is_err()); + + // Non-numerical coordinate: + assert!(p.parse(format!("point{{{}}}", "[aa]").as_str()).is_err()); + + assert!(p + .parse(format!("point{{{}}}", "[\"aa\"]").as_str()) + .is_err()); + + // One or more coordinates + assert!(p.parse(format!("point{{{}}}", "[0]").as_str()).is_ok()); + assert!(p.parse(format!("point{{{}}}", "[0, 0]").as_str()).is_ok()); + assert!(p + .parse(format!("point{{{}}}", "[0, 0, 0]").as_str()) + .is_ok()); + assert!(p + .parse(format!("point{{{}}}", "[0, 0, 0, 0]").as_str()) + .is_ok()); + assert!(p + .parse(format!("point{{{}}}", "[0,0,0,0]").as_str()) + .is_ok()); + } + + #[test] + fn field() { + let p = filters_parser(); + + // Single dot + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".").as_str()) + .is_ok()); + + // Check first character is within allowed characters + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".a").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", "._").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".2").as_str()) + .is_err()); + + // Check second character is within allowed characters + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".fa").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f2").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f_").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f2").as_str()) + .is_ok()); + + // Check we can add subscript + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".[23]").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[0]").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[2]").as_str()) + .is_ok()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[23]").as_str()) + .is_ok()); + + // Invalid index values + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[2.3]").as_str()) + .is_err()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[02]").as_str()) + .is_err()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[-2]").as_str()) + .is_err()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[2e2]").as_str()) + .is_err()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[2E2]").as_str()) + .is_err()); + assert!(p + .parse(format!("filter(<({}, [1]), point{{[0]}})", ".f[+2]").as_str()) + .is_err()); + } + + #[test] + fn string() { + fn test_str_ok(p: &queries::FiltersParser, string: &str) { + let n = format!( + "{}{}{}", + "nifti{uri(\"http://a.nifti.file\"), ", string, " }" + ); + let n = n.as_str(); + + assert!(p.parse(n).is_ok()); + } + + fn test_str_err(p: &queries::FiltersParser, string: &str) { + let n = format!( + "{}{}{}", + "nifti{", string, ", uri(\"http://a.nifti.file\") }" + ); + let n = n.as_str(); + + assert!(p.parse(n).is_err()); + } + + let p = &filters_parser(); + + // Empty String + test_str_ok(p, r#""""#); + + // Usual escapes + test_str_ok(p, r#""\"""#); + test_str_ok(p, r#""\\""#); + test_str_ok(p, r#""\/""#); + test_str_ok(p, r#""\b""#); + test_str_ok(p, r#""\f""#); + test_str_ok(p, r#""\n""#); + test_str_ok(p, r#""\r""#); + test_str_ok(p, r#""\t""#); + + // Unicode Escape + test_str_ok(p, r#""\u0012""#); + test_str_ok(p, r#""\u001F""#); + test_str_ok(p, r#""\u001a""#); + + // ASCI Letters & digit + test_str_ok(p, r#""abcdefghijklmnopqrstuvwxyz""#); + test_str_ok(p, r#""ABCDEFGHIJKLMNOPQRSTUVWXYZ""#); + test_str_ok(p, r#""0123456789""#); + + // Space and some non-white characters + test_str_ok(p, r#"" ,.-;:!?'^&|§°+*ç%_""#); + + // Invalid + test_str_err(p, "\"\u{0010}\""); // rust requires \u{..}, while JSON does not. + } + + #[test] + fn positive_numbers() { + let p = filters_parser(); + + // Integers + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "0").as_str()) + .is_ok()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "+0").as_str()) + .is_ok()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "-0").as_str()) + .is_err()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "1").as_str()) + .is_ok()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "+1").as_str()) + .is_ok()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "-1").as_str()) + .is_err()); + + // Floating point values + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "0.0").as_str()) + .is_ok()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "+0.0").as_str()) + .is_ok()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "-0.0").as_str()) + .is_err()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "0.1").as_str()) + .is_ok()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "+0.01").as_str()) + .is_ok()); + assert!(p + .parse(format!("hypersphere{{[0],{}}}", "-0.01").as_str()) + .is_err()); + } + + #[test] + fn numbers() { + let p = filters_parser(); + + // Integers + assert!(p.parse(format!("point{{[{}]}}", "0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "+0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "-0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "+1").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "-1").as_str()).is_ok()); + + // Floating point values + assert!(p.parse(format!("point{{[{}]}}", "0.0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "+0.0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "-0.0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "0.1").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "+0.01").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "-0.01").as_str()).is_ok()); + } + + #[test] + fn num() { + let p = filters_parser(); + + // Integers + assert!(p.parse(format!("point{{[{}]}}", "0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1e2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1e+2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1e-2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1E2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "100").as_str()).is_ok()); + + assert!(p.parse(format!("point{{[{}]}}", "010").as_str()).is_err()); + + // Floating point values (normalized) + assert!(p.parse(format!("point{{[{}]}}", "0.0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "0.1").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "0.1e0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "0.1e2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "0.1e+2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "0.1e-2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "0.1E2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "0.1E23").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "0.01").as_str()).is_ok()); + + assert!(p.parse(format!("point{{[{}]}}", "0.").as_str()).is_err()); + assert!(p + .parse(format!("point{{[{}]}}", "0.1E03").as_str()) + .is_err()); + assert!(p + .parse(format!("point{{[{}]}}", "0.1E0.3").as_str()) + .is_err()); + + // Floating point values (denormalized) + assert!(p.parse(format!("point{{[{}]}}", "1.0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1.1").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1.1e0").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1.1e2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1.1e+2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1.1e-2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1.1E2").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1.1E23").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "1.01").as_str()).is_ok()); + assert!(p.parse(format!("point{{[{}]}}", "10.1").as_str()).is_ok()); + + assert!(p.parse(format!("point{{[{}]}}", "1.").as_str()).is_err()); + assert!(p.parse(format!("point{{[{}]}}", "01.1").as_str()).is_err()); + assert!(p + .parse(format!("point{{[{}]}}", "1.1E03").as_str()) + .is_err()); + assert!(p + .parse(format!("point{{[{}]}}", "1.1E0.3").as_str()) + .is_err()); + } } } diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..3abc766 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,59 @@ +#[derive(Clone, Debug)] +pub enum LiteralTypes { + String, + Int, + Float, + Bag(Vec), // List of types (heterogeneous) + Vector(Vec), // List of coordinates types (heterogeneous) + Array(usize, Box), // Length, homogeneous type +} + +impl PartialEq for LiteralTypes { + fn eq(&self, other: &Self) -> bool { + match self { + LiteralTypes::String => match other { + LiteralTypes::String => true, + _ => false, + }, + LiteralTypes::Int => match other { + LiteralTypes::Int => true, + _ => false, + }, + LiteralTypes::Float => match other { + LiteralTypes::Float => true, + LiteralTypes::Int => true, + _ => false, + }, + LiteralTypes::Bag(_) => match other { + LiteralTypes::Bag(_) => true, + _ => false, + }, + LiteralTypes::Vector(v) => match other { + LiteralTypes::Vector(ov) => { + let n = v.len(); + if n != ov.len() { + false + } else { + for i in 0..n { + if v[i] != ov[i] { + return false; + } + } + true + } + } + _ => false, + }, + LiteralTypes::Array(n, t) => match other { + LiteralTypes::Array(on, ot) => { + if on == n { + t == ot + } else { + false + } + } + _ => false, + }, + } + } +} diff --git a/src/validators.rs b/src/validators.rs new file mode 100644 index 0000000..f15ae0a --- /dev/null +++ b/src/validators.rs @@ -0,0 +1,171 @@ +use super::database::space; +use super::expression::Validator; +use super::symbols::*; + +pub type ValidationResult = Result; + +impl Validator for Projection { + type ValidationResult = self::ValidationResult; + + fn validate(&self) -> ValidationResult { + match self { + Projection::Nifti(_, _, _) => Err("not yet implemented".to_string()), + Projection::JSON(_, _format, bag) => bag.validate(), + //FIXME: Add support for projections + /* match format.validate() { + Ok(_) => bag.validate(), + Err(_) => Err(()), + }*/ + } + } +} + +impl Validator for Bag { + type ValidationResult = self::ValidationResult; + + fn validate(&self) -> ValidationResult { + fn compare_bag_types(lh: &Box, rh: &Box) -> ValidationResult { + if lh.space().cmp(rh.space()) != std::cmp::Ordering::Equal { + return Err(format!( + "left and right sets are defined in different reference spaces: '{}' vs '{}'.", + lh.space(), + rh.space() + )); + } + + let l = lh.validate(); + let r = rh.validate(); + + match &l { + Err(_) => l, + Ok(tl) => match r { + e @ Err(_) => e, + Ok(tr) => { + if tl != &tr { + Err(format!( + "Incoherent types between left and right sets: '{:?}' vs '{:?}'", + tl, &tr + )) + } else { + l + } + } + }, + } + } + + match self { + Bag::Distinct(bag) => bag.validate(), + Bag::Filter(_, bag) => bag.validate(), + Bag::Complement(bag) => bag.validate(), + Bag::Intersection(lh, rh) => compare_bag_types(lh, rh), + Bag::Union(lh, rh) => compare_bag_types(lh, rh), + Bag::Bag(bags) => { + for b in bags { + let t = b.validate(); + if t.is_err() { + return t; + } + } + + Ok(space::get_type(space::name()).clone()) + } + Bag::Inside(shape) => shape.validate(), + Bag::Outside(shape) => shape.validate(), + } + } +} + +impl Validator for Shape { + type ValidationResult = self::ValidationResult; + + fn validate(&self) -> ValidationResult { + match self { + Shape::Point(_, v) => v.validate(), + Shape::HyperRectangle(_space, pos) => { + let first = pos[0].get_type(); + match pos.len() { + 2 => { + if first != pos[1].get_type() { + Err(format!( + "HyperRectangle: Incompatible types in points definitions: '{:?}' vs '{:?}'", + first, + pos[1].get_type() + )) + } else { + Ok(first) + } + } + _ => { + //FIXME: Implement arbitrary hypercube definition support. For now reject. + Err("not yet implemented".to_string()) + /* + fn check_orthogonal(pos: &Vec) -> bool { + let k = pos.len(); + let mut raw_pos = vec![]; + + for s in pos { + match s { + LiteralTypes::Vector(n) => { + let mut v = vec![]; + for c in n { + v.push(match c { + LiteralNumber::Int(x) => x as f64, + LiteralNumber::Float(x) => x, + }); + } + raw_pos.push(v) + } + _ => pass, + } + } + + for p in raw_pos {} + true + } + match &first { + LiteralTypes::Vector(n) => { + if 2usize.pow(n.len() as u32) == pos.len() { + // Check we have coherent types for the coordinates. + for point in pos { + if first != point.get_type() { + return Err(()); + } + } + + // We need to check the points define a shape with orthogonal faces. + if check_orthogonal(pos) { + Ok(first) + } else { + Err(()) + } + } else { + Err(()) + } + } + _ => Err(()), + }*/ + } + } + } + Shape::HyperSphere(_, pos, _) => pos.validate(), + Shape::Nifti(_) => Err("not yet implemented".to_string()), + } + } +} + +impl Validator for LiteralPosition { + type ValidationResult = self::ValidationResult; + + fn validate(&self) -> ValidationResult { + Ok(self.get_type()) + } +} + +impl Validator for LiteralSelector { + type ValidationResult = self::ValidationResult; + + fn validate(&self) -> ValidationResult { + Ok(self.get_type()) + } +}