feat: improved cli, restructured project, add save

This commit is contained in:
əlemi 2023-05-22 00:43:26 +02:00
parent 700593a2ea
commit 62af1e3c03
Signed by: alemi
GPG key ID: A4895B84D311642C
3 changed files with 270 additions and 217 deletions

View file

@ -1,7 +1,11 @@
mod proto;
mod model;
use clap::{Parser, Subcommand};
use reqwest::StatusCode;
use proto::Body;
use reqwest::Method;
use crate::proto::{Item, Request, Header};
/// API tester and debugger from your CLI
#[derive(Parser, Debug)]
@ -13,134 +17,129 @@ struct PostWomanArgs {
/// Action to run
#[clap(subcommand)]
action: Option<PostWomanActions>,
action: PostWomanActions,
/// add action to collection items
#[arg(short = 'S', long, default_value_t = false)]
save: bool,
/// user agent for requests
#[arg(long, default_value = "postwoman")]
agent: String,
/// show response body of each request
#[arg(short, long, default_value_t = false)]
verbose: bool,
}
#[derive(Subcommand, Debug)]
enum PostWomanActions {
/// run a single GET request
Get {
pub enum PostWomanActions {
/// run a single request to given url
Send {
/// request URL
url: String,
/// request method
#[arg(short = 'X', long, default_value_t = Method::GET)]
method: Method,
/// headers for request
#[arg(short = 'H', long, num_args = 0..)]
headers: Vec<String>,
/// request body
#[arg(short, long, default_value = "")]
data: String,
/// show request that is being sent
#[arg(long, default_value_t = false)]
debug: bool,
/// add action to collection items
#[arg(short = 'S', long, default_value_t = false)]
save: bool,
},
/// run all saved requests
Test {},
/// list saved requests
Show {},
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = PostWomanArgs::parse();
let file = std::fs::File::open(&args.collection)?;
let collection : model::PostWomanCollection = serde_json::from_reader(file)?;
let mut collection : proto::PostWomanCollection = {
let file = std::fs::File::open(&args.collection)?;
serde_json::from_reader(file)?
};
println!("╶┐ * {}", collection.info.name);
println!("{}", collection.info.description);
println!("");
if let Some(action) = args.action {
match args.action {
PostWomanActions::Send {
url, headers, method, data, save, debug
} => {
let item = Item {
name: "TODO!".into(),
event: None,
request: Request {
url: crate::proto::Url::String(url),
method: method.to_string(),
header: Some(
headers
.chunks(2)
.map(|x| Header {
key: x[0].clone(),
value: x[1].clone(), // TODO panics
})
.collect(),
),
body: if data.len() > 0 { Some(Body::Text(data)) } else { None },
description: None,
},
response: vec![],
};
match action {
PostWomanActions::Get { url, headers } => {
if headers.len() % 2 != 0 {
return Err(PostWomanError::throw("headers must come in pairs"));
}
let mut req = reqwest::Client::new()
.get(url);
for h in headers.chunks(2) {
let (k, v) = (&h[0], &h[1]);
req = req.header(k, v);
}
let res = req.send().await?;
println!("{}", res.text().await?);
if debug {
println!("{:?}", item);
}
}
} else {
let res = item.send().await?;
println!(" ├┐ {}", res.status());
let mut tasks = Vec::new();
if args.verbose {
println!(" ││ {}", res.text().await?.replace("\n", "\n ││ "));
}
for item in collection.item {
let t = tokio::spawn(async move {
let r = item.exec().await?;
println!("{} >> {}", item.name, r);
Ok::<(), reqwest::Error>(())
});
tasks.push(t);
}
if save {
// TODO prompt for name and descr
collection.item.push(item);
std::fs::write(&args.collection, serde_json::to_string(&collection)?)?;
println!(" ││ * saved");
}
for t in tasks {
t.await??;
}
println!(" │╵");
},
PostWomanActions::Test { } => {
let mut tasks = Vec::new();
for item in collection.item {
let t = tokio::spawn(async move {
let r = item.send().await?;
println!("{} >> {}", item.name, r.status());
if args.verbose {
println!("{}", r.text().await?.replace("\n", "\n"));
}
Ok::<(), reqwest::Error>(())
});
tasks.push(t);
}
for t in tasks {
t.await??;
}
},
PostWomanActions::Show { } => {
println!("{:?}", collection);
},
}
println!("");
Ok(())
}
impl model::Item {
async fn exec(&self) -> reqwest::Result<StatusCode> {
let method = reqwest::Method::from_bytes(
self.request.method.as_bytes()
).unwrap_or(reqwest::Method::GET); // TODO throw an error rather than replacing it silently
let mut req = reqwest::Client::new()
.request(method, self.request.url.to_string());
if let Some(headers) = &self.request.header {
for h in headers {
req = req.header(h.key.clone(), h.value.clone())
}
}
if let Some(body) = &self.request.body {
req = req.body(body.to_string().clone());
}
let res = req.send().await?;
Ok(res.status())
}
}
// barebones custom error
#[derive(Debug)]
struct PostWomanError {
msg : String,
}
impl PostWomanError {
pub fn throw(msg: impl ToString) -> Box<dyn std::error::Error> {
Box::new(
PostWomanError {
msg: msg.to_string(),
}
)
}
}
impl std::fmt::Display for PostWomanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PostWomanError({})", self.msg)
}
}
impl std::error::Error for PostWomanError {}

View file

@ -1,129 +1,29 @@
use serde::{Serialize, Deserialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug)]
pub struct PostWomanCollection {
pub variables: Vec<String>, // TODO these sure aren't just strings for sure...
pub info: CollectionInfo,
pub item: Vec<Item>,
}
use reqwest::{Method, Result, Client, Response, Url};
#[derive(Serialize, Deserialize, Debug)]
pub struct CollectionInfo {
pub name: String,
pub description: String,
pub schema: String,
use crate::proto::Item;
pub _postman_id: Option<String>,
}
impl Item {
pub async fn send(&self) -> Result<Response> {
let method = Method::from_bytes(
self.request.method.as_bytes() // TODO lol?
).unwrap_or(Method::GET); // TODO throw an error rather than replacing it silently
#[derive(Serialize, Deserialize, Debug)]
pub struct Item {
pub name: String,
pub event: Option<Vec<Event>>,
pub request: Request,
pub response: Vec<String>, // TODO surely isn't just strings
}
let url = Url::from_str(&self.request.url.to_string()).unwrap();
#[derive(Serialize, Deserialize, Debug)]
pub struct Event {
pub listen: String,
pub script: Script,
}
let mut req = Client::new().request(method, url);
#[derive(Serialize, Deserialize, Debug)]
pub struct Script {
pub r#type: String,
pub exec: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Request {
pub url: Url,
pub method: String,
pub header: Option<Vec<Header>>,
pub body: Option<String>,
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Header {
pub key: String,
pub value: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Body {
Json(serde_json::Value),
Text(String),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Url {
Object {
raw: Option<String>,
protocol: String,
host: Vec<String>,
path: Vec<String>,
query: Option<Vec<Query>>,
variable: Option<Vec<String>>, // TODO surely aren't just strings
},
String(String),
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Query {
pub key: String,
pub value: String,
pub equals: bool,
pub description: Option<String>,
}
impl ToString for Body {
fn to_string(&self) -> String {
match self {
Body::Json(v) => serde_json::to_string(v).unwrap(),
Body::Text(s) => s.clone(),
}
}
}
impl ToString for Query {
fn to_string(&self) -> String {
format!("{}={}", self.key, self.value)
}
}
impl ToString for Url {
fn to_string(&self) -> String {
match self {
Url::String(s) => s.clone(),
Url::Object {
raw, protocol,
host,path, query,
variable: _
} => {
match &raw {
Some(s) => s.clone(),
None => {
let mut url = String::new();
url.push_str(&protocol);
url.push_str("://");
url.push_str(&host.join("."));
url.push_str("/");
url.push_str(&path.join("/"));
if let Some(query) = &query {
url.push_str("?");
let q : Vec<String> = query.iter().map(|x| x.to_string()).collect();
url.push_str(&q.join("&"));
}
url
}
}
if let Some(headers) = &self.request.header {
for h in headers {
req = req.header(h.key.clone(), h.value.clone())
}
}
if let Some(body) = &self.request.body {
req = req.body(body.to_string());
}
req.send().await
}
}

154
src/proto.rs Normal file
View file

@ -0,0 +1,154 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct PostWomanCollection {
pub variables: Vec<String>, // TODO these sure aren't just strings for sure...
pub info: CollectionInfo,
pub item: Vec<Item>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CollectionInfo {
pub name: String,
pub description: String,
pub schema: String,
pub _postman_id: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Item {
pub name: String,
pub event: Option<Vec<Event>>,
pub request: Request,
pub response: Vec<String>, // TODO surely isn't just strings
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Event {
pub listen: String,
pub script: Script,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Script {
pub r#type: String,
pub exec: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Request {
pub url: Url,
pub method: String,
pub header: Option<Vec<Header>>,
pub body: Option<Body>,
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Header {
pub key: String,
pub value: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Query {
pub key: String,
pub value: String,
pub equals: bool,
pub description: Option<String>,
}
impl ToString for Query {
fn to_string(&self) -> String {
format!("{}={}", self.key, self.value)
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Body {
Json(serde_json::Value),
Text(String),
}
impl ToString for Body {
fn to_string(&self) -> String {
match self {
Body::Json(v) => serde_json::to_string(v).unwrap(),
Body::Text(s) => s.clone(),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Url {
Object {
raw: Option<String>,
protocol: String,
host: Vec<String>,
path: Vec<String>,
query: Option<Vec<Query>>,
variable: Option<Vec<String>>, // TODO surely aren't just strings
},
String(String),
}
impl ToString for Url {
fn to_string(&self) -> String {
match self {
Url::String(s) => s.clone(),
Url::Object {
raw, protocol,
host,path, query,
variable: _
} => {
match &raw {
Some(s) => s.clone(),
None => {
let mut url = String::new();
url.push_str(&protocol);
url.push_str("://");
url.push_str(&host.join("."));
url.push_str("/");
url.push_str(&path.join("/"));
if let Some(query) = &query {
url.push_str("?");
let q : Vec<String> = query.iter().map(|x| x.to_string()).collect();
url.push_str(&q.join("&"));
}
url
}
}
}
}
}
}
// barebones custom error
// #[derive(Debug)]
// pub struct PostWomanError {
// msg : String,
// }
//
// impl PostWomanError {
// pub fn throw(msg: impl ToString) -> Box<dyn std::error::Error> {
// Box::new(
// PostWomanError {
// msg: msg.to_string(),
// }
// )
// }
// }
//
// impl std::fmt::Display for PostWomanError {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// write!(f, "PostWomanError({})", self.msg)
// }
// }
//
// impl std::error::Error for PostWomanError {}