Compare commits

...

6 commits

Author SHA1 Message Date
3a966bdc8b
feat: respect user agent config
also pass client config to .execute() method so we can configure reqwest
client
2024-10-19 04:14:31 +02:00
763fa7fbcb
chore: remove sample cmd
there's a default config and it outputs ugly inner tables anyway
2024-10-19 04:14:11 +02:00
f6577e6f63
docs: more examples on default config 2024-10-19 04:13:42 +02:00
72c5b443db
feat: run requests in order they appear 2024-10-19 04:13:23 +02:00
543f258724
docs: update default config 2024-10-19 04:12:46 +02:00
9013515290
fix: var replace with ${...} 2024-10-19 03:58:43 +02:00
6 changed files with 54 additions and 141 deletions

3
Cargo.lock generated
View file

@ -507,6 +507,7 @@ checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@ -753,6 +754,7 @@ version = "0.2.0"
dependencies = [ dependencies = [
"clap", "clap",
"http", "http",
"indexmap",
"regex", "regex",
"reqwest", "reqwest",
"serde", "serde",
@ -1250,6 +1252,7 @@ version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [ dependencies = [
"indexmap",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",

View file

@ -8,11 +8,12 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
http = "1.1.0" http = "1.1.0"
indexmap = { version = "2.6", features = ["serde"] }
regex = "1.11" regex = "1.11"
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0.64" thiserror = "1.0.64"
tokio = { version = "1.40", features = ["full"] } tokio = { version = "1.40", features = ["full"] }
toml = "0.8" toml = { version = "0.8", features = ["preserve_order"] }
toml_edit = { version = "0.22", features = ["serde"] } # only to pretty print tables ... toml_edit = { version = "0.22", features = ["serde"] } # only to pretty print tables ...

View file

@ -1,85 +0,0 @@
{
"variables": [],
"info": {
"name": "Sample Postman Collection",
"_postman_id": "35567af6-6b92-26c2-561a-21fe8aeeb1ea",
"description": "A sample collection to demonstrate collections as a set of related requests",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "GET request",
"request": {
"url": {
"raw": "https://api.alemi.dev/dump?source=sample-collection",
"protocol": "https",
"host": [
"alemi",
"dev"
],
"path": [
"dump"
],
"query": [
{
"key": "source",
"value": "sample-collection",
"equals": true,
"description": ""
}
],
"variable": []
},
"method": "GET",
"header": [],
"body": {},
"description": ""
},
"response": []
},
{
"name": "POST requests",
"item": [
{
"name": "Text body",
"request": {
"url": "https://api.alemi.dev/dump",
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "text/plain",
"type": "default"
}
],
"body": {
"mode": "raw",
"raw": "Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium..."
},
"description": ""
},
"response": []
},
{
"name": "Json body",
"request": {
"url": "https://api.alemi.dev/dump",
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\"text\":\"Lorem ipsum\", \"length\":100}"
},
"description": ""
},
"response": []
}
]
}
]
}

33
postwoman.toml Normal file
View file

@ -0,0 +1,33 @@
[client]
user_agent = "postwoman@sample/0.2.0"
[route.healthcheck]
url = "https://api.alemi.dev/"
[route.debug]
url = "https://api.alemi.dev/debug"
method = "PUT"
headers = [
"Content-Type: application/json",
"Authorization: Bearer ${PW_TOKEN}",
]
body = { hello = "world!", success = true }
extract = { type = "body" }
[route.payload]
url = "https://api.alemi.dev/debug"
method = "POST"
body = '''{
"complex": {
"json": "payloads",
"can": "be",
"expressed": "this",
"way": true
}
}'''
extract = { type = "body" }
[route.cookie]
url = "https://api.alemi.dev/getcookie"
method = "GET"
extract = { type = "header", key = "Set-Cookie" }

View file

