diff --git a/Cargo.lock b/Cargo.lock index bc68e7a..c826d6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "0.6.18" @@ -65,19 +71,20 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "appendlist" @@ -112,15 +119,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "block-buffer" @@ -133,15 +134,15 @@ dependencies = [ [[package]] name = "boon" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9672cb0edeadf721484e298c0ed4dd70b0eaa3acaed5b4fd0bd73ca32e51d814" +checksum = "baa187da765010b70370368c49f08244b1ae5cae1d5d33072f76c8cb7112fe3e" dependencies = [ "ahash", "appendlist", "base64", "fluent-uri", - "idna 0.5.0", + "idna", "once_cell", "percent-encoding", "regex", @@ -151,6 +152,18 @@ dependencies = [ "url", ] +[[package]] +name = "borrow-or-share" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cfg-if" version = "1.0.0" @@ -159,9 +172,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.23" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", "clap_derive", @@ -169,9 +182,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ "anstream", "anstyle", @@ -181,9 +194,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck", "proc-macro2", @@ -218,10 +231,14 @@ version = "0.3.2" dependencies = [ "assert-json-diff", "boon", + "geo", "geo-types", "geojson", "geozero", + "jiff", + "json_dotpath", "lazy_static", + "like", "pest", "pest_derive", "pg_escape", @@ -229,7 +246,9 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror 2.0.6", + "thiserror 2.0.11", + "unaccent", + "wkt 0.12.0", ] [[package]] @@ -253,6 +272,31 @@ dependencies = [ "pythonize", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.6" @@ -284,19 +328,42 @@ dependencies = [ "syn", ] +[[package]] +name = "earcutr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01" +dependencies = [ + "itertools", + "num-traits", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + [[package]] name = "fluent-uri" -version = "0.1.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" dependencies = [ - "bitflags", + "borrow-or-share", + "ref-cast", ] [[package]] @@ -308,54 +375,12 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - [[package]] name = "futures-macro" version = "0.3.31" @@ -367,12 +392,6 @@ dependencies = [ "syn", ] -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - [[package]] name = "futures-task" version = "0.3.31" @@ -391,13 +410,9 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -413,17 +428,55 @@ dependencies = [ "version_check", ] +[[package]] +name = "geo" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34f0e6e028c581e82e6822a68869514e94c25e7f8ea669a2d8595bdf7461ccc5" +dependencies = [ + "earcutr", + "float_next_after", + "geo-types", + "geographiclib-rs", + "i_overlay", + "log", + "num-traits", + "robust", + "rstar", + "spade", +] + +[[package]] +name = "geo-traits" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b018fc19fa58202b03f1c809aebe654f7d70fd3887dace34c3d05c11aeb474b5" +dependencies = [ + "geo-types", +] + [[package]] name = "geo-types" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f47c611187777bbca61ea7aba780213f5f3441fd36294ab333e96cfa791b65" +checksum = "3bd1157f0f936bf0cd68dec91e8f7c311afe60295574d62b70d4861a1bfdf2d9" dependencies = [ "approx", "num-traits", + "rayon", + "rstar", "serde", ] +[[package]] +name = "geographiclib-rs" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e5ed84f8089c70234b0a8e0aedb6dc733671612ddc0d37c6066052f9781960" +dependencies = [ + "libm", +] + [[package]] name = "geojson" version = "0.24.1" @@ -448,7 +501,7 @@ dependencies = [ "log", "serde_json", "thiserror 1.0.69", - "wkt", + "wkt 0.11.1", ] [[package]] @@ -464,9 +517,28 @@ dependencies = [ [[package]] name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "hash32" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hashbrown" @@ -474,12 +546,66 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "i_float" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775f9961a8d2f879725da8aff789bb20a3ddf297473e0c90af75e69313919490" +dependencies = [ + "serde", +] + +[[package]] +name = "i_key_sort" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd" + +[[package]] +name = "i_overlay" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01882ce5ed786bf6e8f5167f171a4026cd129ce17d9ff5cbf1e6749b98628ece" +dependencies = [ + "i_float", + "i_key_sort", + "i_shape", + "i_tree", + "rayon", +] + +[[package]] +name = "i_shape" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27dbe9e5238d6b9c694c08415bf00fb370b089949bd818ab01f41f8927b8774c" +dependencies = [ + "i_float", + "serde", +] + +[[package]] +name = "i_tree" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139" + [[package]] name = "icu_collections" version = "1.5.0" @@ -598,16 +724,6 @@ dependencies = [ "syn", ] -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -636,7 +752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -651,12 +767,62 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jiff" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bb0c2e28117985a4d90e3bc70092bc8f226f434c7ec7e23dd9ff99c5c5721a" +dependencies = [ + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2cec2f5d266af45a071ece48b1fb89f3b00b2421ac3a5fe10285a6caaa60d3" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e" +dependencies = [ + "jiff-tzdb", +] + +[[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" @@ -665,9 +831,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" @@ -675,6 +841,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "like" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7281e4b2b1a1fae03463a7c49dd21464de50251a450f6da9715c40c7b21a70" + [[package]] name = "litemap" version = "0.7.4" @@ -683,9 +855,9 @@ checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "memchr" @@ -731,7 +903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.6", + "thiserror 2.0.11", "ucd-trie", ] @@ -780,9 +952,9 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", @@ -790,9 +962,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -800,9 +972,9 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", @@ -813,18 +985,18 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -838,6 +1010,15 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -849,18 +1030,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.23.3" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e484fd2c8b4cb67ab05a318f1fd6fa8f199fcc30819f08f07d200809dba26c15" +checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc" dependencies = [ "cfg-if", "indoc", @@ -876,9 +1057,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.23.3" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e0469a84f208e20044b98965e1561028180219e35352a2afaf2b942beff3b" +checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7" dependencies = [ "once_cell", "target-lexicon", @@ -886,9 +1067,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.23.3" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1547a7f9966f6f1a0f0227564a9945fe36b90da5a93b3933fc3dc03fae372d" +checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d" dependencies = [ "libc", "pyo3-build-config", @@ -896,9 +1077,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.23.3" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb6da8ec6fa5cedd1626c886fc8749bdcbb09424a86461eb8cdf096b7c33257" +checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -908,9 +1089,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.23.3" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a385202ff5a92791168b1136afae5059d3ac118457bb7bc304c197c2d33e7d" +checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" dependencies = [ "heck", "proc-macro2", @@ -931,9 +1112,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -953,6 +1134,46 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.11.1" @@ -988,23 +1209,40 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "robust" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30" + +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless", + "num-traits", + "smallvec", +] + [[package]] name = "rstest" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" dependencies = [ - "futures", "futures-timer", + "futures-util", "rstest_macros", "rustc_version", ] [[package]] name = "rstest_macros" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" dependencies = [ "cfg-if", "glob", @@ -1035,24 +1273,24 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -1061,9 +1299,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "indexmap", "itoa", @@ -1085,9 +1323,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" @@ -1104,6 +1342,18 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "spade" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f5ef1f863aca7d1d7dda7ccfc36a0a4279bd6d3c375176e5e0712e25cb4889" +dependencies = [ + "hashbrown 0.14.5", + "num-traits", + "robust", + "smallvec", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1118,9 +1368,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -1155,11 +1405,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.11", ] [[package]] @@ -1175,9 +1425,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -1196,9 +1446,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -1239,10 +1489,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] -name = "unicode-bidi" -version = "0.3.17" +name = "unaccent" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "a302f53d684e29e3d2ce7a83bb9bd2a0ee4a0793c0d4b6be325983b9b304e935" +dependencies = [ + "unicode-normalization", +] [[package]] name = "unicode-ident" @@ -1272,7 +1525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna", "percent-encoding", ] @@ -1381,9 +1634,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -1400,6 +1653,19 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "wkt" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c591649bd1c9d4e28459758bbb5fb5c0edc7a67060b52422f4761c94ffe961" +dependencies = [ + "geo-traits", + "geo-types", + "log", + "num-traits", + "thiserror 1.0.69", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index d84ab7b..d38ff2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,22 +23,28 @@ license = { workspace = true } keywords = ["cql2"] [dependencies] -boon = "0.6.0" -geo-types = "0.7.13" +boon = "0.6.1" +geo = "0.29.3" +geo-types = "0.7.15" geojson = "0.24.1" geozero = "0.14.0" +jiff = "0.1.24" +json_dotpath = "1.1.0" lazy_static = "1.5" +like = "0.3.1" pest = "2.7" pest_derive = { version = "2.7", features = ["grammar-extras"] } pg_escape = "0.1.1" serde = "1.0" -serde_derive = "1.0" -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_derive = "1.0.217" +serde_json = { version = "1.0.135", features = ["preserve_order"] } thiserror = "2.0" +unaccent = "0.1.0" +wkt = "0.12.0" [dev-dependencies] assert-json-diff = "2" -rstest = "0.23" +rstest = "0.24.0" [workspace] default-members = [".", "cli"] diff --git a/cli/src/lib.rs b/cli/src/lib.rs index b3bb6b0..25f5a8e 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 = expr.reduce(Some(&json!({})))?; + } if self.validate { let validator = Validator::new().unwrap(); let value = serde_json::to_value(&expr).unwrap(); diff --git a/src/error.rs b/src/error.rs index b904c43..30da851 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use crate::Expr; use thiserror::Error; /// Crate-specific error enum. @@ -65,4 +66,40 @@ 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(Expr), + + /// Error Converting Expr to bool + #[error("Could not convert Expression to bool")] + ExprToBool(Expr), + + /// Error Converting Expr to geometry + #[error("Could not convert Expression to Geometry")] + ExprToGeom(Expr), + + /// Error Converting Expr to DateRange + #[error("Could not convert Expression to DateRange")] + ExprToDateRange(Expr), + + /// 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(), + + /// Could not run arith operation + #[error("Could not run operation.")] + OperationError(), + + /// [json_dotpath::Error] + #[error(transparent)] + JsonDotpath(#[from] json_dotpath::Error), + + /// [like::Error] + #[error(transparent)] + Like(#[from] like::InvalidPatternError), } diff --git a/src/expr.rs b/src/expr.rs index 390ba9a..b1211d0 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -1,8 +1,51 @@ -use crate::{Error, Geometry, SqlQuery, Validator}; +use crate::{geometry::spatial_op, temporal::temporal_op, Error, Geometry, SqlQuery, Validator}; +use geo_types::Geometry as GGeom; +use geo_types::{coord, Rect}; +use json_dotpath::DotPaths; +use like::Like; use pg_escape::{quote_identifier, quote_literal}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::HashSet; +use std::ops::Deref; use std::str::FromStr; +use unaccent::unaccent; +use wkt::TryFromWkt; + +const BOOLOPS: &[&str] = &["and", "or"]; +const EQOPS: &[&str] = &["=", "<>"]; +const CMPOPS: &[&str] = &[">", ">=", "<", "<="]; +const SPATIALOPS: &[&str] = &[ + "s_equals", + "s_intersects", + "s_disjoint", + "s_touches", + "s_within", + "s_overlaps", + "s_crosses", + "s_contains", +]; +const TEMPORALOPS: &[&str] = &[ + "t_before", + "t_after", + "t_meets", + "t_metby", + "t_overlaps", + "t_overlappedby", + "t_starts", + "t_startedby", + "t_during", + "t_contains", + "t_finishes", + "to_finishedby", + "t_equals", + "t_disjoint", + "t_intersects", +]; +const ARITHOPS: &[&str] = &["+", "-", "*", "/", "%", "^", "div"]; +const ARRAYOPS: &[&str] = &["a_equals", "a_contains", "a_containedby", "a_overlaps"]; + +// todo: array ops, in, casei, accenti, between, not, like /// A CQL2 expression. /// @@ -18,7 +61,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)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd)] #[serde(untagged)] #[allow(missing_docs)] pub enum Expr { @@ -35,7 +78,289 @@ pub enum Expr { Geometry(Geometry), } +impl TryFrom for Expr { + type Error = Error; + fn try_from(v: Value) -> Result { + serde_json::from_value(v).map_err(Error::from) + } +} + +impl TryFrom for Value { + type Error = Error; + fn try_from(v: Expr) -> Result { + serde_json::to_value(v).map_err(Error::from) + } +} + +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).map_err(Error::from), + _ => Err(Error::ExprToF64(v)), + } + } +} + +impl TryFrom<&Expr> for bool { + type Error = Error; + fn try_from(v: &Expr) -> Result { + match v { + Expr::Bool(v) => Ok(*v), + Expr::Literal(v) => bool::from_str(v).map_err(Error::from), + _ => Err(Error::ExprToBool(v.clone())), + } + } +} + +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(v)), + } + } +} + +impl TryFrom for GGeom { + type Error = Error; + fn try_from(v: Expr) -> Result { + match v { + Expr::Geometry(v) => Ok(GGeom::try_from_wkt_str(&v.to_wkt().unwrap()) + .expect("Failed to convert WKT to Geometry")), + Expr::BBox { ref bbox } => { + let minx: f64 = bbox[0].as_ref().clone().try_into()?; + let miny: f64 = bbox[1].as_ref().clone().try_into()?; + let maxx: f64; + let maxy: f64; + + match bbox.len() { + 4 => { + maxx = bbox[2].as_ref().clone().try_into()?; + maxy = bbox[3].as_ref().clone().try_into()?; + } + 6 => { + maxx = bbox[3].as_ref().clone().try_into()?; + maxy = bbox[4].as_ref().clone().try_into()?; + } + _ => return Err(Error::ExprToGeom(v.clone())), + }; + let rec = Rect::new(coord! {x:minx, y:miny}, coord! {x:maxx,y:maxy}); + Ok(rec.into()) + } + _ => Err(Error::ExprToGeom(v)), + } + } +} + +impl TryFrom for HashSet { + type Error = Error; + fn try_from(v: Expr) -> Result, Error> { + match v { + Expr::Array(v) => { + let mut h = HashSet::new(); + for el in v { + let _ = h.insert(el.to_text()?); + } + Ok(h) + } + _ => Err(Error::ExprToGeom(v)), + } + } +} + +fn cmp_op(left: T, right: T, op: &str) -> Result { + let out = 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")), + }; + match out { + Ok(v) => Ok(Expr::Bool(v)), + _ => Err(Error::OperationError()), + } +} + +fn arith_op(left: Expr, right: Expr, op: &str) -> Result { + let left = f64::try_from(left)?; + let right = f64::try_from(right)?; + let out = 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")), + }; + match out { + Ok(v) => Ok(Expr::Float(v)), + _ => Err(Error::OperationError()), + } +} + +fn array_op(left: Expr, right: Expr, op: &str) -> Result { + let left: HashSet = left.try_into()?; + let right: HashSet = right.try_into()?; + let out = match op { + "a_equals" => Ok(left == right), + "a_contains" => Ok(left.is_superset(&right)), + "a_containedby" => Ok(left.is_subset(&right)), + "a_overlaps" => Ok(!left.is_disjoint(&right)), + _ => Err(Error::OpNotImplemented("Arith")), + }; + match out { + Ok(v) => Ok(Expr::Bool(v)), + _ => Err(Error::OperationError()), + } +} + impl Expr { + /// Update this expression with values from the `properties` attribute of a JSON object + /// + /// # Examples + /// + /// ``` + /// use serde_json::{json, Value}; + /// use cql2::Expr; + /// use std::str::FromStr; + /// + /// let item = json!({"properties":{"eo:cloud_cover":10, "datetime": "2020-01-01 00:00:00Z", "boolfield": true}}); + /// + /// let fromexpr: Expr = Expr::from_str("boolfield = true").unwrap(); + /// let reduced = fromexpr.reduce(Some(&item)).unwrap(); + /// let toexpr: Expr = Expr::from_str("true").unwrap(); + /// assert_eq!(reduced, toexpr); + /// + /// let fromexpr: Expr = Expr::from_str("\"eo:cloud_cover\" + 10").unwrap(); + /// let reduced = fromexpr.reduce(Some(&item)).unwrap(); + /// let toexpr: Expr = Expr::from_str("20").unwrap(); + /// assert_eq!(reduced, toexpr); + /// + /// ``` + pub fn reduce(self, j: Option<&Value>) -> Result { + match self { + Expr::Property { ref property } => { + if let Some(j) = j { + if let Some(value) = j.dot_get::(property)? { + Expr::try_from(value) + } else if let Some(value) = + j.dot_get::(&format!("properties.{}", property))? + { + Expr::try_from(value) + } else { + Ok(self) + } + } else { + Ok(self) + } + } + Expr::Operation { op, args } => { + let args: Vec> = args + .into_iter() + .map(|expr| expr.reduce(j).map(Box::new)) + .collect::>()?; + + if BOOLOPS.contains(&op.as_str()) { + let bools: Result, Error> = args + .iter() + .map(|expr| bool::try_from(expr.as_ref())) + .collect(); + + if let Ok(bools) = bools { + match op.as_str() { + "and" => Ok(Expr::Bool(bools.into_iter().all(|x| x))), + "or" => Ok(Expr::Bool(bools.into_iter().any(|x| x))), + _ => Ok(Expr::Operation { op, args }), + } + } else { + Ok(Expr::Operation { op, args }) + } + } else if op == "not" { + match args[0].deref() { + Expr::Bool(v) => Ok(Expr::Bool(!v)), + _ => Ok(Expr::Operation { op, args }), + } + } else if op == "casei" { + match args[0].as_ref() { + Expr::Literal(v) => Ok(Expr::Literal(v.to_lowercase())), + _ => Ok(Expr::Operation { op, args }), + } + } else if op == "accenti" { + match args[0].as_ref() { + Expr::Literal(v) => Ok(Expr::Literal(unaccent(v))), + _ => Ok(Expr::Operation { op, args }), + } + } else if op == "between" { + Ok(Expr::Bool(args[0] >= args[1] && args[0] <= args[2])) + } else if args.len() != 2 { + Ok(Expr::Operation { op, args }) + } else { + // Two-arg operations operations + let left = args[0].deref().clone(); + let right = args[1].deref().clone(); + + if SPATIALOPS.contains(&op.as_str()) { + Ok(spatial_op(left, right, &op) + .unwrap_or_else(|_| Expr::Operation { op, args })) + } else if TEMPORALOPS.contains(&op.as_str()) { + Ok(temporal_op(left, right, &op) + .unwrap_or_else(|_| Expr::Operation { op, args })) + } else if ARITHOPS.contains(&op.as_str()) { + Ok(arith_op(left, right, &op) + .unwrap_or_else(|_| Expr::Operation { op, args })) + } else if EQOPS.contains(&op.as_str()) || CMPOPS.contains(&op.as_str()) { + Ok(cmp_op(left, right, &op) + .unwrap_or_else(|_| Expr::Operation { op, args })) + } else if ARRAYOPS.contains(&op.as_str()) { + Ok(array_op(left, right, &op) + .unwrap_or_else(|_| Expr::Operation { op, args })) + } else if op == "like" { + let l: String = left.try_into()?; + let r: String = right.try_into()?; + let m: bool = Like::::like(l.as_str(), r.as_str())?; + Ok(Expr::Bool(m)) + } else if op == "in" { + let l: String = left.to_text()?; + let r: HashSet = right.try_into()?; + let isin: bool = r.contains(&l); + Ok(Expr::Bool(isin)) + } else { + Ok(Expr::Operation { op, args }) + } + } + } + _ => Ok(self), + } + } + + /// 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", "boolfield": true}}); + /// + /// 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 { + let reduced = self.reduce(j)?; + match reduced { + Expr::Bool(v) => Ok(v), + _ => Err(Error::NonReduced()), + } + } /// Converts this expression to CQL2 text. /// /// # Examples diff --git a/src/geometry.rs b/src/geometry.rs index 03ccc3d..4133f4f 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,4 +1,8 @@ -use crate::Error; +use std::cmp::Ordering; + +use crate::{Error, Expr}; +use geo::*; +use geo_types::Geometry as GGeom; use geozero::{wkt::Wkt, CoordDimensions, ToGeo, ToWkt}; use serde::{Deserialize, Serialize, Serializer}; @@ -45,6 +49,18 @@ impl Geometry { } } +impl PartialEq for Geometry { + fn eq(&self, other: &Self) -> bool { + self.to_wkt().unwrap() == other.to_wkt().unwrap() + } +} + +impl PartialOrd for Geometry { + fn partial_cmp(&self, _other: &Self) -> Option { + Some(Ordering::Equal) + } +} + fn to_geojson(wkt: &String, serializer: S) -> Result where S: Serializer, @@ -80,3 +96,22 @@ 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: Expr, right: Expr, op: &str) -> Result { + let left: GGeom = GGeom::try_from(left)?; + let right: GGeom = GGeom::try_from(right)?; + let rel = left.relate(&right); + let out = match op { + "s_equals" => rel.is_equal_topo(), + "s_intersects" | "intersects" => rel.is_intersects(), + "s_disjoint" => rel.is_disjoint(), + "s_touches" => rel.is_touches(), + "s_within" => rel.is_within(), + "s_overlaps" => rel.is_overlaps(), + "s_crosses" => rel.is_crosses(), + "s_contains" => rel.is_contains(), + &_ => todo!(), + }; + Ok(Expr::Bool(out)) +} diff --git a/src/lib.rs b/src/lib.rs index d630014..3bf86a8 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::{spatial_op, Geometry}; pub use parser::parse_text; use serde_derive::{Deserialize, Serialize}; use std::{fs, path::Path}; +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 new file mode 100644 index 0000000..ab23fed --- /dev/null +++ b/src/temporal.rs @@ -0,0 +1,83 @@ +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(v)), + } + } +} + +/// Run a temporal operation. +pub fn temporal_op(left_expr: Expr, right_expr: Expr, op: &str) -> Result { + let invop = match op { + "t_after" => "t_before", + "t_metby" => "t_meets", + "t_overlappedby" => "t_overlaps", + "t_startedby" => "t_starts", + "t_contains" => "t_during", + "t_finishedby" => "t_finishes", + _ => op, + }; + + let left: DateRange; + let right: DateRange; + if invop != op { + left = DateRange::try_from(left_expr)?; + right = DateRange::try_from(right_expr)?; + } else { + right = DateRange::try_from(left_expr)?; + left = DateRange::try_from(right_expr)?; + } + + let out = match invop { + "t_before" => Ok(left.end < right.start), + "t_meets" => Ok(left.end == right.start), + "t_overlaps" => { + Ok(left.start < right.end && right.start < left.end && left.end < right.end) + } + "t_starts" => Ok(left.start == right.start && left.end < right.end), + "t_during" => Ok(left.start > right.start && left.end < right.end), + "t_finishes" => Ok(left.start > right.start && left.end == right.end), + "t_equals" => Ok(left.start == right.start && left.end == right.end), + "t_disjoint" => Ok(!(left.start <= right.end && left.end >= right.start)), + "t_intersects" | "anyinteracts" => Ok(left.start <= right.end && left.end >= right.start), + _ => Err(Error::OpNotImplemented("temporal")), + }; + + match out { + Ok(v) => Ok(Expr::Bool(v)), + _ => Err(Error::OperationError()), + } +} diff --git a/tests/reduce_tests.rs b/tests/reduce_tests.rs new file mode 100644 index 0000000..c88d1fd --- /dev/null +++ b/tests/reduce_tests.rs @@ -0,0 +1,43 @@ +use cql2::Expr; +use rstest::rstest; +use serde_json::{json, Value}; +use std::path::Path; + +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"}, + "tstarr": [1,2,3] + }, + "geometry": {"type": "Point", "coordinates": [-93.0, 45]}, + "datetime": "2020-01-01 00:00:00Z" + } + ); + let inexpr: Expr = a.parse().unwrap(); + let reduced = inexpr.reduce(Some(&properties)).unwrap(); + let outexpr: Expr = b.parse().unwrap(); + assert_eq!(reduced, 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..ebe3b4c --- /dev/null +++ b/tests/reductions.txt @@ -0,0 +1,56 @@ +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 +A_EQUALS((1,2,3),(1,2,3)) +true +A_EQUALS((1,2,3),(1,2)) +false +A_OVERLAPS((1,2,3),(1,2)) +true +A_CONTAINS((1,2,3),(1,2)) +true +A_CONTAINEDBY((1,2),(1,2,3)) +true +A_EQUALS(tstarr,(1,2,3)) +true +1 in (1,2,4) +true +'a' in ('a','b','c') +true +'a' in ('d','e','f') +false +'this' like 'th%' +true +not(true) +false +not(1+3=1) +true +casei('aardvarK') = casei('Aardvark') +true +accenti('Café') = accenti('Cafe') +true +1 between 1 and 2 +true +1 between 3 and 4 +false