diff options
author | Guillaume Pasquet <dev@etenil.net> | 2022-03-03 23:18:08 +0000 |
---|---|---|
committer | Guillaume Pasquet <dev@etenil.net> | 2022-03-03 23:18:08 +0000 |
commit | 57af17cfa6e08a6fe9bf6d73d401af17dccfa3bb (patch) | |
tree | cd550ccf0eb78f360e6813ffdfd3c89d817eb21f | |
parent | f797f10c2eda28a221759fc26ddf483f41232604 (diff) |
Introduce JSONPath and potentially break old stuff.
-rw-r--r-- | Cargo.lock | 151 | ||||
-rw-r--r-- | Cargo.toml | 20 | ||||
-rw-r--r-- | README.md | 44 | ||||
-rw-r--r-- | src/barbfile.rs | 76 | ||||
-rw-r--r-- | src/executor.rs | 8 | ||||
-rw-r--r-- | src/main.rs | 3 | ||||
-rw-r--r-- | test_api/test_api_get_filter.barb | 2 | ||||
-rw-r--r-- | test_api/test_api_get_jsonpath.barb | 3 |
8 files changed, 280 insertions, 27 deletions
@@ -45,7 +45,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "barb" -version = "0.2.2" +version = "0.3.0" dependencies = [ "clap 3.0.14", "colored", @@ -53,6 +53,7 @@ dependencies = [ "dotenv", "jq-rs", "jsonformat", + "jsonpath-rust", "regex", "serde", "serde_json", @@ -72,12 +73,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] name = "bumpalo" version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] name = "cc" version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -174,12 +208,27 @@ dependencies = [ ] [[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array", +] + +[[package]] name = "dotenv" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] name = "flate2" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -202,6 +251,15 @@ dependencies = [ ] [[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -286,6 +344,18 @@ dependencies = [ ] [[package]] +name = "jsonpath-rust" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da86e332e6de16fa0e12388aeac5a295c522f9930c103cef43b2d17ccf0ba147" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", +] + +[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -307,6 +377,12 @@ dependencies = [ ] [[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] name = "matches" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -335,6 +411,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] name = "os_str_bytes" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -350,6 +432,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + +[[package]] name = "pkg-config" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -475,6 +600,18 @@ dependencies = [ ] [[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + +[[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -543,6 +680,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] name = "unicode-bidi" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1,6 +1,6 @@ [package] name = "barb" -version = "0.2.2" +version = "0.3.0" edition = "2021" license = "GPL-3.0-or-later" description = "Command-line tool to perform file-based HTTP(s) requests." @@ -14,11 +14,17 @@ categories = ["command-line-utilities","network-programming","web-programming"] [dependencies] ureq = { version = "2.3.1", features = ["json"] } clap = { version = "3.0.14", features = ["derive"] } +jq-rs = { version = "0.4", optional = true } serde = "1.0.130" -serde_json = "1.0.70" -jsonformat = "1.2.0" -jq-rs = "0.4.1" -colored = "2.0.0" -colored_json = "2.1.0" +serde_json = "1.0" +jsonformat = "1.2" +colored = "2.0" +colored_json = "2.1" dotenv = "0.15.0" -regex = "1.5"
\ No newline at end of file +regex = "1.5" +jsonpath-rust = "0.1" + + +[features] +default = [] +jq = ["jq-rs"]
\ No newline at end of file @@ -5,6 +5,20 @@ BARB Barb is a file-based API query tool that works nicely with version control and fits into UNIX terminal usage. +## Installation + +Barb is only available through `Cargo` at this time. To install the default version with JSONPath only, [install rust with rustup](https://rustup.rs).do like so: + +``` +cargo install barb +``` + +If you'd like to have JQ filtering; ensure the `libjq` is installed on your machine, then run: + +``` +cargo install barb --features jq +``` + ## Example usage ``` @@ -19,6 +33,8 @@ barb [options] <file 1> <file 2> ... <file n> - `-r, --raw`: Don't format the response body - `-V, --version`: Print the software version - `-n, --no-color`: Don't use color output +- `-f, --filter`: A JSON path to override any filters defined in the barb file +- `--help`: Displays the help page ## Barb format @@ -27,7 +43,7 @@ Barb uses a custom file format to perform requests. Each file contains _one_ req ``` #POST^http://my-blog.com/posts #Authorization: TOKEN {API_TOKEN} -#|jq.filter +#$$.filter { "title": "A post", @@ -70,6 +86,26 @@ There can be none or many headers. ### Filter +Barb supports [JSONPath](https://goessner.net/articles/JsonPath/) filtering by default, and optionally JQ. + +#### JSONPath + +Barb supports filtering of the response body with JSONPath. This has the following format: + +``` +#$<JSON path> +``` + +The `PATH` supports variable substitution. Refer to the [JSONPath](https://goessner.net/articles/JsonPath/) for more information on the filters and their syntax. + +Filters can be named to populate execution variables by extracting values. Consider the following that will set the value of variable FOOBAR: + +``` +#FOOBAR$<JSON path> +``` + +#### JQ + Barb supports JQ filtering of the response body. This has the following format: ``` @@ -78,6 +114,12 @@ Barb supports JQ filtering of the response body. This has the following format: The `JQ FILTER` supports variable substitution. Refer to the [JQ manual](https://stedolan.github.io/jq/manual/#Basicfilters) for more information on the filters and their syntax. +Filters can be named to populate execution variables by extracting values. Consider the following that will set the value of variable FOOBAR: + +``` +#FOOBAR|<JQ FILTER> +``` + ### Body Anything after the preamble is considered as a body and will be send in the request for the following methods: diff --git a/src/barbfile.rs b/src/barbfile.rs index 91ffd0a..f3cb58f 100644 --- a/src/barbfile.rs +++ b/src/barbfile.rs @@ -2,6 +2,11 @@ use regex::Regex; use std::str::FromStr; use std::string::ToString; use std::{error::Error, fmt}; + +use jsonpath_rust::JsonPathQuery; +use serde_json::{json, Value}; + +#[cfg(feature = "jq")] use jq_rs; #[derive(Debug)] @@ -86,16 +91,22 @@ impl Header { } } +enum FilterType { + JQ, + PATH, +} + pub struct BarbFilter { name: Option<String>, filter: String, + filter_type: FilterType, } impl FromStr for BarbFilter { type Err = BarbParseError; fn from_str(s: &str) -> Result<Self, Self::Err> { - let re = Regex::new("^#(?P<name>[A-Za-z0-9_]*)\\|(?P<filter>.+)$").unwrap(); + let re = Regex::new("^#(?P<name>[A-Za-z0-9_]*)(?P<type>[|$])(?P<filter>.+)$").unwrap(); let groups = re.captures(s).ok_or(BarbParseError {})?; Ok(BarbFilter::new( @@ -104,20 +115,36 @@ impl FromStr for BarbFilter { any => Some(String::from(any)), }, String::from(&groups["filter"]), + match &groups["type"] { + "|" => FilterType::JQ, + _ => FilterType::PATH, + }, )) } } impl PreambleLine for BarbFilter { fn is_match(s: String) -> bool { - let re = Regex::new("^#(?P<name>[A-Za-z0-9_]*)\\|(?P<filter>.+)$").unwrap(); + let re = Regex::new("^#(?P<name>[A-Za-z0-9_]*)[|$](?P<filter>.+)$").unwrap(); re.is_match(s.as_str()) } } impl BarbFilter { - pub fn new(name: Option<String>, filter: String) -> BarbFilter { - BarbFilter { name, filter } + fn new(name: Option<String>, filter: String, filter_type: FilterType) -> BarbFilter { + BarbFilter { + name, + filter, + filter_type, + } + } + + pub fn from_path(filter: String) -> BarbFilter { + BarbFilter { + name: None, + filter_type: FilterType::PATH, + filter, + } } pub fn name(&self) -> &Option<String> { @@ -128,11 +155,38 @@ impl BarbFilter { &self.filter } - pub fn apply(&self, body: &String) -> Result<String, String> { + #[cfg(feature = "jq")] + fn apply_jq(&self, body: &String) -> Result<String, String> { jq_rs::run(self.filter.as_str(), body.as_str()) .map_err(|x| x.to_string()) .map(|x| String::from(x.trim().trim_matches('"'))) } + + fn apply_path(&self, body: &String) -> Result<String, String> { + let json: Value = serde_json::from_str(body.as_str()) + .map_err(|_| String::from("Failed to decode body"))?; + let path = &json.path(self.filter.as_str())?; + Ok(match path { + Value::Array(val) => match val.len() { + 1 => val.first().unwrap(), + _ => path, + }, + _ => path, + } + .to_string() + .trim() + .trim_matches('"') + .to_string()) + } + + pub fn apply(&self, body: &String) -> Result<String, String> { + match self.filter_type { + #[cfg(feature = "jq")] + FilterType::JQ => self.apply_jq(body), + FilterType::PATH => self.apply_path(body), + _ => Ok(body.to_string()), + } + } } struct BarbPreamble { @@ -225,7 +279,7 @@ impl FromStr for BarbFile { if BarbFilter::is_match(String::from(line)) { match BarbFilter::from_str(line) { Ok(filter) => filters.push(filter), - Err(_) => () + Err(_) => (), }; } } @@ -277,7 +331,10 @@ mod tests { .unwrap(); assert!(matches!(barbfile.preamble.method, Method::GET)); assert_eq!(barbfile.preamble.url, "https://blah.com/api/blah"); - assert_eq!(barbfile.preamble.filters[0].filter(), &String::from("filtr")); + assert_eq!( + barbfile.preamble.filters[0].filter(), + &String::from("filtr") + ); assert_eq!(barbfile.preamble.headers.len(), 1); assert_eq!(barbfile.preamble.headers[0].name, "Authorization"); assert_eq!(barbfile.preamble.headers[0].value, "BLAH"); @@ -291,7 +348,10 @@ mod tests { .unwrap(); assert!(matches!(barbfile.preamble.method, Method::POST)); assert_eq!(barbfile.preamble.url, "https://blah.com/api/blah"); - assert_eq!(barbfile.preamble.filters[0].filter(), &String::from("filtr")); + assert_eq!( + barbfile.preamble.filters[0].filter(), + &String::from("filtr") + ); assert_eq!(barbfile.preamble.headers.len(), 1); assert_eq!(barbfile.preamble.headers[0].name, "Authorization"); assert_eq!(barbfile.preamble.headers[0].value, "BLAH"); diff --git a/src/executor.rs b/src/executor.rs index 15704c2..eccab39 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -89,7 +89,7 @@ impl Executor { arg_filter: &Option<String>, ) -> Result<String, String> { if let Some(filter) = arg_filter { - return Ok(BarbFilter::new(None, filter.to_string()).apply(&body)?); + return Ok(BarbFilter::from_path(filter.to_string()).apply(&body)?); } else if bfile.filters().len() > 0 { let mut end_body: String = body.clone(); for filter in bfile.filters().iter() { @@ -144,11 +144,7 @@ impl Executor { } output.end_resp_hdr(); - output.body(self.apply_filters( - &bfile, - response.into_string().unwrap(), - filter, - )?); + output.body(self.apply_filters(&bfile, response.into_string().unwrap(), filter)?); Ok(()) } diff --git a/src/main.rs b/src/main.rs index 2973b27..1738391 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,8 +27,6 @@ struct Args { #[clap(short, long)] filter: Option<String>, #[clap(short, long)] - path: Option<String>, - #[clap(short, long)] no_color: bool, files: Vec<String>, } @@ -62,7 +60,6 @@ fn main() { for file in args.files_iter() { match executor.execute(file, &output, args.jq_filter()) { - //match run_file(&args, &mut executor, file) { Ok(()) => (), Err(err) => println!("{}", err), } diff --git a/test_api/test_api_get_filter.barb b/test_api/test_api_get_filter.barb index 674ff15..83ba52f 100644 --- a/test_api/test_api_get_filter.barb +++ b/test_api/test_api_get_filter.barb @@ -1,3 +1,3 @@ #GET^http://localhost:8080/ #Foo: Bar -#BAR|.status +#|.status diff --git a/test_api/test_api_get_jsonpath.barb b/test_api/test_api_get_jsonpath.barb new file mode 100644 index 0000000..d4f323e --- /dev/null +++ b/test_api/test_api_get_jsonpath.barb @@ -0,0 +1,3 @@ +#GET^http://localhost:8080/ +#Foo: Bar +#$$.status |