feat: better resource management, better show mode
This commit is contained in:
parent
b2af0b8c4f
commit
b63caa2c3c
4 changed files with 258 additions and 111 deletions
74
src/main.rs
74
src/main.rs
|
@ -1,11 +1,14 @@
|
||||||
pub mod model;
|
pub mod model;
|
||||||
mod requestor;
|
mod requestor;
|
||||||
|
mod printer;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
use regex::Regex;
|
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
|
/// API tester and debugger from your CLI
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
@ -59,9 +62,24 @@ pub enum PostWomanActions {
|
||||||
/// show response body of each request
|
/// show response body of each request
|
||||||
#[arg(short, long, default_value_t = false)]
|
#[arg(short, long, default_value_t = false)]
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
|
||||||
|
/// don't make any real request
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
dry_run: bool,
|
||||||
},
|
},
|
||||||
/// list saved requests
|
/// 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]
|
#[tokio::main]
|
||||||
|
@ -70,17 +88,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
let collection = PostWomanCollection::from_path(&args.collection)?;
|
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 {
|
match args.action {
|
||||||
// PostWomanActions::Send {
|
// PostWomanActions::Send {
|
||||||
// url, headers, method, data, save
|
// url, headers, method, data, save
|
||||||
|
@ -129,23 +136,46 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
// if args.verbose { println!(" │╵") }
|
// if args.verbose { println!(" │╵") }
|
||||||
// },
|
// },
|
||||||
PostWomanActions::Test { filter, isolated: _, pretty, verbose } => {
|
PostWomanActions::Test { filter, isolated, pretty, verbose, dry_run } => {
|
||||||
let reqs = collection.requests();
|
|
||||||
|
|
||||||
let matcher = match filter {
|
let matcher = match filter {
|
||||||
Some(rex) => Some(Regex::new(&rex)?),
|
Some(rex) => Some(Regex::new(&rex)?),
|
||||||
None => None,
|
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;
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
eprintln!("[!] no requests match given filter");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
show_results(results, verbose, pretty).await;
|
|
||||||
},
|
},
|
||||||
PostWomanActions::Show { } => {
|
PostWomanActions::Show { filter, verbose, pretty } => {
|
||||||
println!(" ├ {:?}", collection); // TODO nicer print
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(" ╵");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
27
src/model/description.rs
Normal file
27
src/model/description.rs
Normal 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
138
src/printer.rs
Normal 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
|
||||||
|
}
|
130
src/requestor.rs
130
src/requestor.rs
|
@ -1,91 +1,6 @@
|
||||||
use crate::model::collector::{RequestTree, RequestNode};
|
use crate::model::collector::{RequestTree, RequestNode};
|
||||||
use regex::Regex;
|
|
||||||
use tokio::sync::oneshot;
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum TestResultNode {
|
pub enum TestResultNode {
|
||||||
Leaf(oneshot::Receiver<TestResultHolder>),
|
Leaf(oneshot::Receiver<TestResultHolder>),
|
||||||
|
@ -94,14 +9,15 @@ pub enum TestResultNode {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TestResultTree {
|
pub struct TestResultTree {
|
||||||
name: String,
|
pub name: String,
|
||||||
result: TestResultNode,
|
pub description: Option<String>,
|
||||||
|
pub result: TestResultNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TestResultHolder {
|
pub struct TestResultHolder {
|
||||||
request: reqwest::Request,
|
pub request: reqwest::Request,
|
||||||
result: TestResultCase,
|
pub result: TestResultCase,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -110,3 +26,39 @@ pub enum TestResultCase {
|
||||||
Success(reqwest::Response),
|
Success(reqwest::Response),
|
||||||
Failure(reqwest::Error),
|
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 }
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue