From b63caa2c3c00421a9dba0c5bbe905f397571710b Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 21 Jun 2023 18:24:45 +0200 Subject: [PATCH] feat: better resource management, better show mode --- src/main.rs | 74 ++++++++++++++------- src/model/description.rs | 27 ++++++++ src/printer.rs | 138 +++++++++++++++++++++++++++++++++++++++ src/requestor.rs | 130 ++++++++++++------------------------ 4 files changed, 258 insertions(+), 111 deletions(-) create mode 100644 src/model/description.rs create mode 100644 src/printer.rs diff --git a/src/main.rs b/src/main.rs index cda7764..c4fc3de 100644 --- a/src/main.rs +++ b/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, + + /// 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> { 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> { // 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(()) } diff --git a/src/model/description.rs b/src/model/description.rs new file mode 100644 index 0000000..f0f318c --- /dev/null +++ b/src/model/description.rs @@ -0,0 +1,27 @@ +use postman_collection::{v2_0_0, v2_1_0}; + +pub trait IntoOptionalString { + fn as_string(&self) -> Option; +} + +impl IntoOptionalString for v2_0_0::DescriptionUnion { + fn as_string(&self) -> Option { + 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 { + match self { + v2_1_0::DescriptionUnion::String(x) => Some(x.clone()), + v2_1_0::DescriptionUnion::Description( + v2_1_0::Description { content, .. } + ) => content.clone(), + } + } +} diff --git a/src/printer.rs b/src/printer.rs new file mode 100644 index 0000000..8374ab4 --- /dev/null +++ b/src/printer.rs @@ -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::>(); + 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::(&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 +} diff --git a/src/requestor.rs b/src/requestor.rs index d197494..9511706 100644 --- a/src/requestor.rs +++ b/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) -> 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::(&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), @@ -94,14 +9,15 @@ pub enum TestResultNode { #[derive(Debug)] pub struct TestResultTree { - name: String, - result: TestResultNode, + pub name: String, + pub description: Option, + 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, 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 } +}