aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuillaume Pasquet <dev@etenil.net>2022-03-26 06:22:34 +0000
committerGuillaume Pasquet <dev@etenil.net>2022-03-26 06:22:34 +0000
commit560b78ec1706bd166b146e49f5d93940641fe69e (patch)
treeca4b87529858d9d9cf3f15dbd42f2ff5ccabc1b8
parent4d76e3d4c852967430b30cff4f6567cb8d7e9235 (diff)
parent08339229db6da6e76726162ba325a625b558cfb6 (diff)
Merge branch 'feature/16-dependencies' into 'main'
Feature/16 dependencies Closes #16 See merge request guillaume54/barb!3
-rw-r--r--README.md80
-rw-r--r--src/barbfile.rs76
-rw-r--r--src/executor.rs16
-rw-r--r--src/main.rs45
-rw-r--r--test_api/test_api.py14
-rw-r--r--test_api/test_api_auth.barb7
-rw-r--r--test_api/test_api_get_profile.barb3
-rw-r--r--test_api/test_api_get_profile_wrong_dep.barb3
8 files changed, 220 insertions, 24 deletions
diff --git a/README.md b/README.md
index 37d950a..ae6aa40 100644
--- a/README.md
+++ b/README.md
@@ -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}