diff options
author | Guillaume Pasquet <dev@etenil.net> | 2022-03-26 06:22:34 +0000 |
---|---|---|
committer | Guillaume Pasquet <dev@etenil.net> | 2022-03-26 06:22:34 +0000 |
commit | 560b78ec1706bd166b146e49f5d93940641fe69e (patch) | |
tree | ca4b87529858d9d9cf3f15dbd42f2ff5ccabc1b8 | |
parent | 4d76e3d4c852967430b30cff4f6567cb8d7e9235 (diff) | |
parent | 08339229db6da6e76726162ba325a625b558cfb6 (diff) |
Merge branch 'feature/16-dependencies' into 'main'
Feature/16 dependencies
Closes #16
See merge request guillaume54/barb!3
-rw-r--r-- | README.md | 80 | ||||
-rw-r--r-- | src/barbfile.rs | 76 | ||||
-rw-r--r-- | src/executor.rs | 16 | ||||
-rw-r--r-- | src/main.rs | 45 | ||||
-rw-r--r-- | test_api/test_api.py | 14 | ||||
-rw-r--r-- | test_api/test_api_auth.barb | 7 | ||||
-rw-r--r-- | test_api/test_api_get_profile.barb | 3 | ||||
-rw-r--r-- | test_api/test_api_get_profile_wrong_dep.barb | 3 |
8 files changed, 220 insertions, 24 deletions
@@ -5,6 +5,31 @@ BARB Barb is a file-based API query tool that works nicely with version control and fits into UNIX terminal usage. + +<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc --> +**Table of Contents** + +- [BARB](#barb) + - [Installation](#installation) + - [Example usage](#example-usage) + - [CLI options](#cli-options) + - [Barb format](#barb-format) + - [Verb line](#verb-line) + - [Headers](#headers) + - [Filter](#filter) + - [JSONPath](#jsonpath) + - [JQ](#jq) + - [Dependencies](#dependencies) + - [Body](#body) + - [Variable substitution](#variable-substitution) + - [Placeholder format](#placeholder-format) + - [Default value](#default-value) + - [Credits](#credits) + +<!-- markdown-toc end --> + + + ## 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: @@ -120,6 +145,20 @@ Filters can be named to populate execution variables by extracting values. Consi #FOOBAR|<JQ FILTER> ``` +### Dependencies + +A barb file can declare only _one_ dependency which will be executed before the main file is executed. If multiple dependencies are declared, only the last one will be executed. + +Syntax: + +``` +#>relative/path/to/file.barb +``` + +The path to the dependency can either be relative to the current file or absolute. When running multiple barb files which have the same dependency, that dependency will only be executed _once_. + +A barb dependency _cannot have dependencies of its own_. Any dependency declared within a dependency will simply be ignored. + ### Body Anything after the preamble is considered as a body and will be send in the request for the following methods: @@ -134,9 +173,10 @@ Body does not support variable substitution. Barb can include environment variable values and variables defined in `.env` into the requests with the following placeholder format: +#### Placeholder format + ``` {VARIABLE NAME} -{VARIABLE NAME:-DEFAULT} ``` This allows to do the following: @@ -147,10 +187,40 @@ $ cat api-status.barb #GET^{BASE_URL}/api/v1/status $ barb api-status.barb -200 OK GET http://127.0.0.1:8000/api/v1/status -{ - "status": "OK" -} +GET http://127.0.0.1:8000/api/v1/status + +200 OK + +{"status": "OK"} +``` + +#### Default value + +A placholder can be given a default value that will be used if the environment variable is not available. The format is as follows: + +``` +{VARIABLE NAME:-DEFAULT} +``` + +Example: + +``` +$ cat api-ping.barb +#GET^http://{HOST:-foobar.com}/api/ping + +$ barb api-ping.barb +GET http://foobar.com/api/ping + +200 OK + +{"response": "pong"} + +$ HOST=bar.com barb api-ping.barb +GET http://bar.com/api/ping + +200 OK + +{"response": "pong"} ``` ## Credits diff --git a/src/barbfile.rs b/src/barbfile.rs index 858d035..54ee031 100644 --- a/src/barbfile.rs +++ b/src/barbfile.rs @@ -4,6 +4,10 @@ use serde_json::Value; use std::str::FromStr; use std::string::ToString; use std::{error::Error, fmt}; +use std::cmp::{Eq, PartialEq, PartialOrd, Ord, Ordering}; +use std::fs; +use std::path::Path; + #[cfg(feature = "jq")] use jq_rs; @@ -150,6 +154,7 @@ impl BarbFilter { &self.name } + #[cfg(test)] pub fn filter(&self) -> &String { &self.filter } @@ -201,22 +206,23 @@ struct BarbPreamble { pub url: String, pub headers: Vec<Header>, pub filters: Vec<BarbFilter>, - //pub filter: Option<BarbFilter>, + pub dependency: Option<String>, } impl BarbPreamble { - fn new(method: Method, url: String, headers: Vec<Header>, filters: Vec<BarbFilter>) -> Self { + fn new(method: Method, url: String, headers: Vec<Header>, filters: Vec<BarbFilter>, dependency: Option<String>) -> Self { BarbPreamble { method, url, headers, - //filter: Vec<BarbFilter>, filters, + dependency, } } } pub struct BarbFile { + file_name: String, preamble: BarbPreamble, body: Option<String>, } @@ -245,6 +251,38 @@ impl BarbFile { pub fn body(&self) -> &Option<String> { &self.body } + + pub fn dependency(&self) -> Option<String> { + let dep = self.preamble.dependency.as_ref()?; + let dep_path = Path::new(dep); + + if !dep_path.is_absolute() { + let my_path = Path::new(&self.file_name).parent().or(Some(Path::new("")))?; + return Some(String::from(my_path.join(dep_path).to_str()?)); + } + + Some(String::from(dep_path.to_str()?)) + } +} + +impl PartialEq for BarbFile { + fn eq(&self, other: &Self) -> bool { + self.file_name == other.file_name + } +} + +impl Eq for BarbFile {} + +impl PartialOrd for BarbFile { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for BarbFile { + fn cmp(&self, other: &Self) -> Ordering { + self.file_name.cmp(&other.file_name) + } } fn decode_url_line(line: &str) -> Result<(Method, String), BarbParseError> { @@ -264,6 +302,18 @@ fn decode_header(line: &str) -> Result<Header, BarbParseError> { }) } +impl BarbFile { + pub fn from_file(file_name: String) -> Result<Self, BarbParseError> { + let mut bfile = Self::from_str( + fs::read_to_string(file_name.as_str()) + .map_err(|_| BarbParseError {})? + .as_str() + ).map_err(|_| BarbParseError {})?; + bfile.file_name = file_name; + Ok(bfile) + } +} + impl FromStr for BarbFile { type Err = BarbParseError; @@ -272,6 +322,7 @@ impl FromStr for BarbFile { let (method, url) = decode_url_line(lines.next().ok_or(BarbParseError {})?)?; let mut headers: Vec<Header> = vec![]; let mut filters: Vec<BarbFilter> = vec![]; + let mut dependency: Option<String> = None; for line in &mut lines { if line == "" { @@ -283,6 +334,10 @@ impl FromStr for BarbFile { headers.push(decode_header(line).map_err(|_| BarbParseError {})?); } + if let Some('>') = line.chars().nth(1) { + dependency = line.get(2..).map(|x| String::from(x)); + } + if BarbFilter::is_match(String::from(line)) { match BarbFilter::from_str(line) { Ok(filter) => filters.push(filter), @@ -294,7 +349,8 @@ impl FromStr for BarbFile { let body = lines.fold(String::from(""), |acc, x| acc + x); Ok(BarbFile { - preamble: BarbPreamble::new(method, url, headers, filters), + file_name: String::from(""), + preamble: BarbPreamble::new(method, url, headers, filters, dependency), body: if body == "" { None } else { Some(body) }, }) } @@ -419,4 +475,16 @@ mod tests { let subject = String::from(r#"{"status": "OK"}"#); assert_eq!(path_f.apply(&subject).unwrap(), String::from("OK")); } + + #[test] + fn test_parse_dependency() { + let bfile = BarbFile::from_str("#GET^http://test.com\n#>blah.barb").unwrap(); + assert_eq!(bfile.dependency().as_ref().unwrap(), &String::from("blah.barb")); + } + + #[test] + fn test_parse_mult_dependency_keeps_last() { + let bfile = BarbFile::from_str("#GET^http://test.com\n#>blah.barb\n#>foo.barb\n#>bar.barb").unwrap(); + assert_eq!(bfile.dependency().as_ref().unwrap(), &String::from("bar.barb")); + } } diff --git a/src/executor.rs b/src/executor.rs index 763b0e6..0a6e855 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -2,8 +2,6 @@ use crate::barbfile::{BarbFile, BarbFilter}; use crate::output::BarbOutput; use regex::Regex; use std::collections::HashMap; -use std::fs; -use std::str::FromStr; use ureq; use ureq::Error as UreqError; @@ -16,10 +14,10 @@ impl Context { where I: Iterator<Item = (String, String)>, { - let mut toto = HashMap::new(); - toto.extend(vars); + let mut tmp_vars = HashMap::new(); + tmp_vars.extend(vars); - Context { vars: toto } + Context { vars: tmp_vars } } #[cfg(test)] @@ -129,16 +127,10 @@ impl Executor { pub fn execute( &mut self, - file_name: &String, + bfile: &BarbFile, output: &BarbOutput, filter: &Option<String>, ) -> Result<(), String> { - let bfile = BarbFile::from_str( - fs::read_to_string(file_name.as_str()) - .map_err(|_| format!("Failed to read file '{}'", file_name))? - .as_str(), - ) - .map_err(|_| format!("Failed to parse file '{}'", file_name))?; let response = self.run(&bfile, self.make_req(&bfile, output))?; //let response = executor.execute(&bfile, &output)?; diff --git a/src/main.rs b/src/main.rs index 6dfb7d3..0cd839c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod barbfile; mod executor; mod output; +use barbfile::BarbFile; use clap::Parser; use dotenv::dotenv; use executor::{Context, Executor}; @@ -47,14 +48,54 @@ impl Args { } } +fn read_file_barb(file_name: &String) -> Result<BarbFile, String> { + BarbFile::from_file(file_name.to_string()) + .map_err(|_| String::from(format!("Failed to parse file {}", file_name))) +} + fn main() { let args = Args::parse(); dotenv().ok(); let mut executor = Executor::new(Context::new(env::vars())); let output = args.output(); - for file in args.files_iter() { - match executor.execute(file, &output, args.jq_filter()) { + let files: Vec<Result<BarbFile, String>> = args.files_iter() + .map(read_file_barb) + .collect(); + + let (maybe_deps, errors): (Vec<Result<BarbFile, String>>, Vec<Result<BarbFile, String>>) = files.iter() + .map(|x| match x.as_ref().ok() { + Some(bfile) => bfile.dependency(), + None => None + }) + .filter(|x| x.is_some()) + .map(|x| read_file_barb(&String::from(x.unwrap()))) + .partition(|x| x.is_ok()); + + for e in errors { + println!("{}", e.err().unwrap()); + } + + let mut dependencies = maybe_deps.iter() + .map(|x| x.as_ref().unwrap()) + .collect::<Vec<&BarbFile>>(); + dependencies.sort(); + dependencies.dedup(); + + for dep in dependencies { + match executor.execute(&dep, &output, args.jq_filter()) { + Ok(()) => (), + Err(err) => println!("{}", err), + } + } + + for bfile in files { + if let Err(e) = bfile { + println!("{}", e); + continue; + } + + match executor.execute(&bfile.unwrap(), &output, args.jq_filter()) { Ok(()) => (), Err(err) => println!("{}", err), } diff --git a/test_api/test_api.py b/test_api/test_api.py index cce55f4..02eb491 100644 --- a/test_api/test_api.py +++ b/test_api/test_api.py @@ -1,4 +1,4 @@ -from bottle import get, post, run, HTTPError +from bottle import get, post, run, HTTPError, request @get("/") def get_root(): @@ -24,4 +24,16 @@ def error_404(): def error_500(): raise HTTPError(500, body={"error": "server error"}) +@post("/auth") +def post_auth(): + return {"token": "blah1234"} + +@get("/profile") +def get_profile(): + if request.headers.get("TOKEN") != "blah1234": + raise HTTPError(401, body={"error": "Not Authorized"}) + return { + "user": "John Doe" + } + run(host="localhost", port=8080) diff --git a/test_api/test_api_auth.barb b/test_api/test_api_auth.barb new file mode 100644 index 0000000..40da734 --- /dev/null +++ b/test_api/test_api_auth.barb @@ -0,0 +1,7 @@ +#POST^http://localhost:8080/auth +#TOKEN$$.token + +{ + "username": "john.doe", + "password": "foobar123" +}
\ No newline at end of file diff --git a/test_api/test_api_get_profile.barb b/test_api/test_api_get_profile.barb new file mode 100644 index 0000000..9c0811a --- /dev/null +++ b/test_api/test_api_get_profile.barb @@ -0,0 +1,3 @@ +#GET^http://localhost:8080/profile +#>test_api_auth.barb +#TOKEN: {TOKEN} diff --git a/test_api/test_api_get_profile_wrong_dep.barb b/test_api/test_api_get_profile_wrong_dep.barb new file mode 100644 index 0000000..f7ec2e7 --- /dev/null +++ b/test_api/test_api_get_profile_wrong_dep.barb @@ -0,0 +1,3 @@ +#GET^http://localhost:8080/profile +#>test_api_aut.barb +#TOKEN: {TOKEN} |