@ -1,9 +1,7 @@
mod model; mod model;
use std::collections::HashMap;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use model::{Endpoint, Extractor, PostWomanClient, PostWomanConfig, PostWomanError, StringOr}; use model::{PostWomanConfig, PostWomanError};
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
@ -22,9 +20,6 @@ struct PostWomanArgs {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
pub enum PostWomanActions { pub enum PostWomanActions {
/// print an example configragion, pipe to file and start editing
Sample,
/// execute specific endpoint requests /// execute specific endpoint requests
Run { Run {
/// regex query filter, run all with '.*' /// regex query filter, run all with '.*'
@ -49,45 +44,6 @@ pub enum PostWomanActions {
async fn main() -> Result<(), PostWomanError> { async fn main() -> Result<(), PostWomanError> {
let args = PostWomanArgs::parse(); let args = PostWomanArgs::parse();
if matches!(args.action, PostWomanActions::Sample) {
let a = Endpoint {
url: "https://api.alemi.dev/debug".into(),
query: None,
method: None,
headers: None,
body: None,
extract: None,
};
let b = Endpoint {
url: "https://api.alemi.dev/debug".into(),
query: None,
method: Some("PUT".into()),
headers: Some(vec![
"Authorization: Bearer asdfg".into(),
"Cache: skip".into(),
]),
body: Some(StringOr::T(toml::Table::from_iter([("hello".into(), toml::Value::String("world".into()))]))),
extract: Some(StringOr::T(Extractor::Body)),
};
let client = PostWomanClient {
user_agent: Some(APP_USER_AGENT.into()),
};
let cfg = PostWomanConfig {
client,
route: HashMap::from_iter([
("simple".to_string(), a),
("json".to_string(), b),
]),
};
println!("{}", toml_edit::ser::to_string_pretty(&cfg)?);
return Ok(());
}
let collection = std::fs::read_to_string(args.collection)?; let collection = std::fs::read_to_string(args.collection)?;
let config: PostWomanConfig = toml::from_str(&collection)?; let config: PostWomanConfig = toml::from_str(&collection)?;
@ -99,7 +55,7 @@ async fn main() -> Result<(), PostWomanError> {
eprintln!("> executing {name}"); eprintln!("> executing {name}");
let res = endpoint let res = endpoint
.fill() .fill()
.execute() .execute(&config.client)
.await?; .await?;
println!("{res}"); println!("{res}");
} }
@ -109,8 +65,6 @@ async fn main() -> Result<(), PostWomanError> {
// PostWomanActions::Save { name, url, method, headers, body } => { // PostWomanActions::Save { name, url, method, headers, body } => {
// todo!(); // todo!();
// }, // },
PostWomanActions::Sample => unreachable!(),
} }
Ok(()) Ok(())

View file

@ -1,6 +1,8 @@
use std::{collections::HashMap, str::FromStr}; use std::str::FromStr;
use reqwest::{header::{HeaderMap, HeaderName, HeaderValue}}; use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use crate::APP_USER_AGENT;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum PostWomanError { pub enum PostWomanError {
@ -85,7 +87,7 @@ fn replace_recursive(element: toml::Value, from: &str, to: &str) -> toml::Value
impl Endpoint { impl Endpoint {
pub fn fill(mut self) -> Self { pub fn fill(mut self) -> Self {
for (k, v) in std::env::vars() { for (k, v) in std::env::vars() {
let k_var = format!("${k}"); let k_var = format!("${{{k}}}");
self.url = self.url.replace(&k_var, &v); self.url = self.url.replace(&k_var, &v);
if let Some(method) = self.method { if let Some(method) = self.method {
self.method = Some(method.replace(&k_var, &v)); self.method = Some(method.replace(&k_var, &v));
@ -115,7 +117,7 @@ impl Endpoint {
self self
} }
pub async fn execute(self) -> Result<String, PostWomanError> { pub async fn execute(self, opts: &PostWomanClient) -> Result<String, PostWomanError> {
let method = match self.method { let method = match self.method {
Some(m) => reqwest::Method::from_str(&m)?, Some(m) => reqwest::Method::from_str(&m)?,
None => reqwest::Method::GET, None => reqwest::Method::GET,
@ -133,7 +135,12 @@ impl Endpoint {
StringOr::Str(x) => x, StringOr::Str(x) => x,
StringOr::T(json) => serde_json::to_string(&json)?, StringOr::T(json) => serde_json::to_string(&json)?,
}; };
let res = reqwest::Client::new()
let client = reqwest::Client::builder()
.user_agent(opts.user_agent.as_deref().unwrap_or(APP_USER_AGENT))
.build()?;
let res = client
.request(method, self.url) .request(method, self.url)
.headers(headers) .headers(headers)
.body(body) .body(body)
@ -142,7 +149,7 @@ impl Endpoint {
.error_for_status()?; .error_for_status()?;
Ok(match self.extract.unwrap_or_default() { Ok(match self.extract.unwrap_or_default() {
StringOr::Str(query) => todo!(), StringOr::Str(_query) => todo!(),
StringOr::T(Extractor::Debug) => format!("{res:#?}"), StringOr::T(Extractor::Debug) => format!("{res:#?}"),
StringOr::T(Extractor::Body) => res.text().await?, StringOr::T(Extractor::Body) => res.text().await?,
StringOr::T(Extractor::Header { key }) => res StringOr::T(Extractor::Header { key }) => res
@ -188,5 +195,5 @@ pub struct PostWomanClient {
pub struct PostWomanConfig { pub struct PostWomanConfig {
pub client: PostWomanClient, pub client: PostWomanClient,
// it's weird to name it singular but makes more sense in config // it's weird to name it singular but makes more sense in config
pub route: HashMap<String, Endpoint>, pub route: indexmap::IndexMap<String, Endpoint>,
} }