feat: better resource management, better show mode

This commit is contained in:
əlemi 2023-06-21 18:24:45 +02:00
parent b2af0b8c4f
commit b63caa2c3c
Signed by: alemi
GPG key ID: A4895B84D311642C
4 changed files with 258 additions and 111 deletions

View file

@ -1,11 +1,14 @@
pub mod model;
mod requestor;
mod printer;
use clap::{Parser, Subcommand};
use regex::Regex;
use crate::{model::PostWomanCollection, requestor::{send_requests, show_results}};
use crate::{model::PostWomanCollection, requestor::send_requests , printer::{show_results, show_requests}};
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
/// API tester and debugger from your CLI
#[derive(Parser, Debug)]
@ -59,9 +62,24 @@ pub enum PostWomanActions {
/// show response body of each request
#[arg(short, long, default_value_t = false)]
verbose: bool,
/// don't make any real request
#[arg(long, default_value_t = false)]
dry_run: bool,
},
/// list saved requests
Show {},
Show {
/// filter requests to display by url (regex)
filter: Option<String>,
/// pretty-print json outputs
#[arg(short, long, default_value_t = false)]
pretty: bool,
/// show response body of each request
#[arg(short, long, default_value_t = false)]
verbose: bool,
},
}
#[tokio::main]
@ -70,17 +88,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let collection = PostWomanCollection::from_path(&args.collection)?;
println!("╶┐ * {}", collection.name());
if let Some(descr) = &collection.description() {
println!("{}", descr);
}
// if let Some(version) = &collection.version() {
// println!(" │ {}", version);
// }
println!("");
match args.action {
// PostWomanActions::Send {
// url, headers, method, data, save
@ -129,23 +136,46 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// if args.verbose { println!(" │╵") }
// },
PostWomanActions::Test { filter, isolated: _, pretty, verbose } => {
let reqs = collection.requests();
PostWomanActions::Test { filter, isolated, pretty, verbose, dry_run } => {
let matcher = match filter {
Some(rex) => Some(Regex::new(&rex)?),
None => None,
};
let results = send_requests(reqs, matcher).await;
let client = if isolated { None } else {
Some(
reqwest::Client::builder()
.user_agent(APP_USER_AGENT)
.build()
.unwrap()
)
};
match collection.requests(matcher.as_ref()) {
Some(tree) => {
let results = send_requests(tree, client, dry_run).await;
show_results(results, verbose, pretty).await;
},
PostWomanActions::Show { } => {
println!("{:?}", collection); // TODO nicer print
},
None => {
eprintln!("[!] no requests match given filter");
}
}
println!("");
},
PostWomanActions::Show { filter, verbose, pretty } => {
let matcher = match filter {
Some(rex) => Some(Regex::new(&rex)?),
None => None,
};
match collection.requests(matcher.as_ref()) {
Some(tree) => {
show_requests(tree, verbose, pretty);
},
None => {
eprintln!("[!] no requests match given filter");
}
}
},
}
Ok(())
}

27
src/model/description.rs Normal file
View file

@ -0,0 +1,27 @@
use postman_collection::{v2_0_0, v2_1_0};
pub trait IntoOptionalString {
fn as_string(&self) -> Option<String>;
}
impl IntoOptionalString for v2_0_0::DescriptionUnion {
fn as_string(&self) -> Option<String> {
match self {
v2_0_0::DescriptionUnion::String(x) => Some(x.clone()),
v2_0_0::DescriptionUnion::Description(
v2_0_0::Description { content, .. }
) => content.clone(),
}
}
}
impl IntoOptionalString for v2_1_0::DescriptionUnion {
fn as_string(&self) -> Option<String> {
match self {
v2_1_0::DescriptionUnion::String(x) => Some(x.clone()),
v2_1_0::DescriptionUnion::Description(
v2_1_0::Description { content, .. }
) => content.clone(),
}
}
}

138
src/printer.rs Normal file
View file

