From 6497dc7e90b5dfbf3bf59d725a890ba4a2f1ee24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=A3o=20Mata?= Date: Wed, 15 Dec 2021 18:14:43 +0100 Subject: [PATCH] Initial import --- Cargo.lock | 404 ++++++++++++++++++++++++++ Cargo.toml | 16 ++ Makefile | 10 + README.md | 90 ++++++ docs/screenshot-01.png | Bin 0 -> 21852 bytes src/cli.rs | 32 +++ src/main.rs | 206 ++++++++++++++ src/parser.rs | 623 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1381 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 README.md create mode 100644 docs/screenshot-01.png create mode 100644 src/cli.rs create mode 100644 src/main.rs create mode 100644 src/parser.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..83bc732 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,404 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "jaxe" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "nom", + "nom_locate", + "pretty_env_logger", + "serde_json", + "structopt", + "termcolor", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + +[[package]] +name = "nom_locate" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605" +dependencies = [ + "bytecount", + "memchr", + "nom", +] + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "serde" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" + +[[package]] +name = "serde_json" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..13bdb8a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "jaxe" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +structopt = "0.3" +log = "0.4" +pretty_env_logger = "0.4" +serde_json = "1" +termcolor = "1.1" +nom = "7.1.0" +anyhow = "1" +nom_locate = "4.0.0" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9bbcd80 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: install clean + +target/release/jxact: src + cargo build --release + +install: + cargo install --path . + +clean: + cargo clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd65f0d --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Jaxe - The J[son] [Pick]axe + +jaxe parses [new line delimited json](http://ndjson.org/) on the stdin +and outputs a color human readable string on stdout. + +jaxe supports basic filtering with a simple language. Certain json +values can be omited or extracted. Invalid json can be displayed in a +different color or omitted. + + +## Examples + +Considering the following newline delimited json log line: + +``` +$ cat example.log | jq +{ + "logger": "c.a.l.h.logging.RequestLoggingActor", + "http_stime": "43", + "http_method": "PUT", + "http_path": "/api/v1/user", + "at": "2022-03-24T08:56:20.576Z", + "http_service_name": "reposerver", + "msg": "http request", + "http_status": "204", + "level": "INFO" +} +``` + + +Piping that line to jaxe will output: + +``` +I|2022-03-24T08:56:20.576Z|http_method=PUT http_path=/api/v1/user http_service_name=reposerver http_status=204 http_stime=43 logger=c.a.l.h.logging.RequestLoggingActor msg=http request +``` +The output is colorized unless you use `-n/--no-colors`: + +![screenshot 1](docs/screenshot-01.png) + +non json lines will be printed verbatim unless `-j/--no-omit-json` is used. + +Often you have fields you don't care about, you can set `JAXE_OMIT` or use `-o/`to filter those fields out: + +``` +$ export JAXE_OMIT="logger,http_service_name" +$ cat example.log | jaxe +I|2022-03-24T08:56:20.576Z|http_method=PUT http_path=/api/v1/user http_status=204 http_stime=43 msg=http request +``` + +A DSL can be used to filter log lines, using `-f/--filter` or `JAXE_FILTER`: + +``` +$ cat example.log | jaxe --filter 'http_status==404' +$ +$ cat example.log | jaxe --filter 'http_status==204' +I|2022-03-24T08:56:20.576Z|http_method=PUT http_path=/api/v1/user http_status=204 http_stime=43 msg=http request +$ +$ cat example.log | jaxe --filter 'and(contains(msg,"http request"), not(contains(msg,"username")))' +``` + +You can extract only certains values from the json: + +``` +$ cat example.log | jaxe --extract http_method,http_status,msg +I|2022-03-24T08:56:20.576Z|http_method=PUT http_status=204 msg=http request +``` + +### Filtering DSL + +The following DSL can be used with `-f/--filter` to filter lines. `.` +can be used to refer to values nested in json objects. + + +| Expression | Example | +|-----------------------------:|:--------------------------------------------------:| +| equals/not equals `==`, `!=` | `mykey0.otherkey == myvalue` | +| `and(exp)/or(exp)` | `and(http_status == 200, http_method != GET)` | +| `not(exp)` | `not(and(http_status == 200, http_method != GET))` | +| `contains(key, str)` | `contains(mykey, somestr)` | +| `exists(key)` | `exists(mykey)` | + +## Configuration + +`JAXE_OMIT` and `JAXE_FILTER` can be set the same was as `-o/--omit` and `-f/--filter`. + +## Install + +Binaries can be downloaded for the releases tab. + + diff --git a/docs/screenshot-01.png b/docs/screenshot-01.png new file mode 100644 index 0000000000000000000000000000000000000000..95b0f1d70274bc504de9f34dfe984aa433a5eca7 GIT binary patch literal 21852 zcmc$`V|XM{*Y`UUOf(bQw(VqM+eQZy+jcsd*w)0hZQHi(_U)IXgox6&Z{Q7}_lnS9pl!epbW6|+xTBDy3N$qI z+K&A8w{L{sBt?F!xanPFLOH33Vhp)TlGIOoUWszg76eMg(u-0N6Xp@-%%b7|vwz{B zw*~~T)_j9S7vat$?0CYrAaqjd++wzOYj1jUaj7HvZYnP~dD+h6dilPU0NSqGbKZ-S z)s&X1nP`8W4@fgDhlQZIt{#GD%2vdYaW=NbqdPUr6b02728e8ZeyI z_O<>i#*6aL?PPI#u+057olH_QMtkCIaoeTLGV%Uk18jY@!BU(Lbi~B_2K)I6 zZ44UcBwEc<^{Psx^@;@9(RkY6(wtb#H0|lX$RwL|lQ^`a>Ca}N+X@`5x0Amp#O^;g z5K>FkDqipB%BfZUUC%4aZ+KpHU@-$YecqpU2O@}g zyq>GWWn$BbQ;nS4N0Yc54;w6(S`Qw(zCLW%o9%4~Rw!N-A-~}=1BN59Se_YD0=Xh= z(*qvQSLSklgG+8V6b00Jz1(`g-tU+c%4dJ>kHkly)4jgmO(Ed(nk)Y;77sgeiH~j) zaM+h@3c6`e4zH>)P00m3@3_MOC7bq8`yzC)?Xv6XxYB%V_*L`*m0+wTolXf<$q-2Ve;fGn!2>yw+35;`~qD^u-rm?S0#yy4GW zNRL(=m4h;=a(Oh+O1(-?w|1X=Pu9!J3p51MiPVJ}!0_=H>I6!~-4(NJ$nGV5UtNcY z@bH(5rmg*{I5O$Ov1A0%i{V%j;+dExLMKk@;b%-NtP!3AlO4ZJ&{!{&slRD$sgx)Y z?YBCg$zWATll=B$m+xEd^8E@!BJrY=o~7w6yFFbnQK+J_74IZ`26x_UcXvD(J!quG z5!~I~J)Fw69UW+%TJ=LeOG=k(heHxXcs|PZ6&WBQCdO>g^LaV2SSaOt*^jU16BYEr zwm@~as6`F)t4^#9P_o4dFM%Z4S+nh`7$%CXtr_%uSWsVUvW-CwkiMy)QmH|eO{zsa zzV%8Rp}{ZNDF54`!4Bn7&FtW0J69wN8g6tMb6V-NzJbmt@A0S6y3`K1=nE^C;(NZSSX3XHQ-Q4W(w4DEE zsEk|6@{j-hx=#&zpa~Hlztdz49kx`rtq~AFht(+X37S|HO9^;9F5B9Ni86=^ueBy) z|2bQU64?N)8uI{p9&}h@S21d0TJ7Vv5r1$f?9^%-08cO)`654xqrkVGWf`Q0(*;MG zVp-Qm=eA!w-_t*%vfL-Ee={q_j%P{U@;6ikR5ib-V|=~Sm@Be|fKJ(`Vcrt}sa~lg z$sO`DHX1D|Te(<{Z6xF)W%5|fAU$`Gu7hnxef@kZV82SjG?C(0w^gC9_INA0o;_n;->fb_ij_dLC@JeXM1VPRFd z1?JIMKITKB>+ADv> znhkOc;sm&kI)Yl3PnjNzqkH!8v}I<2#p6PY4<_U`unUn&pz3y*YIfOjgr8$-bXP*F zzXf*6j|TA4X>}?Ye_K%3|Ao!^&wM6tF;DYUCWSFqDv^4wSe^sBk*u(~REwom7K_o~ zThhEd|4V;7N5rs+{y*Y&pLUNX<~Ma^5}NY^NLbY8olt^hGgh=*=&*6sVmZ9#kJ~W@ zkmD}ysL#q892h7HAJ#bv?}4OV_z5rT4LEBZ*xTgyz8)l(FWbq7Q(?Zpgj!8-&xzV+ z|5yc68&5_rM zvU+b`2!_sKd{L0^N|C+BGR|YY;Om9L>!!rlP5@a< zRaAEtpie58W;;KIAILj+KCY?ZQr$B%FUtK2^R|Xb@*KWf#_Z}zt>Q+Xx{eWyfIN-$ zKrq^CAHA_86bu^+w>0-~wq!g|J!vnK-{JW*xJtQ>{rh$=Mu5GAEl+=k6RHmsxt`8f zP_kxbW{5x!JX^H|V;WFHyEr*e{YeEMZHTMKg(Sxz@uJ z@Zpsr-N32R8Bme|bgJcAt<>H-I;p+-`I5y+*A#&Q6SwatuP~`@LNo=iZy6FZg118lm$rw(Dqqsg#3xmL~fPmV2bcPtA2)<57=_0S-+k}#NkN#)d zP<|h)HXK{4w^GQ^!pV%TVS?-qy|$rlv5%0dY!2%c41xs9)m*V+eou7<{u;CNVZD0O z{378^$0Asv-&`%O;zjMjQXE#A1zI0ifvp)__3G2lDv(vpJuK=Yx7J&)#FU&6jE3+Y zv~;lQRRWF%82MQ$jrMIny!App=lwU1ATtrr16QcDxN4mI~>Z^i*PI(mo3#bA9UNCh%9rF>zne}uqScX z(MBLqvP-woIKH&PCVsQF+jAlYSi{L;h@#M?MoFEESg$p1bb7ayG#Idsq%e_)F_lv{ zdomsGEEuTx-mJ&-3yd@T`>wZ^PO+jOG#x|WaZD8N^S&L3(Cavv!4=2SnM2K8qlhdY z?2)L3%=@(N`0@JChXK!;vf;2l>9PJYB57Ngq2_ zx@U?d&JxNQmW`D`(h*%D<+xYz^3wdOVHWdF;M1w${C%j5rr^QXJJT+lVDWzixbeWlao@$l7QC278k(^dw)wKMsljptB%U+n+%d^3B zlSKEv>-jT&vY+eM{qN&DMCa4xZ{hS1jTu-DW!h^PrO;Kxgh0PH6=f26c<5WhNbs`%@3p{)uXHb0*6SEAP z82ZcNdtUg)f7kBZfq($jO$Swu4`v+EB*V#6YSj)3eMIbwrz+l)viFm^Lmjq{n?uEEutrtQT$v@8S6|$tOvB|Ln+@o&urSukPl-yuv~otL@Mh$%f5&0#zTSH8 z6!-N6F_eob90UsW?b5eAuJng~np-p8mMr~Ia_iqYV5lQq!l-JdVkZ@4R-E|O$bso` zkVI~rgsq#^V)xoV{#$nv&o%RBZtT(pTfJ9ZG#mF*=F{OfqC|PsYhXG7UC;xv*-~|k zhAUdF>BMM%6x~nfUpTN}EOC2d5t{ceN7+{kWyv)kc9ns13mH0Q`=RFzOL}Lsg>V+c zUt}xtO9HzTIAept`(kRhZBbU&J;Z)R(F0nO7{PEDVr*tb2DM2tUaHa7u+B6EY_aa& z^AWPvx5*K9a++ECz{2KZQUz1>dSKglgp?mF)R!<@w>qFYmIg| zD0P!*EuLniCA*0*b5i3yF`1Hu&5<}%Gf4eMj5C4f!K?E!eF<{tTu#RdV&6R{spa`p z{T0Ix*orh~+N#M|E~~!^eTsUtTD@ggj)#`NKmlJ$ngT~jWzKD0{=v^b@8Mc|2 zy97y2gKa8x;<-%tBVd(SE^Z$MKfTRKe=MfjT|z}~bI4DArkI!rQ7&>gHSSNiy0eC{ zmRDpjlIuiBb|PSm%B0Zy^AR=`l0xM>NDvXVh;jyJRr zWk|O1?`_&d@SdRzm(A6KM2ApGJwXs%bQ8jBV;~@bs@GN{)JP6?7$;G2KPS1By>^cCydO3rA6}F4uT9{E>heG(zYVN2FEJN-BgAH3%|ofkn8WS~4g&xJKaz2l}Yk2PTx|v0~3n^T18W2JyBDQZIHI zs%3{2Yy+~V1yS5AJ|c~~g(@JgXBdAofQ-554!IDo|A`J)v$0 zenbYw4jNJ=w2De?`&fBZ}Q|Nx=?V8t6 zlBpANaNG(;Tf*oIDNf^e_kFY`zXT&AZl>_;9z(-VRYoeix*IaV ziH1!yVwV^!7a3K9ZybYDzt>Fh4J{)YQXj&CjUKE4f#5$>?u{$4nNEhpj$j7@A_0%V zrHla9!)>-K2RGrGYfiguzcoC?0QjB>%&~}UnVxO2qlR76)5-E+=!&zXI(4G>JR)}n zRzYwdtMP_8s7JfQ0@EcQZH!M$=X*l$39Z&qzwG^2rpY&R6EIBe&YWq%_xh!`-LisLHW`S@HrFS_|6*m9l~?(dSkG{`0Q zDT37{RHXuq7WEy66X|=zVq&|d+i*QTSaL|^#T9O=1%!sJ$nszZo8vSRHbLtcL-ItNN{qh&Fxux!8ktFOli z{iw3-DHc*~E*y))V;J!|C^a39P3{28er_X!=d^i7F&-$ecDr3OWSYvuJA(QxaR28gM*DPz;xfcSF zK+Hcpxa%M9K3*(#B(rlqbbsm+0vg8Is%3e#cHIGL!)~0o253v5RC3LsLwc?GeOPFS zx0h{O)_cv08~rUJ#T`3c3MkWxs>`AV@6-)+(XN&c&%htp4&4oliJcgZhz@p6T?Rgm zqqVHW+uzQ2qRuIct^nw9(;1^{t0uaMyb=CuD9!&iU+nr_JsM=lt14Gf`518O8q%SU z)(nR=0!zX_*cNMx*eVH(!f;>}!nlYi?Dm`>y`T>)?-BaKlaPWK9cep1qE&3cs}vpd z^*f9zD%jXe`!9tI{?Yoj8iYtu-Ez?QuVcdM}ub(Uht!iy(`faAn`Ec#XdnMz$n;#Nb?^-prF= z;=1xUAit%KbGN1k*5+d5RqP%a06j>^iK3_7>4v3pG$6U#|F8^qq(OFJ=*8KHCNuJtW=0Ag)K(9B=mo zU?;eh#07(g&dMAV!8#Of%VeP5LW>_6^RTz@X{Ml(jkda#pE!HIL{S&Fjuo7cE@Ucb8jn_6)@_*Y8K!(+BqP`Q{G!R){pKFAGrK|*1oqYVnO z(^7Q*vY8d3D<8g1!Xo0L6Pcf{5VKMKCrITtp)}mLywv6E+jzlAFDuR1Q>LM8OkafS zgRH&U56nLGtL_ubAgm-OVsA{8k1L|IkfR15aosvhgPAcCF*2<(?+;H*(Hu=6DS|2G zODk4#&S~DZRm!Tp{8Q<175kmh@9MTTKtz2rdop2x#!A#^MDpG=! zcQdz;Rn9Y^3g~({qLb z!wln%Cpg`WN(K;VIGk0bZWOQ! zC2_CRu&#CKUHw0>Mxl997$YQk*QjiCnhyLlfCBS|^<~@g=bAl$KZJI}Gp^Bqg zR8eG~Ov)W{9G7l|Lc+&9GN2ClnA+z(Z7?68-u~BL`R`-3x0tnDa3(yeuD~hNc{sHz?VG6F|)n28b$`wS@9FEyB-nLFf5(v&r&$7-452Qp< z$ip>8EL3Q`EAPwj&IMjmcY3_AbA9>O1Z6Tu8yHD3b(jpKjp{r{A;KUnY*h?Za}#Fh zLn!Syn;}z`_##&UiJ{r88f>s5)JRFbp$~D54Ckm79}&(Ud|0YKW48;{j8Z(>w7DGu z8hgpcIDA!1sT5v1hXyCDZ-D9XZ9vs(hL8QvOr5&827?EEL)946tgEXyh0Soss9^NZP1QA(TUnSaMEUQ7cCvVh;=BbEQ7cy-<) zO5nQj6_@|aSNrOesy)tTz3d0PMNs`zE(Ke5J*=0y_nhYq6>5sml`xN$f4nB{j!Qe@t7yKu) z7xghmHOY@Y7898yj)o8u@!3U{Mk%JZ$p>3Y#$TphPA1EyUPOKSE{(AXN8jQ$mXkmC zXGVNRe`8ZzunEJ5StEUUhK0-Y8|wOXgbC!p4UbQ4RCigiA?DM`^N0dmqr$8eX2gs= z)=5g^#wEwfe<$PB>rjjM2OBHy3alvg<0t%?=38hz|*6VvJ`+Q%P_v-_(y^C{mVm zBGV%V+gF^1C5-UZEfyZuYlKf~4~J9QzS*-tH=9mWDnVM(J4$)IU*+o^`26MOgHku2 z;dRPmoT>Ctd*QrQulQ|eWn)I`)*7wmdW#WA| zwHv(qrTT;BM_<};aSAVVz$MX8g`Aof95uk-+E%XxN$jtJjq0ikMd^0Z|Z}@X2oi=+2QhC;(E}WgYkvi%sn-K zM2vjV)~j=j9mL%I{Di^ce)?-hJ1}RR#IaHTbNC07EGIXYW`o75daCxRVwFVwpR~%T zqZtt}Gz&Rk$#Kc;F?pk_U8xf(m(10+oWG~1G$I1B*6A3Ug;5+_kG>?G95?b$g9QHG zDR0(ClLF^y4Vsl#$$seHuDhF1?EK3SW+fs|+t`L>e-wgoexi-2D5xbwO$Mr?sOB8i zQ5(SSki)&EUu!E+4U3?3Q&T2YYA8o9I-Dmb{+u~zo=-~_l|5;7(z5)Wkzry_dzswm zK_?tzO51GDnFb&8`HMrZV;fTv1HDuU60L_*Ll)Q5zKit?)@nK3=fmb9(tc*?D)Mi(b;Kc3Yv5a<^t`1D+3R^_ zB3!cf%kL$oLufy0veLO~`+n9K1|AzZuVdG9=o*Q`{xI%%(n6L0Wq+dCyzCb(;+JQu zRSJo#K{7;n=r#WqKd)7b<;QKbzyuSiArLL1BXX&bBSx@Yg_3$@CNc+Qe0AOj$*C&> z4toMdOGDxyLtztmQF%buR|!u!l(ES)I-X!SkLX6K^ZabI9-|s7TU!Xte{J|R;Ss|y z5MHJiWi*UxENCS{BG;AI3bpjz30=cs_wlw3w)y!ad|PdMFX z*eE6~BHv!A@aa1|3RxXEbh;f2tZzgY4ZAcNbCs@GhXtG%yh(A5)n2G>F4tVqkqSTrnmBrDE(`7Yho)jN^W@o_le7xYOBysMArjx0?l-YlnN*OpS?e^AE{!mq7TErqrXb`(nvPd=$g3J# zgeHXon@D3@Yj?j(W3z1D8Hl-!fBfXVa@6+F`eZpVs>_K>OLXYaci16EONNuu( zY}luQ{l2kjr5BnN$$U8awcPA37Un{$%lNg?!=7^@J`yYl5;ecx@8pe7?hYr;)|#re zZXvFduKdOhT?^Yy%RzX?>RI;bLfO~bRhQn0EQDkk!55qL_<^G$lRATKhCrYC2y2Jf zwvBL(#KRASUvK7jRa+mQBbMyW>ZEl$f1P5$XtWJlAgdQm@N#1~=aO84f_RiNo%$<* zUCayipYRGZVRKwcnJ)dG7sPFdCt(e-1^Fb_@@B6uH_4l>84W~m!z7|^ zXd__6aaA(&CeknQv1{rCI}&VbsT*{(d%O8&-1N=f`|Ppd&%;R&XhgKuue6ZBR*=G{`*N_G-i`3%`vpkcq;tsOthm7j8ongbiwt=@LXBfVc6G@{eZ@q zTPR5c$-nBpqn5Y#lD)bRUUH3tDus>*oW1uS%4RYa5Yc!%kJo(&5lSHa{vg{USwyG^ zup*<%4^llqg8c;tJpU6@m-H;we_2fTMYJG{IP~a^sQ5eFO)&>grDCRC=pC8apha}` z5blZCr`i242hJZ{Bnr{uL*i4NHv4e)oNqkLtS%t}Co*43?^rbOkpsdRNImdJo;PMY zwq(;(uGC=cRar@l^V3=x@KC0QHk0@1xawbaI-SbjXC7vu3m_nn@At%}+SLyI+2^vo zgZPqx6|JVDnO~y=cb;ktdi*E(V>60E(=Jysqq?oLu6-W(>(#529uxE*m)2W%TLfKt z%sYIsdNn3T+7q=lz{&R%m%J+?UlMf_*o$y3jwk9xp&El>-`eHcHa_;YH_&p$_lY zZG4obfJ94G^BF4bT;!5Dvvx%1+wIq`&nzbX)8p@wiy#5n&vN3N!IJM&M`xU^)c0|@ z2MVe)lUujH%dx0Di+^#LQ9;%3i07((E>}1dolXmtL9g9xDk~Tk4HzB$3DVmo1so5H z+?zncIY>CI)M;VHVWp7I(rL6>86xoYy59axm2xfl3SvX_ylw`P^?g#EEz(&3uv;xN zg2r^A%+_NJK&9PC`ziml>1iAC6H6|Gvj#AN++4%YvJ+yU+vXx#I-bfR+z3*nd$02 ze5co@ciPp-I{b8s#gZ+icq|80m{~-7!7LOzk5f%3F*e!^FsLRd_j;a5#2i5UXn#mK4hZ!M!b~%@(V_)^eG5abP{MJWeD^p1J-<~7RaMjU*?WK#>($AT< z^O>>8mL%x)vv z?rTzZWUWToZH7d!Vp$j-Pe7{F$59)Dr`L&_PR&vCozE7@zl=-X+h0%1C zpqvGEv6UXj?cG1XLJ?!7nNDQ3x$B&U+TJud?w=`-*F3`-v1m2#H0(L4^82`m;^}fY zp-JH+gf2p*Ou$$Hqm2Bbkx)60MYU9|${WAMb4;MFYh1DX=R3iR-RqLK77p6wsC8@Z z!=PRpP|E&dHn==kO2K>v`I*9I}-WD1V?dC2J00Y}0JeDkHMW%@oZ!Iw+bhJ7Gs znM9*rX)>0yRN113o=q;BHcb~85OACdVhw4O3ZUQEp0<2_LBwG>pKosv3>J$i2(UAr z$rCM8s{rAJ^N$y6IBO`oAgzjoloaB@V<;N$Z*D%E&0yBoy5kfGiHMPdrnx`I<#G^I z?(*^87Y|Hk=%rXWw|oUn{ComodThSUq|!Rl?LU?cBJptu@p_{|-q@2z^0uFdRB~b?oN+XruNl!f z92u##+)6bs*Gm)l7+G3EMzfBIv|EoA3;_YcDd;CLk=yi&%M$Rxzob+qO!XYPbhg-QmUAXrXr`F>?G=y!tj01or%Z&AS1 zNgWj1u57Q{5ez-I9njhXGlgEK*)M6ley!PluhDwVW~*~U`;yex=Vn*bN^ECm=U>)v zh^Q4v-a+8K_M5Rl*C0Y=gOafYhk`FSuF!5W8Uq(&H7+9s0%BuhXFc0AYK)MOkkr(O z$y7^K;wj|CbbsTC2MV#+gG0mB{%1YOZ~vX8et6Jn^JVTzf>yDqMB?|v#oI@ko-#(h z{%9{F)o){NG)vUN0h(-~ZT8M|xB2R;vXX$RvJ*M8!o>|jpZ$EC&=)00NWPl~V&tno z8QT%y_JPIv#DxcC+3uxv#ZEGofO8Hr-PAX2=``%jx47qSN88Z^R;)UOxvY2$AqM0U zhmK7Kp}`v}$&dwq)ZCRE!E3xi|Kb*D^?lD3@(aj9BeZG#P`6LJZ^}s#nV5;F#*DgJ zq(=9misVQi8U+wgP;>kehBai$n_hRaN}cbw(j!1JR4NET_mje8bIO8zd1E`Wo3pB} zBml2gvmH^mJnv1A1fcK%#N@;?`@FkJ9y)OwHn?7G!lF?Ri`=VVZ%qk&(JG^w1nLzQ zv3_LoxEf`PIcSQ2=+@W?w@;4h;L(8duCT|)`SHZ^2J=nCIZSK>ueJG2QWSWco3Kgq zsl_ZA^wG456Q>SkEn5%?(9`m8#&PjyIXUin{zT;S({b^LR`D?;gAMyJt8;fI#!QQA zd8}x;4)uGFk4#}}bW05cG08`)NJuSCZHemd4sb0?-4UeMHkPBcNts(kXK_rbK)lMG z`b3fmTr+sNlN;{<7MhdPD$A+cz_S4nvqk+(F0Lsvlgg~TwRAqUEb{z)?2%Ztj|G9F zu$%+0)A8E`k`KP6H6E)2Pzx zkRZ|v(zdfgW)fh*%Ee--fn_!=1ks{+HhY6FR~J#}a8TvC?4q9Dq(>g}uBXs^4<=BU zlOTuUzjU4jeQu&Wk2?*};3b417f?j>oCjgFtFmv!t0=%wfmguYA`fQCD_hi_Do9^v^O`FUD+ztx85lkb0!n^p8(Q7E3kUZ{kRkNNzfb6{bBo7`tbKRGaS3)#nCQ$FD4?RSeMm^-T(MN2`!Rq~MX(ks#g#HNCH26$nF zL8*z);n_lI(ck4uFT^>BRQ`1H8hBM(sa9%oFA0ag$R?Z@=BG3rvDebo;e8M}@g~vq zqk4~}^|Y>L!%3LF_NlZ7mgrG}AgP?65AMujthXo!mOC7Xe0?MeH|yRFE~z4n(7Z1E z26weMbq~MvcXjc$Ua5|L%I?`E*#}wOZ*$IU#6}DIZ1w9edu-%l1@6{O{awA7Kjm9qIl)9kQ_Y`7Zfaw*@f@!>GsAazL-mzDOZf#y&5hYF1BI@q7}K z>W0+-43FQdIR#~JZ*RsVb;(YDJdtK;Cg0Q-IlVE+m!y%X(Jqw$#V#TWkIe!VKJ*!L z8PIMLGabofr6?{{&`IF)Ocd|dV6hM<@lABXpUzSbg8YKOM?ztIGLvV>V;hvcbT}M{ z2`iD_mC!#IO=h_FP4{sPn`OC+fjPuu^)=fHVHG1>m^cFcg_D}BV=YBh%Ns3^w7h`x z=EE5Ij=^4lFEZH>8I1t@<){{qxt;YZJr5(Vr5{3EA;NBh{Ydb#D$wQ$AqIbzkz1x=E9MW$_jc1$noO1V+z-1xY#!kfAnvrum6xc zm%XM=ct;!Y%B1WlrC~=)h~Xp+)I93;2OswkdzH)N-f29FcaU*8)Grd<14t2Xg1rx| zsEcM56y+9)-8_fuM(#;Us#NA35)N0R;|wr7;M z0HQE%`)%m&s^9I5u116JL@il0?q6^;daG}DyCRY+2zU2bFk zv6p(xSlB~RX47@5JeZfgQ*WhRu}|IprXPaqu1W8?UwBPORVrAcgoaK`tKW$mQH3hb zDP65i#6cSwm&U4oQcL?s1W1aa-yRo>C*X>{C9nTg%`56Zc=ui3^W-N)lzgpw1r>xF zdmU_lEI0!atjSCa2~rlm7{{3(hDHC}CyyCN6a;;GWnssPkTugHl!VhQ z^zrS8eWDj>p~{1ULF>*3_l&EkPru{`oz#LYzwiEEJn5e{ov;hiz^p=ra^XT)AE$29 zXx5JmEPC$UkpAo)^Gpg!#-CTwF)wJpR&GbmpeHY&UzhBin~u(tWL(_>M#rSYpuyPU zC=M5FU3L0`GWw7b7LWixTf8F-nXoslfNG>Dxb3}`%71TS2{tri>waN*7H@e!>4H|J zTzLJlI37@wgS^5Gp~gh z@Qpg1Mwa*+k{QEEmAG)?bq7Wy2e@@+a@G-Qc%XMe*~duMJ1? z#ypuMQyb)+Z zQkb)8`^*SJwpj#Q8Fl2{)78fDrJR6Z^_gR`Cqqe}|Aiz#tY$HW-+i3U@~uDOXtLTH zC6C%#Q%9 zc=w&AH|=bmD!GX@R<=B*oa!86M(@BVnZ*V7VxNNlHUM+Vv@@l$nY7>s*3F;8pbWx3 z3@+VPrveFOj!pQWv27X3-@gu+__G0ZQfKTY3#F>hK&0Z3F$YFn<3aY8Xfn}U8!}0m zyxmugU|M2vJ=}XyfS3remud)Z%gYFrb~b7M1|uu4EG>oHCmf4-lP-s=MbxBsn41OP zg&9M772jAE&XD2FlSDcf4^N$uL!kv$?a{8lYIS*$5!yVD!4L6L^$aW;P9Yc-US59K zBUqXOVlVCQX4Nx)i0A#$ar=?{@4if0w!`WMPXm03!8J7w?9+`9s>bt#l9<=h*R2r&Yz@C0(uM3j>S*h?b@_d`Hm|th$1-fRr#? z_muLM5Kw{ZU!-`?v^`h0IIZs- zZ?8p$#K`m2$A3EO%%<4UCW0h@Og6{@Ow?_Oe?>fPK$0ltqf?eRs8Q>VG?Gd;Hoe0N zm=*}#?FE{KSR_z(BD17%pbk4o)nOa%?BclZfj5t2cQEHlHKG{p3GZP#L{how7hkTB z;(KS0xCzyA9r#;Oljz>eN?B8VE7`}xE3nD5Tf9VaSSxTUomVwAjxdY)q}5jzL&ToN|0V_;fESUGFWQ$wdkzPq7mpwm;JCXkGfcQkPLeB2Lllp zW0)8jSv*R+nqGDvU&7e-2zoM)b^U#Hj@U8J416B<2Dmlc>ZGE%YD~zfy^4yQh>F_HyY9Q7wh5s0tG)u4P2X=_)2X2) zHwH~O^dtD_p}`N-*fF;I2!xv$;kkmn{?bIyE$V!v^|VRwaXRaYBhu;Yh?31ymxDVM)y2vBSr1|>BHJpj{2a%lw`~FtF!mmt3n;RdEQZid zfWJ8b<5NXlFgM}9{Lj@KEc5u<(~D+`dB4K+{^8#W8R!D>_hQ%CGu3bXV(VYo+7UMRAyjSzS6Ff;r_(mX#G5& z35@(1mF-JoT=^ae)2y?3XP_MqS4*sEu%j9TnVA9;@vS7t82PeyJh0zYSY4Q8uM z0eR5C3wq1*ice;sNQv`Y6lhP7;>aKz3jeS_Z9XN-yw&B~WnlU9&Y~iRn8Q*GWQXhj zm2fpDg7g3Tr8A~fZSWiQM+PYqMw-rFw2^hSp)Qf^iy>5DR5u1Op$UXS3Sa;+N)Iyqw>e=j&vM_ z@^&7REju1_tzK1bqLwqC>p5a57|S7jx4T#Gghm9s1Z>Sd^nqHTwX4ur=5hxaFR^sZ z+vO1}a0@4*Ys^_n3ruE}4B$^m$=a~;g{!O#5}7sAfu zN)QpIt$ZsjLh@=T7x8~l*AayDQ~;ObBoLq>s?x5bjlx!|L z>hj5Fd1>ZsfO04{23!D8nfu*doZUnwPiSF4ITsMZ2|^7r^VXiTgJ43VCT;a4G~y-T zsR24I$wyhEv?Qs}CxQ309yIF5C1HjIJ3^pPjA2Kkur%WseYNUvVC2*~z7lL0K2~22 zy5DyVW-E^Q_XqI;hv5`VHN4wBK}vbg0d}Dgqjm1`}GN=a>Da zxOZyJmR5{w^7_EOxoWS$>yNe@fZ-5?#& zY7b=1rtr`q$^|L*Bska&Oa&VSS03vdVO2O{!*(OotcknJ zKm>$Nh^U>asosi%#G;((W3G1#2Qac(-Vr(uk zM;#)*4~;`zGN_i^{f1QEQXF`aj(}X_9aR!xITk7AEy`SbN6fero))g_gbGUgb-xFc zH@ko9PCSqCkF4b=hJcEBYkj0q zDP*^22}`bb-ifjFeJ-7&IQ&2zP_pqxL$m^Ej}Vmo%5jgIwPamPJAgQ;c19eg24{M9zVF5x@51<@7;pm?P`u_$I zvL{mC(JX_4g7=_;9nSTWH5nZ!Lyl^r2o9MEdkG!7SdY|TRORv-lpbqeR_o4N6uDzr zP2o?KDvWI>;dih6B|Yb=wkpzIQ^N)+2kuH>7o{|niE&GsQTo$t?x7Cmb*rA^H!@F} zu?p%*?(0b&i=QKuaWUd<1QR628~=n5dJLr1+!oz4_Y+V2_6_y@KhFgap)fuqMr3v*y9~(Cy>aSd zO~N?*Z7Mj(KMO7!Q(&xC9jeII+o(keheBLc5^q_(LeEI zWc)@rl+vGhS){@-W)ib`dug4c7f&EL?CZ`xDf6N4#ugq-4SjK068w}(=rLuzMjKa zfk$H;myri(I9A^*sV+oksk5Q1?wJ!;3b59(^~@{27K@5CZBS)~W^5om^@2faz3Z`P z?t&Z(b;PD{zqK4S_*>Q@@ek;=8TJ4wP?LMLuT$vpkU;yXFTlvt?_N2o8!z1NlG%0N zQnm)fUw{55JN$Aix!qvz7aQJ(S`9d%mIDI#Tfe zd`5#V2hLf{|MAbT{o7FdRb0&)oGU7zK@VF14vzi~k z&+lln@`3-~$3IWXaJ;GxKMO;5M!KJDw55FmUTTcF`2?w8eYCj4o%gQG*pHMKDsr2r z@ZAC3-L-h}7Kh(-r7W5HFDKy$Z`8t^)#rfU}cPqs-udcGiGn zs~#zgbx%AYqt1j)mTN?vpT28DL(sGJBi>ky)Zh$vCh zZ4`?2mM!?EEVAszU+vu;xvDiD?6wkFboRmA0@iq%Ob)R|0r3UHoVHs&*O(-uG7M!E z?0dR~fdt_vklCC`uld|UjUDGA>oi}z7ZLnkYmqn*uzWPBIN5P#4%)1c;c@iPhOU1V zR?|KMIt7~~iE=Bg%cuC@mk*@8vE9!R1LsrE_0-9r*P^y}N=(0?Om%g?+4x|%q%?sVGE4H}5Ukh{z9G;Gv zDbi-M_jgF{6v}S|Tx5$&EvgB~0t9mZvFpYzF!%Gl_mdkN(k)z^X$hJnxe2`@1&3=Y zv{T&2VM75vTU zl1qwb$5roEyyV5Qtrhb;Na?QU=%2GXUZ|MZZ@`>+2-`~dZ*SSaVqdSe$~?o#jYpZJ zzA5WH2S9Szq+fmsI)h*beEoxd<)FJxsWE&1L&6e3Qn+@|j1CnU3Gyy|O0mtF&p!b# zh)$uEv+lvTh!9O~If*hd&-^#qp-uue7Iic*Ng4a?-3#TD1zEV@v|D0n*IuR0H=cQS znT+H*sw$vcv*CIV{TJQ!#E-0SoD;Het~GJ$P%Hc4_kR$XO*Ly(KQr#24T_kj%`^Wd zjnc)_g0i92B^nG&KFHSt>B(Rs%3H%@Npi|7NV)0s(4iY~^0McA@oBt?$i!C%?KS^a zR{4$QTo-Hc7Sr$xp({6{(`7x$drO5PzhUn9Vh`$P+d|O(Xh;rCOwps2p?xSi`2?&! zLQ`A24`5IKomIndV`Wxo+6prlZ51@-hq0SRXhZ~yurr)9ABPVI4M0O53z zVG*~1FZ{L?D>{wqqaAI%XSQ5*H&7T@&g5+2;YZ=-<~k#oLfXg{eHjyUTlre~9$SBo z_qc?fwtB=MD(#0^Qx-!D?CjNF*z#f@+V{6gMA8{Et zh_n<{zt1RswML)lDEbBGInTD*wc&zx+mH=F_EPnpkA|gocBd;qJ65$HnCyXln3DVw zeYwhq^4W?Yr-{+VM=MmA#;_a|ugz6YvH*kK!f~9g@3Js0vfZpX+KE#&CG#i$CC>9h z1L*99G=f6H^>Pp-kqiY=9hT0k!C9BT6r05L;NtNmeR=HY^EkX8Ae202nlHo&>g3-_ z`w1+l*=kXTob}N9SRS3xF4o*Qj{M?JzVH#!E%zdUjIkt#e2O0$X$ok7S40+IGxd$S z)+aNMZs0;2B;Q3-`%ue?75|3AntMgG^|yzA-!e?v*Sr}g0qHps-|@c7@U4koeq=u^ zKX3GJ;?-!4_#y8ZQYOOLQdEmf2D7A1%RDG};M^a&?v=f+`J)u~nLSM=yj>&V_K#!J zG=&lvdef`gs_UHVzGlFapGRNgr@#8p-RkG$D@a3 zcT%c!RLdeJxXZq)?FwpDj84nnj4&f}&-&Z5(O-22Hq&g&H-k5N*@f^YHZ^9jx**jl z0>WVuMtsFeU?iv5>@rncmOk$VKIf2!6Scy5nvs-S_TOs+h=`=C6YGs4VU7GMmDpyx zPerH#=TfiZ!_KY{wsA1ft8s0dGaz?D9T{#csyIwVIk+zR1|RT!d86{Lr4AqIr{ZN_ zwEpeguV?f~4(;oZbl#2fM+qp_O?VMavUN!Elk7}}`=GyK4E#=fA<>_Iw`{y$qDMf2 zi~25gfA!V2mnCjcJo=~^;*Fcd&zZR@VJgc6twQ#kqPfJ5s%G3+T)r3QHXH(fp$kNz zPSws)_p+Nn`bSygAsnd?tF8!<5YiP-fQX#e`@Nug)~FFu6~#ph%c#WM!DC_(CE1w6 z3Ld3n%5eVl+RM!)c`j#~8rr2=OIZ@vliFFe^$Cd*9fmR;%h<(b6*+e1lGa|$i7_~m zh$eF5@*GJoluGfd$!l0Yv`puXyFLlO8QVzsrvx?j?upy}v+Fr`vds)dh4S|XXnWcP z4k?Z;%GmwxKRr2eoBElb;jdq@>vwvvv@9U^@Y9-d`TQjkEBYaz2dVg zPU}y|%pn>(`%FnslvCPgw^-fiyj$3X9vt4T(CfFM(j+6=1BZDxv;Y?KLn=#cjmoQ1 z;erR=(wl3<>YC(eT0vB5GaWY_=IH$!;<7SBa$-L{bvt0{8gm#eMJcv*@?7V>H#t!V zG8jU;UP~J4QC_3tOjlTT{W@UO$oNf^;HgMJ?vb|03^CSpBtB_BV|P>Y6pj(Ir?FnK zVgDeiC=E&Vk}hGU@j00k9WiDfg8jYsSSmNWQ@wDY1KPd2Z zAc$d$y@V`yMhZI%-MP2@RVgM55`DGKWZnj!8zMLz%v7y@o1bR=3RjSieltRIr~t_k zJuCOxT)9h)S$c81E=d8>CYtT6JnF%V?#JM`j=yy4{KHy|dxi?7 zC0!86CSaN-O?~{ip5?Y}ow5?oBJJj_%Q$u@Q}oMyNB;T2@RFrpAa4pC*jUM~{1xBPX-HtO25ysckOHhl>h`Pe5Pms)BF8?O!NFB;Ylv z*W|q^+cKov7_ZPtnltP`N|44^xRM{lXPZ+cwbJ)PL~1jK-!N%>fox0-C>)XbB@VAK zKbd;~;W-$~=D`!X&JfFvY#Vz)hly9AtmaUQ+@yZ(Lay2kiQnEO@;&7x7VP^mUWFE) z|A}{%^F}lbqUj|oB4Xv{PIym%e^}XRZX#V=kI(HGXeU2M6%ry@c!}M=(!-PXJJm_~ z*4}YXxNifI!RxnpY$GwrxP_-CFFC?Hs1MH%7cwF$27X;I{tTCPsk{7nPj4{hik95g z6PJWpZ}J1wzVdA%2(mV*7m=WRxl)VGYO1ohwktAyq{>z|%deZ*vzo|7k=QA^vRt^>!%*}JTK4SYU3&AsY}f2j@r9o^^PzB+0hkgudmUa zkq-_e&1Pc>Bv1+ma{-NMyj~Iv$BtXyB9OsR|BgkgXn1?FRkEiiHh)E=5V|G49lO83 z9PMnRWTQaPm~_-3I#I1gTvrkGbxT8T#}hv09c&b=P17?(oFXjwdGPHx$xPbN=VeDR zxs11X6}W%;*Jiyrz9-4&=8N-rzyvg!QX{OK412b;jry6g;31ZEoRyg+QMc2JphK8Z z-LTT>srW9UKEFsx=EY4dazj85;q}Y{(h!Cl9WcOp?ATB zTeTxHc^7zp7MNpg(CKZx*5KB?Y^AzD4>lMVHU=>}N=9}#V?X<5P7k863f{Si@mNY_ zNL5iOAZ6vF0l7X=UYKLuT+ZAA{J>Yj$yt3B(QinA@j*1WO#gh-QMJM@JRU1o$?6_& zGv#my8&$4g#35R)%awhJKLP1|(02NTh+Ojf!^I|5b|HxO&7bj?oaFe5dn$TBhdXLo zXck>*IX^h9;J>h6`U&gFri4}kUD)vmmoeIRx}D;` zzIXPKYLCnweM<&O%T!0g&?8LBE<@)(RE%eT*_>)V#I+b@0*a**^ru~R|LspVuUudj z(CeK*(Je7$=VW%&gm`VA*5|R{{uZwvK^`l^d#ePdHc4sLIcY`@l?0T>?;4 zY0f%XVQZP(p-;X%_^0(!(o}73(;=`Q4y!VdBFw|AynDd*#3&rJ#hj%4IoEl{L9E>9%5DUxKs4fLRv*|Z_Vc?BaqbNRb)Yp7v(wp~p1_4f3(m(pub(crate) Vec); + +impl Default for MultOpt { + fn default() -> Self { + Self(Vec::new()) + } +} + + +impl std::fmt::Display for MultOpt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl FromStr for MultOpt { + type Err = io::Error; + + fn from_str(src: &str) -> Result { + if src == "[]" { + Ok(MultOpt::default()) + } else { + let v: Vec = src.split(",").map(|v| v.to_string()).collect(); + Ok(MultOpt(v)) + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ae11e77 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,206 @@ +use std::io::{self, Write, BufRead}; +use std::collections::HashMap; +use termcolor::{BufferWriter, WriteColor, ColorChoice, Color, ColorSpec}; +use anyhow::Result; +use serde_json::Value; +use structopt::StructOpt; + +mod parser; +mod cli; + +use cli::*; + +#[derive(Debug, StructOpt)] +#[structopt(name = "jaxe", about = "A j[son] [pick]axe!")] +pub(crate) struct Opt { + /// Fields to extract, default to extracting all fields + #[structopt(short, long, default_value)] + extract: MultOpt, + + /// Fields to omit + #[structopt(short, long, default_value)] + omit: MultOpt, + + /// Do not print non-json lines + #[structopt(short = "j", long)] + no_omit_json: bool, + + /// Filter by. See parse language + #[structopt(short = "f", long)] + filter: Vec, + + /// level keys. The first of these keys in the json line will be used as the level of the log line and formatted at the start of the line. + #[structopt(short, long)] + level: Vec, + + /// Time keys. The first of these keys in the json line will be used as the date of the log line and formatted after the level. + #[structopt(short, long)] + time: Vec, + + /// Disable colors + #[structopt(short, long)] + no_colors: bool, +} + +fn level_to_color(level: &str) -> Color { + match level { + "TRACE" => Color::Magenta, + "DEBUG" => Color::Blue, + "INFO" => Color::Green, + "WARN" => Color::Yellow, + "ERROR" => Color::Red, + _ => Color::Red + } +} + +fn run_filters(filters: &Vec, line: &Value) -> Result { + for filter in filters.iter() { + let res = parser::filter(&filter, line)?; + + if ! res { + log::debug!("Line ignored, it does not match filter {}", filter); + return Ok(false) + } + } + + Ok(true) +} + +fn write_formatted_line(opts: &Opt, line: Value, output: &termcolor::BufferWriter) -> Result<()> { + if ! run_filters(&opts.filter, &line)? { + return Ok(()) + } + + let mut json = serde_json::from_value::>(line)?; + + for key in opts.omit.0.iter() { + log::debug!("Not writing key {} due to --omit", key); + json.remove(key); + } + + let mut buffer = output.buffer(); + + for key in &opts.level { + if let Some(level) = json.get(key).and_then(|s| s.as_str()) { + buffer.set_color(ColorSpec::new().set_fg(Some(level_to_color(level))))?; + write!(&mut buffer, "{}", level.chars().nth(0).unwrap_or('?'))?; + buffer.set_color(ColorSpec::new().set_fg(None))?; + write!(&mut buffer, "|")?; + json.remove(key); + + break; + } + } + + for key in &opts.time { + if let Some(at) = json.get(key).and_then(|s| s.as_str()) { + buffer.set_color(ColorSpec::new().set_fg(None))?; + write!(&mut buffer, "{}|", at)?; + json.remove(key); + break; + } + } + + let mut keys: Vec<&String> = json.keys().collect(); + keys.sort(); + + for key in keys { + if ! opts.extract.0.is_empty() && ! opts.extract.0.contains(key) { + log::debug!("Not writing key {} due to --extract", key); + continue; + } + + let value: &Value = json.get(key).unwrap(); + buffer.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?; + + write!(&mut buffer, "{}", key)?; + + if let Some(n) = value.as_str().and_then(|s| s.parse::().ok()) { + buffer.set_color(ColorSpec::new().set_fg(None).set_dimmed(true))?; + write!(&mut buffer, "=")?; + buffer.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_dimmed(true))?; + write!(&mut buffer, "{} ", n)?; + } else if let Some(s) = value.as_str() { + buffer.set_color(ColorSpec::new().set_fg(None).set_dimmed(true))?; + write!(&mut buffer, "=")?; + buffer.set_color(ColorSpec::new().set_fg(None).set_dimmed(false))?; + write!(&mut buffer, "{} ", s)?; + } else { + buffer.set_color(ColorSpec::new().set_fg(None).set_dimmed(true))?; + write!(&mut buffer, "=")?; + buffer.set_color(ColorSpec::new().set_fg(None))?; + write!(&mut buffer, "{} ", value)?; + } + } + + writeln!(&mut buffer, "")?; + output.print(&buffer)?; + + Ok(()) +} + + +fn main() -> io::Result<()> { + pretty_env_logger::init(); + + let mut opts = Opt::from_args(); + + if opts.time.is_empty() { + opts.time.push("time".to_owned()); + opts.time.push("at".to_owned()); + } + + if opts.level.is_empty() { + opts.level.push("level".to_owned()); + } + + if let Some(e) = std::env::var("JAXE_OMIT").ok() { + opts.omit = MultOpt(e.split(",").map(|s| s.to_owned()).collect()); + } + + if let Some(e) = std::env::var("JAXE_FILTER").ok() { + opts.filter = vec![e.to_owned()]; + } + + let mut line_buffer = String::new(); + let stdin = io::stdin(); + let mut handle = stdin.lock(); + + let bufwtr = if opts.no_colors { + BufferWriter::stdout(ColorChoice::Never) + } else { + BufferWriter::stdout(ColorChoice::Auto) + }; + + loop { + match handle.read_line(&mut line_buffer) { + Err(_) | Ok(0) => { + log::debug!("Finished"); + break; + }, + Ok(c) => + log::debug!("read {} bytes", c) + } + + match serde_json::from_str(&line_buffer) { + Ok(json) => + write_formatted_line(&opts, json, &bufwtr).unwrap(), + Err(err) => { + log::debug!("Could not parse line as json: {:?}", err); + + if ! opts.no_omit_json { + let mut obuf = bufwtr.buffer(); + obuf.set_color(ColorSpec::new().set_fg(Some(Color::White)).set_dimmed(true))?; + + write!(&mut obuf, "{}", line_buffer)?; + + bufwtr.print(&mut obuf)?; + } + } + } + + line_buffer.clear() + } + + Ok(()) +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..513163a --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,623 @@ +use nom::{IResult, AsChar, branch}; + +use nom::bytes::complete::tag; +use nom::character::complete::{multispace0, char}; +use nom::multi::separated_list1; +use nom::sequence::{tuple, delimited, separated_pair}; +use serde_json::Value; +use nom::InputTakeAtPosition; +use anyhow::Result; +use anyhow::anyhow; +use anyhow::bail; + +use nom_locate::LocatedSpan; + +type Span<'a> = LocatedSpan<&'a str>; + +fn path_segment(input: Span) -> IResult { + let (rest, v) = input.split_at_position_complete(|item| ! item.is_alphanum() && item != '_' && item != '-')?; + Ok((rest, v.to_string())) +} + +fn path(input: Span) -> IResult { + let (rest, matched) = separated_list1(tag("."), path_segment)(input)?; + Ok((rest, EPath(matched))) +} + +fn unquoted_value(input: Span) -> IResult { + let (rest, v) = input.split_at_position_complete(|item| item.is_whitespace() || item == ',' || item == ')' || item == '"' )?; + Ok((rest, v.fragment())) +} + +fn end_quoted_string(input: Span) -> IResult { + let (rest, v) = input.split_at_position_complete(|item| item == '"' )?; + Ok((rest, v.fragment())) +} + +// TODO: This is not right... Would need to parse escaped chars etc +fn value(input: Span) -> IResult { + branch::alt(( + delimited(char('"'), end_quoted_string, char('"')), + unquoted_value, + ))(input) +} + +fn operation_equals(input: Span) -> IResult { + let (input, (path, value)) = separated_pair(path, tuple((multispace0, tag("=="), multispace0)), value)(input)?; + Ok((input, Exp::Equals(path, value.to_owned()))) +} + +fn operation_not_equals(input: Span) -> IResult { + let (input, (path, value)) = separated_pair(path, tuple((multispace0, tag("!="), multispace0)), value)(input)?; + Ok((input, Exp::NotEquals(path, value.to_string()))) +} + +fn operation(input: Span) -> IResult { + operation_not_equals(input).or(operation_equals(input)) +} + +fn exists(input: Span) -> IResult { + let (rest, (_, path)) = tuple((tag("exists"), delimited(tag("("), path, tag(")"))))(input)?; + Ok((rest, Exp::Exists(path))) +} + +fn exp(input: Span) -> IResult { + branch::alt((or, and, not, contains, exists, operation))(input) +} + +fn comma(input: Span) -> IResult { + let (res, _) = tuple((multispace0, tag(","), multispace0))(input)?; + Ok((res, ())) +} + +fn not(input: Span) -> IResult { + let (rest, (_, _, exp)) = tuple((tag("not"), multispace0, delimited(tag("("), exp, tag(")"))))(input)?; + Ok((rest, Exp::Not(exp.into()))) +} + +fn and(input: Span) -> IResult { + let (input, _) = tag("and")(input)?; + let (input, exps) = delimited(tag("("), separated_list1(comma, exp), tag(")"))(input)?; + Ok((input, Exp::And(exps))) +} + +fn or(input: Span) -> IResult { + let (input, _) = tag("or")(input)?; + let (input, exps) = delimited(tag("("), separated_list1(comma, exp), tag(")"))(input)?; + Ok((input, Exp::Or(exps))) +} + +fn contains(input: Span) -> IResult { + let (input, _) = tag("contains")(input)?; + let (input, (path, val)) = delimited(tag("("), separated_pair(path, comma, value), tag(")"))(input)?; + Ok((input, Exp::Contains(path, val.into()))) +} + +pub fn parse(input: &str) -> Result { + let input = Span::new(input); + let (rest, op) = exp(input).map_err(|err| anyhow!("Could not parse filter: {}", err))?; + + if rest.len() != 0 { + bail!("Could not parse the complete filter: {}, left over: {}", input, rest) + } + + Ok(op) +} + +pub fn filter(input: &str, target: &Value) -> Result { + let exp = parse(input)?; + let evalued = eval(&exp, target); + let as_bool = evalued.as_bool().unwrap_or(false); + Ok(as_bool) +} + +#[derive(Debug, PartialEq)] +pub struct EPath(Vec); + +#[derive(Debug, PartialEq)] +pub enum Exp { + Equals(EPath, String), + NotEquals(EPath, String), + Exists(EPath), + Not(Box), + And(Vec), + Or(Vec), + Contains(EPath, String) +} + +fn descend_to<'a>(path: &EPath, target: &'a Value) -> Option<&'a Value> { + let mut pointer = path.0.join("/"); + pointer.insert_str(0, "/"); + target.pointer(&pointer) +} + +fn string_value(value: &Value) -> Option { + match value { + Value::Bool(b) => + Some(b.to_string()), + Value::String(s) => + Some(s.to_string()), + Value::Number(n) => + Some(n.to_string()), + _ => + None + } +} + +pub fn eval_equals<'a>(path: &EPath, value: &str, target: &'a Value) -> &'a Value { + let path_value = descend_to(path, target).unwrap_or(&Value::Null); + + if let Some(p) = string_value(path_value) { + if p == value { + &Value::Bool(true) + } else { + &Value::Bool(false) + } + } else { + &Value::Bool(false) + } +} + +pub fn eval_not_equals<'a>(path: &EPath, value: &str, target: &'a Value) -> &'a Value { + if *eval_equals(path, value, target) == Value::Bool(true) { + &Value::Bool(false) + } else { + &Value::Bool(true) + } +} + +pub fn eval_exists<'a>(path: &EPath, target: &'a Value) -> &'a Value { + let v = descend_to(path, target); + + if v.is_some() { + &Value::Bool(true) + } else { + &Value::Bool(false) + } +} + +fn eval_not<'a>(exp: &Exp, target: &'a Value) -> &'a Value { + let new_val = eval(exp, target); + if new_val.as_bool().unwrap_or(false) == true { + &Value::Bool(false) + } else { + &Value::Bool(true) + } +} + +fn eval_and<'a>(conditions: &Vec, target: &'a Value) -> &'a Value { + for cond in conditions { + let left_val = eval(cond, target); + + if left_val.as_bool().unwrap_or(false) == false { + return &Value::Bool(false) + } + }; + + &Value::Bool(true) +} + +fn eval_or<'a>(conditions: &Vec, target: &'a Value) -> &'a Value { + for cond in conditions { + let left_val = eval(cond, target); + + if left_val.as_bool().unwrap_or(false) { + return &Value::Bool(true) + } + } + + &Value::Bool(false) +} + +fn eval_contains<'a>(path: &EPath, val: &String, target: &'a Value) -> &'a Value { + let path_value = descend_to(path, target).unwrap_or(&Value::Null); + + if let Some(p) = string_value(path_value) { + if p.contains(val) { + &Value::Bool(true) + } else { + &Value::Bool(false) + } + } else { + &Value::Bool(false) + } +} + +pub fn eval<'a>(exp: &Exp, target: &'a Value) -> &'a Value { + match exp { + Exp::Contains(ref path, ref val) => + eval_contains(path, val, target), + Exp::Or(ref conditions) => + eval_or(conditions, target), + Exp::And(ref conditions) => + eval_and(conditions, target), + Exp::Not(ref exp) => { + eval_not(exp, target) + }, + Exp::Exists(ref path) => + eval_exists(path, target), + Exp::Equals(path, value) => + eval_equals(&path, value, target), + Exp::NotEquals(path, value) => + eval_not_equals(&path, value, target) + } +} + +#[cfg(test)] +mod test { + + use serde_json::json; + use super::*; + + #[test] + fn pathsegment_test() { + let i = LocatedSpan::from("mykey0"); + + let v = super::path_segment(i).unwrap(); + + assert_eq!(v.0.len(), 0); + assert_eq!(v.1, "mykey0"); + } + + #[test] + fn path_test() { + let i = LocatedSpan::from("mykey_0.otherkey1"); + + let (rest, m) = super::path(i).unwrap(); + + assert!(rest.is_empty()); + assert_eq!(m.0, vec!["mykey_0", "otherkey1"]); + } + + #[test] + fn operation_equals_test() { + let i = LocatedSpan::from("mykey0.otherkey1 == myval"); + + let (_, m) = operation_equals(i).unwrap(); + + if let Exp::Equals(path, value) = m { + assert_eq!(path, EPath(vec!["mykey0".to_string(), "otherkey1".to_string()])); + + assert_eq!(value, "myval".to_string()); + } else { + panic!("Could not match: {:?}", m) + } + } + + #[test] + fn operation_not_equals_test() { + let i = LocatedSpan::from("mykey0.otherkey1 != myval"); + + let (_, m) = super::operation_not_equals(i).unwrap(); + + if let Exp::NotEquals(path, value) = m { + + assert_eq!(path, super::EPath(vec!["mykey0".to_string(), "otherkey1".to_string()])); + + assert_eq!(value, "myval".to_string()); + } else { + panic!("Could not match: {:?}", m) + } + } + + #[test] + fn exp_test() { + let i = "mykey0.otherkey1 == myval"; + + let m = super::parse(i).unwrap(); + + if let Exp::Equals(path, value) = m { + assert_eq!(path, super::EPath(vec!["mykey0".to_string(), "otherkey1".to_string()])); + + assert_eq!(value, "myval".to_string()); + } else { + panic!("Could not match: {:?}", m) + } + } + + #[test] + fn exp_not_equals_test() { + let i = "mykey0.otherkey1 != myval"; + + let m = super::parse(i).unwrap(); + + if let super::Exp::NotEquals(path, value) = m { + assert_eq!(path, super::EPath(vec!["mykey0".to_string(), "otherkey1".to_string()])); + + assert_eq!(value, "myval".to_string()); + } else { + panic!("Could not match: {:?}", m) + } + } + + + #[test] + fn eval_equals_test() { + let payload = json!({ + "mykey0": { + "otherkey1": "myval" + } + }); + + let i = "mykey0.otherkey1 == myval"; + let m = super::parse(i).unwrap(); + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)) + } + + #[test] + fn eval_equals_not_str_test() { + let payload = json!({ + "mykey0": { + "otherkey1": 1 + } + }); + + let i = "mykey0.otherkey1 == 1"; + let m = super::parse(i).unwrap(); + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)) + } + + #[test] + fn eval_equals_number_test() { + let payload = json!({ + "mykey0": { + "otherkey1": 1 + } + }); + + let i = "mykey0.otherkey1 == 1"; + let m = super::parse(i).unwrap(); + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)) + } + + #[test] + fn eval_bool_test() { + let i = "mykey0.otherkey1 == true"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "otherkey1": true + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn eval_equals_no_found_test() { + let i = "mykey0.otherkey1 == myval"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": "myval" + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(false)); + } + + #[test] + fn exists_parse_test() { + let i = "exists(mykey0.mykey1)"; + let m = super::parse(i).unwrap(); + + assert_eq!(m, Exp::Exists(EPath(vec!["mykey0".into(), "mykey1".into()]))); + } + + #[test] + fn exists_eval_test() { + let i = "exists(mykey0.randomkey)"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": "myval" + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn not_equals_eval_test() { + let i = "not(mykey0.randomkey == myval)"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": "myval" + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(false)); + + let i = "not(mykey0.randomkey == myval0)"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": "myval" + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn and_eval_test() { + let i = "and(not(mykey0.randomkey == something), mykey0.randomkey != somethingelse)"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": "myval" + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn triple_and_eval_test() { + let i = "and(not(mykey0.randomkey == something), mykey0.randomkey != somethingelse, mykey0.randomkey == myval)"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": "myval" + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn or_eval_test() { + let i = "or(mykey0.randomkey == something, mykey0.randomkey == myval0)"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": "myval" + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(false)); + } + + #[test] + fn triple_or_eval_test() { + let i = "or(mykey0.randomkey == something, mykey0.randomkey == myval0, mykey0.randomkey == myval)"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": "myval" + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn contains_test() { + let i = "contains(mykey0.randomkey, my)"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": "myval" + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + + let i = "contains(mykey0.randomkey, somethingelse)"; + let m = super::parse(i).unwrap(); + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(false)); + } + + #[test] + fn contains_int_test() { + let i = "contains(mykey0.randomkey, 11)"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": 1111 + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn not_contains_int_test() { + let i = "not(contains(mykey0.randomkey, 2))"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey0": { + "randomkey": 1111 + } + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn index_arrays_test() { + let i = "mykey.0 == myval1"; + let m = super::parse(i).unwrap(); + + let payload = json!({ + "mykey": ["myval1", "myval2"] + }); + + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn array_path_exists_test() { + let payload = json!({ + "mykey": ["myval1", "myval2"] + }); + + let i = "exists(mykey.0)"; + let m = super::parse(i).unwrap(); + let res = super::eval(&m, &payload); + assert_eq!(*res, serde_json::Value::Bool(true)); + } + + #[test] + fn filter_test() { + let payload = json!({ + "mykey0": 1 + }); + + let i = "contains(mykey0, 1)"; + let res = filter(i, &payload).unwrap(); + assert_eq!(res, true); + } + + #[test] + fn contains_string_non_alphanumeric_test() { + let payload = json!({ + "mykey": "myval1.something-else" + }); + + let i = "contains(mykey, myval1.something-el)"; + let res = filter(i, &payload).unwrap(); + assert_eq!(res, true); + } + + #[test] + fn quoted_values_test() { + let payload = json!({ + "mykey": "myval1.some()thing-else" + }); + + let i = "contains(mykey, \"some()\")"; + let res = filter(i, &payload).unwrap(); + assert_eq!(res, true); + } +}