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;
|
||||
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;
|
||||
},
|
||||
None => {
|
||||
eprintln!("[!] no requests match given filter");
|
||||
}
|
||||
}
|
||||
|
||||
show_results(results, verbose, pretty).await;
|
||||
},
|
||||
PostWomanActions::Show { } => {
|
||||
println!(" ├ {:?}", collection); // TODO nicer print
|
||||
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");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
println!(" ╵");
|
||||
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 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 }
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue