feat: improved cli, restructured project, add save
This commit is contained in:
parent
700593a2ea
commit
62af1e3c03
3 changed files with 270 additions and 217 deletions
195
src/main.rs
195
src/main.rs
|
@ -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 {}
|
||||
|
|
138
src/model.rs
138
src/model.rs
|
@ -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
154
src/proto.rs
Normal 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 {}
|
Loading…
Reference in a new issue