From cef5164938e5e26e8d4f282860a04fb60cc6542f Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 12 Dec 2024 15:21:19 -0600 Subject: [PATCH 1/8] start of work to match cql2 against json --- Cargo.lock | 13 +++++ Cargo.toml | 1 + src/expr.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index bc68e7a..bcb1937 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,6 +221,7 @@ dependencies = [ "geo-types", "geojson", "geozero", + "json_dotpath", "lazy_static", "pest", "pest_derive", @@ -657,6 +658,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "json_dotpath" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbdcfef3cf5591f0cef62da413ae795e3d1f5a00936ccec0b2071499a32efd1a" +dependencies = [ + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index d84ab7b..1d1455c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ boon = "0.6.0" geo-types = "0.7.13" geojson = "0.24.1" geozero = "0.14.0" +json_dotpath = "1.1.0" lazy_static = "1.5" pest = "2.7" pest_derive = { version = "2.7", features = ["grammar-extras"] } diff --git a/src/expr.rs b/src/expr.rs index 390ba9a..c994653 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -3,6 +3,7 @@ use pg_escape::{quote_identifier, quote_literal}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::str::FromStr; +use json_dotpath::DotPaths; /// A CQL2 expression. /// @@ -35,7 +36,157 @@ pub enum Expr { Geometry(Geometry), } +impl From for Expr{ + fn from(v: Value)-> Expr { + let e: Expr = serde_json::from_value(v).unwrap(); + e + } +} + +impl Into for Expr{ + fn into(self) -> Value { + let v: Value = serde_json::to_value(self).unwrap(); + v + } +} + +impl TryInto for Expr{ + type Error = (); + fn try_into(self) -> Result { + match self { + Expr::Float(v) => Ok(v), + Expr::Literal(v) => f64::from_str(&v).or(Err(())), + _ => Err(()) + } + } +} + impl Expr { + /// Insert values from properties from json + /// + /// # Examples + /// + /// ``` + /// use serde_json::{json, Value}; + /// use cql2::Expr; + /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z"}}); + /// let mut expr_json = json!( + /// { + /// "op": "+", + /// "args": [ + /// {"property": "eo:cloud_cover"}, + /// 10 + /// ] + /// } + /// ); + /// + /// let mut expr: Expr = serde_json::from_value(expr_json).unwrap(); + /// println!("Initial {:?}", expr); + /// expr.reduce(&item); + /// + /// let output: f64; + /// if let Expr::Float(v) = expr { + /// output = v; + /// } else { + /// assert!(false); + /// output = 0.0; + /// } + /// println!("Modified {:?}", expr); + /// + /// assert_eq!(20.0, output); + /// + /// ``` + pub fn reduce(&mut self, j:&Value) { + match self { + Expr::Property{property} => { + let mut prefixproperty: String = "properties.".to_string(); + prefixproperty.push_str(property); + + let propexpr: Option; + if j.dot_has(&property) { + propexpr = j.dot_get(&property).unwrap(); + } else { + let mut prefixproperty: String = "properties.".to_string(); + prefixproperty.push_str(property); + propexpr = j.dot_get(&prefixproperty).unwrap(); + } + if let Some(v) = propexpr { + *self = Expr::from(v); + } + + }, + Expr::Operation{op, args} => { + for arg in args.iter_mut() { + arg.reduce(j); + }; + + // binary operations + if args.len() == 2 { + // numerical binary operations + let left: Result = (*args[0].clone()).try_into(); + let right: Result = (*args[1].clone()).try_into(); + if let (Ok(l),Ok(r)) = (left, right) { + match op.as_str() { + "+" => {*self = Expr::Float(l + r);}, + "-" => {*self = Expr::Float(l - r);}, + "*" => {*self = Expr::Float(l * r);}, + "/" => {*self = Expr::Float(l / r);}, + "%" => {*self = Expr::Float(l % r);}, + "^" => {*self = Expr::Float(l.powf(r));}, + "=" => {*self = Expr::Bool(l == r);}, + "<=" => {*self = Expr::Bool(l <= r);}, + "<" => {*self = Expr::Bool(l < r);}, + ">=" => {*self = Expr::Bool(l >= r);}, + ">" => {*self = Expr::Bool(l > r);}, + "<>" => {*self = Expr::Bool(l != r);}, + _ => () + } + } + + } + }, + _ => () + } + } + /// Run CQL against a JSON Value + /// + /// # Examples + /// + /// ``` + /// use serde_json::{json, Value}; + /// use cql2::Expr; + /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z"}}); + /// let mut expr_json = json!( + /// { + /// "op" : ">", + /// "args" : [ + /// { + /// "op": "+", + /// "args": [ + /// {"property": "eo:cloud_cover"}, + /// 17 + /// ] + /// }, + /// 2 + /// ] + /// } + /// ); + /// + /// + /// let mut expr: Expr = serde_json::from_value(expr_json).unwrap(); + /// + /// + /// assert_eq!(true, expr.matches(&item).unwrap()); + /// + /// ``` + pub fn matches(&self, j: &Value) -> Result { + let mut e = self.clone(); + e.reduce(j); + match e { + Expr::Bool(v) => Ok(v), + _ => Err(()) + } + } /// Converts this expression to CQL2 text. /// /// # Examples From 8ac73fac6643cae2cfe208c46aba2d78e9c44250 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 12 Dec 2024 17:18:22 -0600 Subject: [PATCH 2/8] add boolean ops, add reduce option to cli --- Cargo.lock | 133 ++++++++++++++++++++++++++++++++------------- Cargo.toml | 1 + cli/src/lib.rs | 10 +++- src/expr.rs | 144 +++++++++++++++++++++++++++++++++++-------------- 4 files changed, 211 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcb1937..d4fb1f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,10 +185,10 @@ version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -218,6 +218,7 @@ version = "0.3.2" dependencies = [ "assert-json-diff", "boon", + "derive_is_enum_variant", "geo-types", "geojson", "geozero", @@ -264,6 +265,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "derive_is_enum_variant" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ac8859845146979953797f03cc5b282fb4396891807cdb3d04929a88418197" +dependencies = [ + "heck 0.3.3", + "quote 0.3.15", + "syn 0.11.11", +] + [[package]] name = "digest" version = "0.10.7" @@ -281,8 +293,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -364,8 +376,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -475,6 +487,15 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.5.0" @@ -595,8 +616,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -767,8 +788,8 @@ dependencies = [ "pest", "pest_meta", "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -820,8 +841,8 @@ dependencies = [ "phf_generator", "phf_shared", "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -915,8 +936,8 @@ checksum = "fdb6da8ec6fa5cedd1626c886fc8749bdcbb09424a86461eb8cdf096b7c33257" dependencies = [ "proc-macro2", "pyo3-macros-backend", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -925,11 +946,11 @@ version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a385202ff5a92791168b1136afae5059d3ac118457bb7bc304c197c2d33e7d" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "pyo3-build-config", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -942,6 +963,12 @@ dependencies = [ "serde", ] +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + [[package]] name = "quote" version = "1.0.37" @@ -1023,11 +1050,11 @@ dependencies = [ "glob", "proc-macro-crate", "proc-macro2", - "quote", + "quote 1.0.37", "regex", "relative-path", "rustc_version", - "syn", + "syn 2.0.90", "unicode-ident", ] @@ -1068,8 +1095,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -1129,6 +1156,17 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote 0.3.15", + "synom", + "unicode-xid", +] + [[package]] name = "syn" version = "2.0.90" @@ -1136,10 +1174,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.37", "unicode-ident", ] +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -1147,8 +1194,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -1182,8 +1229,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -1193,8 +1240,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -1272,6 +1319,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + [[package]] name = "unindent" version = "0.2.3" @@ -1444,8 +1503,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", "synstructure", ] @@ -1465,8 +1524,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] [[package]] @@ -1485,8 +1544,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", "synstructure", ] @@ -1508,6 +1567,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", - "quote", - "syn", + "quote 1.0.37", + "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index 1d1455c..9af08b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ keywords = ["cql2"] [dependencies] boon = "0.6.0" +derive_is_enum_variant = "0.1.1" geo-types = "0.7.13" geojson = "0.24.1" geozero = "0.14.0" diff --git a/cli/src/lib.rs b/cli/src/lib.rs index b3bb6b0..0612d44 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use clap::{ArgAction, Parser, ValueEnum}; use cql2::{Expr, Validator}; +use serde_json::json; use std::io::Read; /// The CQL2 command-line interface. @@ -30,6 +31,10 @@ pub struct Cli { #[arg(long, default_value_t = true, action = ArgAction::Set)] validate: bool, + /// Reduce the CQL2 + #[arg(long, default_value_t = false, action = ArgAction::Set)] + reduce: bool, + /// Verbosity. /// /// Provide this argument several times to turn up the chatter. @@ -95,7 +100,7 @@ impl Cli { InputFormat::Text } }); - let expr: Expr = match input_format { + let mut expr: Expr = match input_format { InputFormat::Json => cql2::parse_json(&input)?, InputFormat::Text => match cql2::parse_text(&input) { Ok(expr) => expr, @@ -104,6 +109,9 @@ impl Cli { } }, }; + if self.reduce { + expr.reduce(&json!({})); + } if self.validate { let validator = Validator::new().unwrap(); let value = serde_json::to_value(&expr).unwrap(); diff --git a/src/expr.rs b/src/expr.rs index c994653..9f9c59f 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -1,9 +1,10 @@ use crate::{Error, Geometry, SqlQuery, Validator}; +use derive_is_enum_variant::is_enum_variant; +use json_dotpath::DotPaths; use pg_escape::{quote_identifier, quote_literal}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::str::FromStr; -use json_dotpath::DotPaths; /// A CQL2 expression. /// @@ -19,7 +20,7 @@ use json_dotpath::DotPaths; /// /// Use [Expr::to_text], [Expr::to_json], and [Expr::to_sql] to use the CQL2, /// and use [Expr::is_valid] to check validity. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, is_enum_variant)] #[serde(untagged)] #[allow(missing_docs)] pub enum Expr { @@ -36,27 +37,38 @@ pub enum Expr { Geometry(Geometry), } -impl From for Expr{ - fn from(v: Value)-> Expr { +impl From for Expr { + fn from(v: Value) -> Expr { let e: Expr = serde_json::from_value(v).unwrap(); e } } -impl Into for Expr{ - fn into(self) -> Value { - let v: Value = serde_json::to_value(self).unwrap(); +impl From for Value { + fn from(v: Expr) -> Value { + let v: Value = serde_json::to_value(v).unwrap(); v } } -impl TryInto for Expr{ + +impl TryInto for Expr { type Error = (); fn try_into(self) -> Result { match self { Expr::Float(v) => Ok(v), Expr::Literal(v) => f64::from_str(&v).or(Err(())), - _ => Err(()) + _ => Err(()), + } + } +} + +impl TryInto for Expr { + type Error = (); + fn try_into(self) -> Result { + match self { + Expr::Bool(v) => Ok(v), + _ => Err(()), } } } @@ -69,7 +81,7 @@ impl Expr { /// ``` /// use serde_json::{json, Value}; /// use cql2::Expr; - /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z"}}); + /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z", "boolfield": true}}); /// let mut expr_json = json!( /// { /// "op": "+", @@ -95,16 +107,17 @@ impl Expr { /// /// assert_eq!(20.0, output); /// + /// /// ``` - pub fn reduce(&mut self, j:&Value) { + pub fn reduce(&mut self, j: &Value) { match self { - Expr::Property{property} => { + Expr::Property { property } => { let mut prefixproperty: String = "properties.".to_string(); prefixproperty.push_str(property); - let propexpr: Option; - if j.dot_has(&property) { - propexpr = j.dot_get(&property).unwrap(); + let mut propexpr: Option = None; + if j.dot_has(property) { + propexpr = j.dot_get(property).unwrap(); } else { let mut prefixproperty: String = "properties.".to_string(); prefixproperty.push_str(property); @@ -113,39 +126,89 @@ impl Expr { if let Some(v) = propexpr { *self = Expr::from(v); } - - }, - Expr::Operation{op, args} => { + } + Expr::Operation { op, args } => { + let mut alltrue: bool = true; + let mut anytrue: bool = false; + let mut allbool: bool = true; for arg in args.iter_mut() { arg.reduce(j); - }; + let b: Result = arg.as_ref().clone().try_into(); + match b { + Ok(true) => anytrue = true, + Ok(false) => { + alltrue = false; + } + _ => { + alltrue = false; + allbool = false; + } + } + } + + // boolean operators + if allbool { + match op.as_str() { + "and" => { + *self = Expr::Bool(alltrue); + } + "or" => { + *self = Expr::Bool(anytrue); + } + _ => (), + } + return; + } // binary operations if args.len() == 2 { // numerical binary operations let left: Result = (*args[0].clone()).try_into(); let right: Result = (*args[1].clone()).try_into(); - if let (Ok(l),Ok(r)) = (left, right) { + if let (Ok(l), Ok(r)) = (left, right) { match op.as_str() { - "+" => {*self = Expr::Float(l + r);}, - "-" => {*self = Expr::Float(l - r);}, - "*" => {*self = Expr::Float(l * r);}, - "/" => {*self = Expr::Float(l / r);}, - "%" => {*self = Expr::Float(l % r);}, - "^" => {*self = Expr::Float(l.powf(r));}, - "=" => {*self = Expr::Bool(l == r);}, - "<=" => {*self = Expr::Bool(l <= r);}, - "<" => {*self = Expr::Bool(l < r);}, - ">=" => {*self = Expr::Bool(l >= r);}, - ">" => {*self = Expr::Bool(l > r);}, - "<>" => {*self = Expr::Bool(l != r);}, - _ => () + "+" => { + *self = Expr::Float(l + r); + } + "-" => { + *self = Expr::Float(l - r); + } + "*" => { + *self = Expr::Float(l * r); + } + "/" => { + *self = Expr::Float(l / r); + } + "%" => { + *self = Expr::Float(l % r); + } + "^" => { + *self = Expr::Float(l.powf(r)); + } + "=" => { + *self = Expr::Bool(l == r); + } + "<=" => { + *self = Expr::Bool(l <= r); + } + "<" => { + *self = Expr::Bool(l < r); + } + ">=" => { + *self = Expr::Bool(l >= r); + } + ">" => { + *self = Expr::Bool(l > r); + } + "<>" => { + *self = Expr::Bool(l != r); + } + _ => (), } } - } - }, - _ => () + } + _ => (), } } /// Run CQL against a JSON Value @@ -155,7 +218,7 @@ impl Expr { /// ``` /// use serde_json::{json, Value}; /// use cql2::Expr; - /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z"}}); + /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z", "boolfield": true}}); /// let mut expr_json = json!( /// { /// "op" : ">", @@ -178,13 +241,16 @@ impl Expr { /// /// assert_eq!(true, expr.matches(&item).unwrap()); /// + /// + /// let mut expr2: Expr = "boolfield and 1 + 2 = 3".parse().unwrap(); + /// assert_eq!(true, expr2.matches(&item).unwrap()); /// ``` - pub fn matches(&self, j: &Value) -> Result { + pub fn matches(&self, j: &Value) -> Result { let mut e = self.clone(); e.reduce(j); match e { Expr::Bool(v) => Ok(v), - _ => Err(()) + _ => Err(()), } } /// Converts this expression to CQL2 text. From 93d7940d4592df3fefcfdc1488d4de4000a72fc5 Mon Sep 17 00:00:00 2001 From: David Bitner Date: Mon, 16 Dec 2024 09:19:13 -0600 Subject: [PATCH 3/8] Update src/expr.rs Co-authored-by: Pete Gadomski --- src/expr.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/expr.rs b/src/expr.rs index 9f9c59f..bb36373 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -112,17 +112,7 @@ impl Expr { pub fn reduce(&mut self, j: &Value) { match self { Expr::Property { property } => { - let mut prefixproperty: String = "properties.".to_string(); - prefixproperty.push_str(property); - - let mut propexpr: Option = None; - if j.dot_has(property) { - propexpr = j.dot_get(property).unwrap(); - } else { - let mut prefixproperty: String = "properties.".to_string(); - prefixproperty.push_str(property); - propexpr = j.dot_get(&prefixproperty).unwrap(); - } + let propexpr = j.dot_get(property).or_else(|_| j.dot_get(&format!("properties.{}", property)))?; if let Some(v) = propexpr { *self = Expr::from(v); } From fa337c43d909576d3ac072729574a44a701aa142 Mon Sep 17 00:00:00 2001 From: David Bitner Date: Mon, 16 Dec 2024 09:21:34 -0600 Subject: [PATCH 4/8] Update src/expr.rs Co-authored-by: Pete Gadomski --- src/expr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/expr.rs b/src/expr.rs index bb36373..6d6d734 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -74,7 +74,7 @@ impl TryInto for Expr { } impl Expr { - /// Insert values from properties from json + /// Update this expression with values from the `properties` attribute of a JSON object /// /// # Examples /// From bf6c285fc0e4671e4b45c5c2aa54dcd7848e764d Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 17 Dec 2024 12:39:14 -0600 Subject: [PATCH 5/8] add support for temporal and geospatial operators, add reduce tests --- tests/reduce_tests.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/reductions.txt | 24 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/reduce_tests.rs create mode 100644 tests/reductions.txt diff --git a/tests/reduce_tests.rs b/tests/reduce_tests.rs new file mode 100644 index 0000000..215f190 --- /dev/null +++ b/tests/reduce_tests.rs @@ -0,0 +1,42 @@ +use cql2::Expr; +use rstest::rstest; +use std::path::Path; +use serde_json::{Value, json}; + +fn read_lines(filename: impl AsRef) -> Vec { + std::fs::read_to_string(filename) + .unwrap() // panic on possible file-reading errors + .lines() // split the string into an iterator of string slices + .map(String::from) // make each slice into a string + .collect() // gather them together into a vector +} +fn validate_reduction(a: String, b: String){ + let properties: Value = json!( + { + "properties": { + "eo:cloud_cover": 10, + "boolfalse": false, + "booltrue": true, + "stringfield": "string", + "tsfield": {"timestamp": "2020-01-01 00:00:00Z"} + }, + "geometry": {"type": "Point", "coordinates": [-93.0, 45]}, + "datetime": "2020-01-01 00:00:00Z" + } + ); + let mut inexpr: Expr = a.parse().unwrap(); + inexpr.reduce(Some(&properties)); + let outexpr: Expr = b.parse().unwrap(); + assert_eq!(inexpr, outexpr); +} + +#[rstest] +fn validate_reduce_fixtures() { + let lines = read_lines("tests/reductions.txt"); + let a = lines.clone().into_iter().step_by(2); + let b = lines.clone().into_iter().skip(1).step_by(2); + let zipped = a.zip(b); + for (a,b) in zipped{ + validate_reduction(a, b); + } +} diff --git a/tests/reductions.txt b/tests/reductions.txt new file mode 100644 index 0000000..f48da18 --- /dev/null +++ b/tests/reductions.txt @@ -0,0 +1,24 @@ +1 + 1 +2 +1 + 2 = 4 +false +1 + 3 = 7 +false +true and false or true +true +"eo:cloud_cover" = 10 +true +eo:cloud_cover + 10 = 20 +true +booltrue +true +boolfalse +false +booltrue or boolfalse +true +2 > eo:cloud_cover +false +2 > eo:cloud_cover - 10 +true +S_EQUALS(POINT(-93.0 45.0), geometry) +true From f9a6c2267a45d4ecbb40df921512f020d41908b3 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 17 Dec 2024 12:39:43 -0600 Subject: [PATCH 6/8] mend --- Cargo.lock | 272 ++++++++++++++++++++++++++++---------------- Cargo.toml | 4 +- cli/src/lib.rs | 2 +- src/error.rs | 24 ++++ src/expr.rs | 296 +++++++++++++++++++++++++++--------------------- src/geometry.rs | 16 +++ src/lib.rs | 4 +- src/temporal.rs | 65 +++++++++++ 8 files changed, 458 insertions(+), 225 deletions(-) create mode 100644 src/temporal.rs diff --git a/Cargo.lock b/Cargo.lock index d4fb1f0..e1b663e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,12 @@ dependencies = [ "url", ] +[[package]] +name = "c_vec" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd7a427adc0135366d99db65b36dae9237130997e560ed61118041fb72be6e8" + [[package]] name = "cfg-if" version = "1.0.0" @@ -185,10 +191,10 @@ version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -218,10 +224,12 @@ version = "0.3.2" dependencies = [ "assert-json-diff", "boon", - "derive_is_enum_variant", + "enum-as-inner", "geo-types", "geojson", + "geos", "geozero", + "jiff", "json_dotpath", "lazy_static", "pest", @@ -265,17 +273,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "derive_is_enum_variant" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ac8859845146979953797f03cc5b282fb4396891807cdb3d04929a88418197" -dependencies = [ - "heck 0.3.3", - "quote 0.3.15", - "syn 0.11.11", -] - [[package]] name = "digest" version = "0.10.7" @@ -293,8 +290,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -376,8 +385,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -450,6 +459,29 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "geos" +version = "9.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d199db00644057267a8a68ee72df92aa59a32036b487b2a2b76fd0b3fca32b" +dependencies = [ + "c_vec", + "geos-sys", + "libc", + "num", +] + +[[package]] +name = "geos-sys" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc873d24aefc72aa94c3c1c251afb82beb7be5926002746c0e1f585fef9854c" +dependencies = [ + "libc", + "pkg-config", + "semver", +] + [[package]] name = "geozero" version = "0.14.0" @@ -487,15 +519,6 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.5.0" @@ -616,8 +639,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -679,6 +702,31 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jiff" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db69f08d4fb10524cacdb074c10b296299d71274ddbc830a8ee65666867002e9" +dependencies = [ + "jiff-tzdb-platform", + "windows-sys", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "json_dotpath" version = "1.1.0" @@ -736,6 +784,70 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -788,8 +900,8 @@ dependencies = [ "pest", "pest_meta", "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -841,8 +953,8 @@ dependencies = [ "phf_generator", "phf_shared", "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -866,6 +978,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "portable-atomic" version = "1.10.0" @@ -936,8 +1054,8 @@ checksum = "fdb6da8ec6fa5cedd1626c886fc8749bdcbb09424a86461eb8cdf096b7c33257" dependencies = [ "proc-macro2", "pyo3-macros-backend", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -946,11 +1064,11 @@ version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a385202ff5a92791168b1136afae5059d3ac118457bb7bc304c197c2d33e7d" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "pyo3-build-config", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -963,12 +1081,6 @@ dependencies = [ "serde", ] -[[package]] -name = "quote" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" - [[package]] name = "quote" version = "1.0.37" @@ -1050,11 +1162,11 @@ dependencies = [ "glob", "proc-macro-crate", "proc-macro2", - "quote 1.0.37", + "quote", "regex", "relative-path", "rustc_version", - "syn 2.0.90", + "syn", "unicode-ident", ] @@ -1095,8 +1207,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -1156,17 +1268,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "syn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" -dependencies = [ - "quote 0.3.15", - "synom", - "unicode-xid", -] - [[package]] name = "syn" version = "2.0.90" @@ -1174,19 +1275,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", - "quote 1.0.37", + "quote", "unicode-ident", ] -[[package]] -name = "synom" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" -dependencies = [ - "unicode-xid", -] - [[package]] name = "synstructure" version = "0.13.1" @@ -1194,8 +1286,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -1229,8 +1321,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -1240,8 +1332,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -1319,18 +1411,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-xid" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" - [[package]] name = "unindent" version = "0.2.3" @@ -1503,8 +1583,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", "synstructure", ] @@ -1524,8 +1604,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] [[package]] @@ -1544,8 +1624,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", "synstructure", ] @@ -1567,6 +1647,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", - "quote 1.0.37", - "syn 2.0.90", + "quote", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 9af08b8..d7e9d4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,12 @@ keywords = ["cql2"] [dependencies] boon = "0.6.0" -derive_is_enum_variant = "0.1.1" +enum-as-inner = "0.6.1" geo-types = "0.7.13" geojson = "0.24.1" +geos = "9.1.1" geozero = "0.14.0" +jiff = "0.1.15" json_dotpath = "1.1.0" lazy_static = "1.5" pest = "2.7" diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 0612d44..e9ad861 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -110,7 +110,7 @@ impl Cli { }, }; if self.reduce { - expr.reduce(&json!({})); + expr.reduce(Some(&json!({}))); } if self.validate { let validator = Validator::new().unwrap(); diff --git a/src/error.rs b/src/error.rs index b904c43..485f11c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,10 @@ pub enum Error { #[error(transparent)] Geozero(#[from] geozero::error::GeozeroError), + /// [geos::Error] + #[error(transparent)] + Geos(#[from] geos::Error), + /// Invalid CQL2 text #[error("invalid cql2-text: {0}")] InvalidCql2Text(String), @@ -65,4 +69,24 @@ pub enum Error { /// validator's data. #[error("validation error")] Validation(serde_json::Value), + + /// Error Converting Expr to f64 + #[error("Could not convert Expression to f64")] + ExprToF64(), + + /// Error Converting Expr to bool + #[error("Could not convert Expression to bool")] + ExprToBool(), + + /// Error Converting Expr to geos geometry + #[error("Could not convert Expression to Geos Geometry")] + ExprToGeom(), + + /// Error Converting Expr to DateRange + #[error("Could not convert Expression to DateRange")] + ExprToDateRange(), + + /// Operator not implemented. + #[error("Operator {0} is not implemented for this type.")] + OpNotImplemented(&'static str), } diff --git a/src/expr.rs b/src/expr.rs index 6d6d734..b088806 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -1,5 +1,6 @@ -use crate::{Error, Geometry, SqlQuery, Validator}; -use derive_is_enum_variant::is_enum_variant; +use crate::{DateRange, Error, Geometry, SqlQuery, Validator, geometry::spatial_op, temporal::temporal_op}; +use enum_as_inner::EnumAsInner; +use geos::Geometry as GGeom; use json_dotpath::DotPaths; use pg_escape::{quote_identifier, quote_literal}; use serde::{Deserialize, Serialize}; @@ -20,7 +21,7 @@ use std::str::FromStr; /// /// Use [Expr::to_text], [Expr::to_json], and [Expr::to_sql] to use the CQL2, /// and use [Expr::is_valid] to check validity. -#[derive(Debug, Serialize, Deserialize, Clone, is_enum_variant)] +#[derive(Debug, Serialize, Deserialize, Clone, EnumAsInner)] #[serde(untagged)] #[allow(missing_docs)] pub enum Expr { @@ -37,42 +38,95 @@ pub enum Expr { Geometry(Geometry), } -impl From for Expr { - fn from(v: Value) -> Expr { - let e: Expr = serde_json::from_value(v).unwrap(); - e +impl TryFrom for Expr { + type Error = Error; + fn try_from(v: Value) -> Result { + serde_json::from_value(v).map_err(Error::from) } } -impl From for Value { - fn from(v: Expr) -> Value { - let v: Value = serde_json::to_value(v).unwrap(); - v +impl TryFrom for Value { + type Error = Error; + fn try_from(v: Expr) -> Result { + serde_json::to_value(v).map_err(Error::from) } } - -impl TryInto for Expr { - type Error = (); - fn try_into(self) -> Result { - match self { +impl TryFrom for f64 { + type Error = Error; + fn try_from(v: Expr) -> Result { + match v { Expr::Float(v) => Ok(v), - Expr::Literal(v) => f64::from_str(&v).or(Err(())), - _ => Err(()), + Expr::Literal(v) => f64::from_str(&v).map_err(Error::from), + _ => Err(Error::ExprToF64()), } } } -impl TryInto for Expr { - type Error = (); - fn try_into(self) -> Result { - match self { +impl TryFrom for bool { + type Error = Error; + fn try_from(v: Expr) -> Result { + match v { Expr::Bool(v) => Ok(v), - _ => Err(()), + Expr::Literal(v) => bool::from_str(&v).map_err(Error::from), + _ => Err(Error::ExprToBool()), } } } +impl TryFrom for String { + type Error = Error; + fn try_from(v: Expr) -> Result { + match v { + Expr::Literal(v) => Ok(v), + Expr::Bool(v) => Ok(v.to_string()), + Expr::Float(v) => Ok(v.to_string()), + _ => Err(Error::ExprToBool()), + } + } +} + +impl TryFrom for GGeom { + type Error = Error; + fn try_from(v: Expr) -> Result { + match v { + Expr::Geometry(v) => Ok(GGeom::new_from_wkt(&v.to_wkt().unwrap()) + .expect("Failed to convert WKT to Geos Geometry")), + _ => Err(Error::ExprToGeom()), + } + } +} + +impl PartialEq for Expr { + fn eq(&self, other: &Self) -> bool { + self.to_text().unwrap() == other.to_text().unwrap() + } +} + +fn binary_bool(left: &T, right: &T, op: &str) -> Result { + match op { + "=" => Ok(left == right), + "<=" => Ok(left <= right), + "<" => Ok(left < right), + ">=" => Ok(left >= right), + ">" => Ok(left > right), + "<>" => Ok(left != right), + _ => Err(Error::OpNotImplemented("Binary Bool")), + } +} + +fn arith(left: &f64, right: &f64, op: &str) -> Result { + match op { + "+" => Ok(left + right), + "-" => Ok(left - right), + "*" => Ok(left * right), + "/" => Ok(left / right), + "%" => Ok(left % right), + "^" => Ok(left.powf(*right)), + _ => Err(Error::OpNotImplemented("Arith")) + } +} + impl Expr { /// Update this expression with values from the `properties` attribute of a JSON object /// @@ -81,40 +135,48 @@ impl Expr { /// ``` /// use serde_json::{json, Value}; /// use cql2::Expr; - /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z", "boolfield": true}}); - /// let mut expr_json = json!( - /// { - /// "op": "+", - /// "args": [ - /// {"property": "eo:cloud_cover"}, - /// 10 - /// ] - /// } - /// ); + /// use std::str::FromStr; /// - /// let mut expr: Expr = serde_json::from_value(expr_json).unwrap(); - /// println!("Initial {:?}", expr); - /// expr.reduce(&item); - /// - /// let output: f64; - /// if let Expr::Float(v) = expr { - /// output = v; - /// } else { - /// assert!(false); - /// output = 0.0; - /// } - /// println!("Modified {:?}", expr); + /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z", "boolfield": true}}); /// - /// assert_eq!(20.0, output); + /// let mut fromexpr: Expr = Expr::from_str("boolfield = true").unwrap(); + /// fromexpr.reduce(Some(&item)); + /// let mut toexpr: Expr = Expr::from_str("true").unwrap(); + /// assert_eq!(fromexpr, toexpr); /// + /// let mut fromexpr: Expr = Expr::from_str("\"eo:cloud_cover\" + 10").unwrap(); + /// fromexpr.reduce(Some(&item)); + /// let mut toexpr: Expr = Expr::from_str("20").unwrap(); + /// assert_eq!(fromexpr, toexpr); /// /// ``` - pub fn reduce(&mut self, j: &Value) { + pub fn reduce(&mut self, j: Option<&Value>) { match self { + Expr::Interval { interval } => { + for arg in interval.iter_mut() { + arg.reduce(j); + } + } + Expr::Timestamp { timestamp } => { + timestamp.reduce(j); + } + Expr::Date { date } => { + date.reduce(j); + } Expr::Property { property } => { - let propexpr = j.dot_get(property).or_else(|_| j.dot_get(&format!("properties.{}", property)))?; - if let Some(v) = propexpr { - *self = Expr::from(v); + if let Some(j) = j { + let propexpr: Option; + if j.dot_has(property) { + propexpr = j.dot_get(property).unwrap(); + } else { + propexpr = j.dot_get(&format!("properties.{}", property)).unwrap(); + } + + println!("j:{:?} property:{:?}", j, property); + println!("propexpr: {:?}", propexpr); + if let Some(v) = propexpr { + *self = Expr::try_from(v).unwrap(); + } } } Expr::Operation { op, args } => { @@ -123,16 +185,16 @@ impl Expr { let mut allbool: bool = true; for arg in args.iter_mut() { arg.reduce(j); - let b: Result = arg.as_ref().clone().try_into(); - match b { - Ok(true) => anytrue = true, - Ok(false) => { - alltrue = false; - } - _ => { + + if let Ok(bool) = arg.as_ref().clone().try_into() { + if bool { + anytrue = true; + } else { alltrue = false; - allbool = false; } + } else { + alltrue = false; + allbool = false; } } @@ -141,59 +203,63 @@ impl Expr { match op.as_str() { "and" => { *self = Expr::Bool(alltrue); + return } "or" => { *self = Expr::Bool(anytrue); + return } _ => (), } - return; } // binary operations if args.len() == 2 { - // numerical binary operations - let left: Result = (*args[0].clone()).try_into(); - let right: Result = (*args[1].clone()).try_into(); - if let (Ok(l), Ok(r)) = (left, right) { - match op.as_str() { - "+" => { - *self = Expr::Float(l + r); - } - "-" => { - *self = Expr::Float(l - r); - } - "*" => { - *self = Expr::Float(l * r); - } - "/" => { - *self = Expr::Float(l / r); - } - "%" => { - *self = Expr::Float(l % r); - } - "^" => { - *self = Expr::Float(l.powf(r)); - } - "=" => { - *self = Expr::Bool(l == r); - } - "<=" => { - *self = Expr::Bool(l <= r); - } - "<" => { - *self = Expr::Bool(l < r); - } - ">=" => { - *self = Expr::Bool(l >= r); - } - ">" => { - *self = Expr::Bool(l > r); - } - "<>" => { - *self = Expr::Bool(l != r); - } - _ => (), + let left: &Expr = args[0].as_ref(); + let right: &Expr = args[1].as_ref(); + + if let (Ok(l), Ok(r)) = + (f64::try_from(left.clone()), f64::try_from(right.clone())) + { + if let Ok(v) = arith(&l, &r, op) { + *self = Expr::Float(v); + return; + } + if let Ok(v) = binary_bool(&l, &r, op) { + *self = Expr::Bool(v); + return; + } + } else if let (Ok(l), Ok(r)) = + (bool::try_from(left.clone()), bool::try_from(right.clone())) + { + if let Ok(v) = binary_bool(&l, &r, op) { + *self = Expr::Bool(v); + return; + } + } else if let (Ok(l), Ok(r)) = ( + GGeom::try_from(left.clone()), + GGeom::try_from(right.clone()), + ) { + println!("Is Spatial Op. {:?} ({:?}, {:?})", op, left, right); + if let Ok(v) = spatial_op(&l, &r, op) { + *self = Expr::Bool(v); + return; + } + } else if let (Ok(l), Ok(r)) = ( + DateRange::try_from(left.clone()), + DateRange::try_from(right.clone()), + ) { + if let Ok(v) = temporal_op(&l, &r, op) { + *self = Expr::Bool(v); + return; + } + } else if let (Ok(l), Ok(r)) = ( + String::try_from(left.clone()), + String::try_from(right.clone()), + ) { + if let Ok(v) = binary_bool(&l, &r, op) { + *self = Expr::Bool(v); + return; } } } @@ -209,33 +275,11 @@ impl Expr { /// use serde_json::{json, Value}; /// use cql2::Expr; /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z", "boolfield": true}}); - /// let mut expr_json = json!( - /// { - /// "op" : ">", - /// "args" : [ - /// { - /// "op": "+", - /// "args": [ - /// {"property": "eo:cloud_cover"}, - /// 17 - /// ] - /// }, - /// 2 - /// ] - /// } - /// ); - /// - /// - /// let mut expr: Expr = serde_json::from_value(expr_json).unwrap(); - /// - /// - /// assert_eq!(true, expr.matches(&item).unwrap()); - /// /// - /// let mut expr2: Expr = "boolfield and 1 + 2 = 3".parse().unwrap(); - /// assert_eq!(true, expr2.matches(&item).unwrap()); + /// let mut expr: Expr = "boolfield and 1 + 2 = 3".parse().unwrap(); + /// assert_eq!(true, expr.matches(Some(&item)).unwrap()); /// ``` - pub fn matches(&self, j: &Value) -> Result { + pub fn matches(&self, j: Option<&Value>) -> Result { let mut e = self.clone(); e.reduce(j); match e { diff --git a/src/geometry.rs b/src/geometry.rs index 03ccc3d..e9414fe 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,6 +1,7 @@ use crate::Error; use geozero::{wkt::Wkt, CoordDimensions, ToGeo, ToWkt}; use serde::{Deserialize, Serialize, Serializer}; +use geos::{Geometry as GGeom, Geom}; const DEFAULT_NDIM: usize = 2; @@ -80,3 +81,18 @@ fn geojson_ndims(geojson: &geojson::Geometry) -> usize { GeometryCollection(v) => v.first().map(geojson_ndims).unwrap_or(DEFAULT_NDIM), } } + +/// Run a spatial operation. +pub fn spatial_op(left: &GGeom, right: &GGeom, op: &str) -> Result { + match op { + "s_equals" => Ok(left == right), + "s_intersects" | "intersects" => left.intersects(right).map_err(Error::from), + "s_disjoint" => left.disjoint(right).map_err(Error::from), + "s_touches" => left.touches(right).map_err(Error::from), + "s_within" => left.within(right).map_err(Error::from), + "s_overlaps" => left.overlaps(right).map_err(Error::from), + "s_crosses" => left.crosses(right).map_err(Error::from), + "s_contains" => left.contains(right).map_err(Error::from), + _ => Err(Error::OpNotImplemented("Spatial")), + } +} diff --git a/src/lib.rs b/src/lib.rs index d630014..e15f1a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,14 +34,16 @@ mod error; mod expr; mod geometry; mod parser; +mod temporal; mod validator; pub use error::Error; pub use expr::Expr; -pub use geometry::Geometry; +pub use geometry::{Geometry, spatial_op}; pub use parser::parse_text; use serde_derive::{Deserialize, Serialize}; use std::{fs, path::Path}; +pub use temporal::{DateRange, temporal_op}; pub use validator::Validator; /// A SQL query, broken into the query and parameters. diff --git a/src/temporal.rs b/src/temporal.rs new file mode 100644 index 0000000..88e5678 --- /dev/null +++ b/src/temporal.rs @@ -0,0 +1,65 @@ +use crate::{Error, Expr}; +use jiff::{Timestamp, ToSpan}; + +/// Struct to hold a range of timestamps. +#[derive(Debug,Clone,PartialEq)] +pub struct DateRange { + start: Timestamp, + end: Timestamp, +} + +impl TryFrom for DateRange { + type Error = Error; + fn try_from(v: Expr) -> Result { + + match v { + Expr::Interval{interval} => { + let start_str: String = interval[0].to_text()?; + let end_str: String = interval[1].to_text()?; + let start: Timestamp = start_str.parse().unwrap(); + let end: Timestamp = end_str.parse().unwrap(); + Ok(DateRange{start, end}) + } + Expr::Timestamp{timestamp} => { + let start_str: String = timestamp.to_text()?; + let start: Timestamp = start_str.parse().unwrap(); + Ok(DateRange{start, end: start}) + } + Expr::Date{date} => { + let start_str: String = date.to_text()?; + let start: Timestamp = start_str.parse().unwrap(); + let end: Timestamp = start + 1.day() - 1.nanosecond(); + Ok(DateRange{start, end}) + } + Expr::Literal(v) => { + let start: Timestamp = v.parse().unwrap(); + Ok(DateRange{start, end: start}) + } + _ => Err(Error::ExprToDateRange()), + } + } +} + +/// Run a temporal operation. +pub fn temporal_op(left: &DateRange, right: &DateRange, op: &str) -> Result { + match op { + "t_before" => Ok(left.end < right.start), + "t_after" => temporal_op(right, left, "t_before"), + "t_meets" => Ok(left.end == right.start), + "t_metby" => temporal_op(right, left, "t_meets"), + "t_overlaps" => { + Ok(left.start < right.end && right.start < left.end && left.end < right.end) + } + "t_overlappedby" => temporal_op(right, left, "t_overlaps"), + "t_starts" => Ok(left.start == right.start && left.end < right.end), + "t_startedby" => temporal_op(right, left, "t_starts"), + "t_during" => Ok(left.start > right.start && left.end < right.end), + "t_contains" => temporal_op(right, left, "t_during"), + "t_finishes" => Ok(left.start > right.start && left.end == right.end), + "t_finishedby" => temporal_op(right, left, "t_finishes"), + "t_equals" => Ok(left.start == right.start && left.end == right.end), + "t_disjoint" => Ok(!(temporal_op(left, right, "t_intersects").unwrap())), + "t_intersects" | "anyinteracts" => Ok(left.start <= right.end && left.end >= right.start), + _ => Err(Error::OpNotImplemented("temporal")), + } +} From 7eff2828155b50f7c8def4cf25fdcd1f8cc9db29 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 17 Dec 2024 12:40:19 -0600 Subject: [PATCH 7/8] mend --- src/expr.rs | 10 ++++++---- src/geometry.rs | 2 +- src/lib.rs | 4 ++-- src/temporal.rs | 17 ++++++++--------- tests/reduce_tests.rs | 6 +++--- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/expr.rs b/src/expr.rs index b088806..0901575 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -1,4 +1,6 @@ -use crate::{DateRange, Error, Geometry, SqlQuery, Validator, geometry::spatial_op, temporal::temporal_op}; +use crate::{ + geometry::spatial_op, temporal::temporal_op, DateRange, Error, Geometry, SqlQuery, Validator, +}; use enum_as_inner::EnumAsInner; use geos::Geometry as GGeom; use json_dotpath::DotPaths; @@ -123,7 +125,7 @@ fn arith(left: &f64, right: &f64, op: &str) -> Result { "/" => Ok(left / right), "%" => Ok(left % right), "^" => Ok(left.powf(*right)), - _ => Err(Error::OpNotImplemented("Arith")) + _ => Err(Error::OpNotImplemented("Arith")), } } @@ -203,11 +205,11 @@ impl Expr { match op.as_str() { "and" => { *self = Expr::Bool(alltrue); - return + return; } "or" => { *self = Expr::Bool(anytrue); - return + return; } _ => (), } diff --git a/src/geometry.rs b/src/geometry.rs index e9414fe..91a301e 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,7 +1,7 @@ use crate::Error; +use geos::{Geom, Geometry as GGeom}; use geozero::{wkt::Wkt, CoordDimensions, ToGeo, ToWkt}; use serde::{Deserialize, Serialize, Serializer}; -use geos::{Geometry as GGeom, Geom}; const DEFAULT_NDIM: usize = 2; diff --git a/src/lib.rs b/src/lib.rs index e15f1a6..3bf86a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,11 +39,11 @@ mod validator; pub use error::Error; pub use expr::Expr; -pub use geometry::{Geometry, spatial_op}; +pub use geometry::{spatial_op, Geometry}; pub use parser::parse_text; use serde_derive::{Deserialize, Serialize}; use std::{fs, path::Path}; -pub use temporal::{DateRange, temporal_op}; +pub use temporal::{temporal_op, DateRange}; pub use validator::Validator; /// A SQL query, broken into the query and parameters. diff --git a/src/temporal.rs b/src/temporal.rs index 88e5678..6c20cee 100644 --- a/src/temporal.rs +++ b/src/temporal.rs @@ -2,7 +2,7 @@ use crate::{Error, Expr}; use jiff::{Timestamp, ToSpan}; /// Struct to hold a range of timestamps. -#[derive(Debug,Clone,PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct DateRange { start: Timestamp, end: Timestamp, @@ -11,29 +11,28 @@ pub struct DateRange { impl TryFrom for DateRange { type Error = Error; fn try_from(v: Expr) -> Result { - match v { - Expr::Interval{interval} => { + Expr::Interval { interval } => { let start_str: String = interval[0].to_text()?; let end_str: String = interval[1].to_text()?; let start: Timestamp = start_str.parse().unwrap(); let end: Timestamp = end_str.parse().unwrap(); - Ok(DateRange{start, end}) + Ok(DateRange { start, end }) } - Expr::Timestamp{timestamp} => { + Expr::Timestamp { timestamp } => { let start_str: String = timestamp.to_text()?; let start: Timestamp = start_str.parse().unwrap(); - Ok(DateRange{start, end: start}) + Ok(DateRange { start, end: start }) } - Expr::Date{date} => { + Expr::Date { date } => { let start_str: String = date.to_text()?; let start: Timestamp = start_str.parse().unwrap(); let end: Timestamp = start + 1.day() - 1.nanosecond(); - Ok(DateRange{start, end}) + Ok(DateRange { start, end }) } Expr::Literal(v) => { let start: Timestamp = v.parse().unwrap(); - Ok(DateRange{start, end: start}) + Ok(DateRange { start, end: start }) } _ => Err(Error::ExprToDateRange()), } diff --git a/tests/reduce_tests.rs b/tests/reduce_tests.rs index 215f190..ae7ffc8 100644 --- a/tests/reduce_tests.rs +++ b/tests/reduce_tests.rs @@ -1,7 +1,7 @@ use cql2::Expr; use rstest::rstest; +use serde_json::{json, Value}; use std::path::Path; -use serde_json::{Value, json}; fn read_lines(filename: impl AsRef) -> Vec { std::fs::read_to_string(filename) @@ -10,7 +10,7 @@ fn read_lines(filename: impl AsRef) -> Vec { .map(String::from) // make each slice into a string .collect() // gather them together into a vector } -fn validate_reduction(a: String, b: String){ +fn validate_reduction(a: String, b: String) { let properties: Value = json!( { "properties": { @@ -36,7 +36,7 @@ fn validate_reduce_fixtures() { let a = lines.clone().into_iter().step_by(2); let b = lines.clone().into_iter().skip(1).step_by(2); let zipped = a.zip(b); - for (a,b) in zipped{ + for (a, b) in zipped { validate_reduction(a, b); } } From 56b56e61c29a4752f6f5f461d25b145ea3c95900 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 17 Dec 2024 12:46:12 -0600 Subject: [PATCH 8/8] mend --- src/error.rs | 4 ++++ src/expr.rs | 14 ++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/error.rs b/src/error.rs index 485f11c..fb95400 100644 --- a/src/error.rs +++ b/src/error.rs @@ -89,4 +89,8 @@ pub enum Error { /// Operator not implemented. #[error("Operator {0} is not implemented for this type.")] OpNotImplemented(&'static str), + + /// Expression not reduced to boolean + #[error("Could not reduce expression to boolean")] + NonReduced(), } diff --git a/src/expr.rs b/src/expr.rs index 0901575..b5b8163 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -167,12 +167,11 @@ impl Expr { } Expr::Property { property } => { if let Some(j) = j { - let propexpr: Option; - if j.dot_has(property) { - propexpr = j.dot_get(property).unwrap(); + let propexpr: Option = if j.dot_has(property) { + j.dot_get(property).unwrap() } else { - propexpr = j.dot_get(&format!("properties.{}", property)).unwrap(); - } + j.dot_get(&format!("properties.{}", property)).unwrap() + }; println!("j:{:?} property:{:?}", j, property); println!("propexpr: {:?}", propexpr); @@ -229,7 +228,6 @@ impl Expr { } if let Ok(v) = binary_bool(&l, &r, op) { *self = Expr::Bool(v); - return; } } else if let (Ok(l), Ok(r)) = (bool::try_from(left.clone()), bool::try_from(right.clone())) @@ -281,12 +279,12 @@ impl Expr { /// let mut expr: Expr = "boolfield and 1 + 2 = 3".parse().unwrap(); /// assert_eq!(true, expr.matches(Some(&item)).unwrap()); /// ``` - pub fn matches(&self, j: Option<&Value>) -> Result { + pub fn matches(&self, j: Option<&Value>) -> Result { let mut e = self.clone(); e.reduce(j); match e { Expr::Bool(v) => Ok(v), - _ => Err(()), + _ => Err(Error::NonReduced()), } } /// Converts this expression to CQL2 text.