@ -0,0 +1,138 @@
use crate::{requestor::{TestResultTree, TestResultNode, TestResultCase}, model::collector::{RequestTree, RequestNode}};
// TODO maybe make a generic trait Displayable and make just one recursive function?
pub async fn show_results(tree: TestResultTree, verbose: bool, pretty: bool) {
show_results_r(tree, verbose, pretty, 0).await
}
#[async_recursion::async_recursion]
pub async fn show_results_r(tree: TestResultTree, verbose: bool, pretty: bool, depth: usize) {
let indent_skip = "".repeat(depth);
let indent_node = match depth {
0 => "".into(),
1 => "".into(),
x => "".repeat(x - 1) + "",
};
match tree.result {
TestResultNode::Leaf(rx) => {
let res = rx.await.unwrap();
let method = res.request.method().as_str();
let url = res.request.url().as_str();
match res.result {
TestResultCase::Skip => {
println!("{} ? --- >> {} {}", indent_node, method, url);
if verbose {
println!("{} [skipped]", indent_skip);
println!("{}", indent_skip);
}
},
TestResultCase::Success(response) => {
let status_code = response.status().as_u16();
let marker = if status_code < 400 { '✓' } else { '×' };
println!("{} {} {} >> {} {}", indent_node, marker, status_code, method, url);
if verbose {
let body = process_json_body(response.text().await.unwrap(), pretty);
println!("{} {}", indent_skip, body.replace("\n", &format!("\n{} ", indent_skip)));
println!("{}", indent_skip);
}
},
TestResultCase::Failure(err) => {
println!("{} ! ERROR >> {} {}", indent_node, method, url);
if verbose {
println!("{} {}", indent_skip, err);
println!("{}", indent_skip);
}
}
}
},
TestResultNode::Branch(results) => {
println!("{}─┐ {}", indent_node, tree.name);
if verbose {
if let Some(descr) = tree.description {
println!("{}{}", indent_skip, descr);
}
println!("{}", indent_skip);
}
for res in results {
show_results_r(res, verbose, pretty, depth + 1).await;
}
println!("{}", indent_skip);
},
}
}
pub fn show_requests(tree: RequestTree, verbose: bool, pretty: bool) {
show_requests_r(tree, verbose, pretty, 0);
}
pub fn show_requests_r(tree: RequestTree, verbose: bool, pretty: bool, depth: usize) {
let indent_skip = "".repeat(depth);
let indent_node = match depth {
0 => "".into(),
1 => "".into(),
x => "".repeat(x - 1) + "",
};
match tree.request {
RequestNode::Leaf(request) => {
let method = request.method().as_str();
let url = request.url().as_str();
println!("{} * {} {}", indent_node, method, url);
if verbose {
let headers = request.headers()
.iter()
.map(|(k, v)| format!("{}:{}", k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap()))
.collect::<Vec<String>>();
if headers.len() > 0 {
if pretty {
println!("{} [", indent_skip);
for h in headers {
println!("{} {}", indent_skip, h);
}
println!("{} ]", indent_skip);
} else {
println!("{} [ {} ]", indent_skip, headers.join(", "));
}
}
if let Some(body) = request.body() {
if let Some(bytes) = body.as_bytes() {
let txt = process_json_body(std::str::from_utf8(bytes).unwrap().to_string(), pretty);
println!("{} {}", indent_skip, txt.replace("\n", &format!("\n{} ", indent_skip)));
} else {
println!("{} << streaming body >>", indent_skip);
}
}
println!("{}", indent_skip);
}
},
RequestNode::Branch(requests) => {
println!("{}─┐ {}", indent_node, tree.name);
if verbose {
if let Some(descr) = tree.description {
println!("{}{}", indent_skip, descr);
}
println!("{}", indent_skip);
}
for req in requests {
show_requests_r(req, verbose, pretty, depth + 1);
}
println!("{}", indent_skip);
},
}
}
fn process_json_body(txt: String, pretty: bool) -> String {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&txt) {
if pretty {
if let Ok(t) = serde_json::to_string_pretty(&v) {
return t;
}
} else {
if let Ok(t) = serde_json::to_string(&v) { // try to minify it anyway
return t;
}
}
}
txt
}

View file

@ -1,91 +1,6 @@
use crate::model::collector::{RequestTree, RequestNode};
use regex::Regex;
use tokio::sync::oneshot;
#[async_recursion::async_recursion]
pub async fn send_requests(tree: RequestTree, filter: Option<Regex>) -> TestResultTree {
let result : TestResultNode;
match tree.request {
RequestNode::Leaf(req) => {
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
let request = req.build().unwrap();
if filter.is_some() && !filter.unwrap().is_match(request.url().as_str()) {
tx.send(TestResultHolder { request, result: TestResultCase::Skip }).unwrap();
} else {
let c = reqwest::Client::default();
let res = match c.execute(request.try_clone().unwrap()).await {
Ok(res) => TestResultCase::Success(res),
Err(e) => TestResultCase::Failure(e),
};
tx.send(TestResultHolder { request, result: res }).unwrap();
}
});
result = TestResultNode::Leaf(rx);
},
RequestNode::Branch(requests) => {
let mut out = Vec::new();
for req in requests {
out.push(send_requests(req, filter.clone()).await);
}
result = TestResultNode::Branch(out);
}
}
TestResultTree { name: tree.name, result }
}
pub async fn show_results(tree: TestResultTree, verbose: bool, pretty: bool) {
show_results_r(tree, verbose, pretty, 0).await
}
#[async_recursion::async_recursion]
pub async fn show_results_r(tree: TestResultTree, verbose: bool, pretty: bool, depth: usize) {
let indent = "".repeat(depth);
match tree.result {
TestResultNode::Leaf(rx) => {
let res = rx.await.unwrap();
let method = res.request.method().as_str();
let url = res.request.url().as_str();
match res.result {
TestResultCase::Skip => {},
TestResultCase::Success(response) => {
let status_code = response.status().as_u16();
let marker = if status_code < 400 { '✓' } else { '×' };
println!("{}{} {} >> {} {}", indent, marker, status_code, method, url);
if verbose {
let mut body = response.text().await.unwrap();
if pretty {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
if let Ok(t) = serde_json::to_string_pretty(&v) {
body = t;
}
}
}
println!("{}{}", indent, body.replace("\n", &format!("\n{} ", indent)));
println!("{}", indent);
}
},
TestResultCase::Failure(err) => {
println!("{} ├ ! ERROR >> {} {}", indent, method, url);
if verbose {
println!("{}{}", indent, err);
println!("{}", indent);
}
}
}
},
TestResultNode::Branch(results) => {
println!("{} ├─┐ {}", indent, tree.name);
for res in results {
show_results_r(res, verbose, pretty, depth + 1).await;
}
println!("{} │ ╵", indent);
},
}
}
#[derive(Debug)]
pub enum TestResultNode {
Leaf(oneshot::Receiver<TestResultHolder>),
@ -94,14 +9,15 @@ pub enum TestResultNode {
#[derive(Debug)]
pub struct TestResultTree {
name: String,
result: TestResultNode,
pub name: String,
pub description: Option<String>,
pub result: TestResultNode,
}
#[derive(Debug)]
pub struct TestResultHolder {
request: reqwest::Request,
result: TestResultCase,
pub request: reqwest::Request,
pub result: TestResultCase,
}
#[derive(Debug)]
@ -110,3 +26,39 @@ pub enum TestResultCase {
Success(reqwest::Response),
Failure(reqwest::Error),
}
#[async_recursion::async_recursion]
pub async fn send_requests(tree: RequestTree, client: Option<reqwest::Client>, dry_run: bool) -> TestResultTree {
let result : TestResultNode;
match tree.request {
RequestNode::Leaf(request) => {
let (tx, rx) = oneshot::channel();
let c = client.unwrap_or(
reqwest::Client::builder()
.user_agent(crate::APP_USER_AGENT)
.build()
.unwrap()
);
tokio::spawn(async move {
let res = if dry_run { TestResultCase::Skip } else {
match c.execute(request.try_clone().unwrap()).await {
Ok(res) => TestResultCase::Success(res),
Err(e) => TestResultCase::Failure(e),
}
};
tx.send(TestResultHolder { request, result: res }).unwrap();
});
result = TestResultNode::Leaf(rx);
},
RequestNode::Branch(requests) => {
let mut out = Vec::new();
for req in requests {
out.push(send_requests(req, client.clone(), dry_run).await);
}
result = TestResultNode::Branch(out);
}
}
TestResultTree { name: tree.name, description: tree.description, result }
}