Compare commits
7 commits
1fdd40a68e
...
a5cef7f269
Author | SHA1 | Date | |
---|---|---|---|
a5cef7f269 | |||
b82a63eeb7 | |||
6915a71802 | |||
2695c850bb | |||
ce0d725c43 | |||
77fec19a22 | |||
26d996fbf9 |
9 changed files with 184 additions and 189 deletions
71
Cargo.lock
generated
71
Cargo.lock
generated
|
@ -665,16 +665,6 @@ version = "0.4.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lock_api"
|
|
||||||
version = "0.4.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"scopeguard",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.22"
|
version = "0.4.22"
|
||||||
|
@ -799,29 +789,6 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot"
|
|
||||||
version = "0.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.9.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"smallvec",
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
@ -885,15 +852,6 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
@ -1054,12 +1012,6 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
|
@ -1142,15 +1094,6 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signal-hook-registry"
|
|
||||||
version = "1.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
@ -1293,25 +1236,11 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-macros"
|
|
||||||
version = "2.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-native-tls"
|
name = "tokio-native-tls"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
|
@ -22,6 +22,13 @@ 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 = ["rt-multi-thread"] }
|
||||||
toml = { version = "0.8", features = ["preserve_order"] }
|
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 ...
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = "symbols"
|
||||||
|
panic = "abort"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
[client] # HTTP client configuration
|
[client] # HTTP client configuration
|
||||||
user_agent = "postwoman@sample/0.2.0"
|
user_agent = "postwoman@sample/0.2.0"
|
||||||
|
timeout = 60 # max time for each request to complete, in seconds
|
||||||
|
redirects = 5 # allow up to five redirects, defaults to none
|
||||||
|
|
||||||
[env] # these will be replaced in routes options. environment vars overrule these
|
[env] # these will be replaced in routes options. environment vars overrule these
|
||||||
PW_TOKEN = "set-me-as-and-environment-variable!"
|
PW_TOKEN = "set-me-as-and-environment-variable!"
|
||||||
|
|
|
@ -7,11 +7,8 @@ pub enum PostWomanError {
|
||||||
#[error("invalid method: {0:?}")]
|
#[error("invalid method: {0:?}")]
|
||||||
InvalidMethod(#[from] http::method::InvalidMethod),
|
InvalidMethod(#[from] http::method::InvalidMethod),
|
||||||
|
|
||||||
#[error("invalid header name: {0:?}")]
|
#[error("invalid header: {0:?}")]
|
||||||
InvalidHeaderName(#[from] reqwest::header::InvalidHeaderName),
|
InvalidHeader(#[from] InvalidHeaderError),
|
||||||
|
|
||||||
#[error("invalid header value: {0:?}")]
|
|
||||||
InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
|
|
||||||
|
|
||||||
#[error("contains Unprintable characters: {0:?}")]
|
#[error("contains Unprintable characters: {0:?}")]
|
||||||
Unprintable(#[from] reqwest::header::ToStrError),
|
Unprintable(#[from] reqwest::header::ToStrError),
|
||||||
|
@ -19,9 +16,6 @@ pub enum PostWomanError {
|
||||||
#[error("header '{0}' not found in response")]
|
#[error("header '{0}' not found in response")]
|
||||||
HeaderNotFound(String),
|
HeaderNotFound(String),
|
||||||
|
|
||||||
#[error("invalid header: '{0}'")]
|
|
||||||
InvalidHeader(String),
|
|
||||||
|
|
||||||
#[error("error opening collection: {0:?}")]
|
#[error("error opening collection: {0:?}")]
|
||||||
ErrorOpeningCollection(#[from] std::io::Error),
|
ErrorOpeningCollection(#[from] std::io::Error),
|
||||||
|
|
||||||
|
@ -46,3 +40,13 @@ pub enum PostWomanError {
|
||||||
#[error("regex failed matching in content: {0}")]
|
#[error("regex failed matching in content: {0}")]
|
||||||
NoMatch(String),
|
NoMatch(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum InvalidHeaderError {
|
||||||
|
#[error("invalid header name: {0:?}")]
|
||||||
|
Name(#[from] http::header::InvalidHeaderName),
|
||||||
|
#[error("invalid header value: {0:?}")]
|
||||||
|
Value(#[from] http::header::InvalidHeaderValue),
|
||||||
|
#[error("invalid header format: {0}")]
|
||||||
|
Format(String)
|
||||||
|
}
|
||||||
|
|
111
src/main.rs
111
src/main.rs
|
@ -2,12 +2,11 @@ mod model;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod ext;
|
mod ext;
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
use model::PostWomanConfig;
|
pub use model::PostWomanCollection;
|
||||||
|
|
||||||
pub use errors::PostWomanError;
|
pub use errors::PostWomanError;
|
||||||
|
|
||||||
pub static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
pub static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
/// API tester and debugger from your CLI
|
/// API tester and debugger from your CLI
|
||||||
|
@ -21,9 +20,13 @@ struct PostWomanArgs {
|
||||||
/// action to run
|
/// action to run
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
action: Option<PostWomanActions>,
|
action: Option<PostWomanActions>,
|
||||||
|
|
||||||
|
/// start a multi-thread runtime, with multiple worker threads
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
multi_threaded: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Default)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum PostWomanActions {
|
pub enum PostWomanActions {
|
||||||
/// execute specific endpoint requests
|
/// execute specific endpoint requests
|
||||||
Run {
|
Run {
|
||||||
|
@ -44,63 +47,77 @@ pub enum PostWomanActions {
|
||||||
},
|
},
|
||||||
|
|
||||||
/// show all registered routes in current collection
|
/// show all registered routes in current collection
|
||||||
#[default]
|
List {
|
||||||
List,
|
/// show verbose details for each route
|
||||||
|
#[arg(short = 'V', long, default_value_t = false)]
|
||||||
// Save {
|
verbose: bool,
|
||||||
// /// name for new endpoint
|
},
|
||||||
// name: String,
|
|
||||||
// /// url of endpoint
|
|
||||||
// url: String,
|
|
||||||
// /// method
|
|
||||||
// method: Option<String>,
|
|
||||||
// /// headers
|
|
||||||
// headers: Vec<String>,
|
|
||||||
// /// body
|
|
||||||
// body: Option<String>,
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIMESTAMP_FMT: &str = "%H:%M:%S%.6f";
|
const TIMESTAMP_FMT: &str = "%H:%M:%S%.6f";
|
||||||
|
|
||||||
fn print_results(res: String, name: String, before: chrono::DateTime<chrono::Local>, suffix: String) {
|
fn main() -> Result<(), PostWomanError> {
|
||||||
let after = chrono::Local::now();
|
|
||||||
let elapsed = (after - before).num_milliseconds();
|
|
||||||
let timestamp = after.format(TIMESTAMP_FMT);
|
|
||||||
eprintln!(" + [{timestamp}] {name} {suffix}done in {elapsed}ms", );
|
|
||||||
print!("{}", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), PostWomanError> {
|
|
||||||
let args = PostWomanArgs::parse();
|
let args = PostWomanArgs::parse();
|
||||||
|
|
||||||
let collection = std::fs::read_to_string(args.collection)?;
|
let collection_raw = std::fs::read_to_string(&args.collection)?;
|
||||||
let config: PostWomanConfig = toml::from_str(&collection)?;
|
let collection: PostWomanCollection = toml::from_str(&collection_raw)?;
|
||||||
|
|
||||||
match args.action.unwrap_or_default() {
|
if args.multi_threaded {
|
||||||
PostWomanActions::List => {
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
let ua = config.client.user_agent.unwrap_or(APP_USER_AGENT.to_string());
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("failed creating tokio multi-thread runtime")
|
||||||
|
.block_on(async { run_postwoman(args, collection).await })
|
||||||
|
} else {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("failed creating tokio current-thread runtime")
|
||||||
|
.block_on(async { run_postwoman(args, collection).await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_postwoman(args: PostWomanArgs, collection: PostWomanCollection) -> Result<(), PostWomanError> {
|
||||||
|
let action = args.action.unwrap_or(PostWomanActions::List { verbose: false });
|
||||||
|
|
||||||
|
match action {
|
||||||
|
PostWomanActions::List { verbose } => {
|
||||||
|
let ua = collection.client.user_agent.unwrap_or(APP_USER_AGENT.to_string());
|
||||||
println!("> {ua}");
|
println!("> {ua}");
|
||||||
|
|
||||||
for (key, value) in config.env {
|
for (key, value) in collection.env {
|
||||||
println!("+ {key}: {}", ext::stringify_toml(&value));
|
println!("+ {key}: {}", ext::stringify_toml(&value));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
for (name, endpoint) in config.route {
|
for (name, mut endpoint) in collection.route {
|
||||||
println!("- {name}: \t{} \t{}", endpoint.method.unwrap_or("GET".into()), endpoint.url);
|
println!("- {name}: \t{} \t{}", endpoint.method.as_deref().unwrap_or("GET"), endpoint.url);
|
||||||
|
if verbose {
|
||||||
|
if let Some(ref query) = endpoint.query {
|
||||||
|
for query in query {
|
||||||
|
println!(" |? {query}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref headers) = endpoint.headers {
|
||||||
|
for header in headers {
|
||||||
|
println!(" |: {header}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(body) = endpoint.body() {
|
||||||
|
println!(" |> {body}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PostWomanActions::Run { query, parallel, repeat, debug } => {
|
PostWomanActions::Run { query, parallel, repeat, debug } => {
|
||||||
let pattern = regex::Regex::new(&query)?;
|
let pattern = regex::Regex::new(&query)?;
|
||||||
let mut joinset = tokio::task::JoinSet::new();
|
let mut joinset = tokio::task::JoinSet::new();
|
||||||
let client = Arc::new(config.client);
|
let client = std::sync::Arc::new(collection.client);
|
||||||
let env = Arc::new(config.env);
|
let env = std::sync::Arc::new(collection.env);
|
||||||
for (name, mut endpoint) in config.route {
|
for (name, mut endpoint) in collection.route {
|
||||||
if pattern.find(&name).is_some() {
|
if pattern.find(&name).is_some() {
|
||||||
if debug { endpoint.extract = Some(ext::StringOr::T(model::Extractor::Debug)) };
|
if debug { endpoint.extract = Some(ext::StringOr::T(model::ExtractorConfig::Debug)) };
|
||||||
for i in 0..repeat {
|
for i in 0..repeat {
|
||||||
let suffix = if repeat > 1 {
|
let suffix = if repeat > 1 {
|
||||||
format!("#{} ", i+1)
|
format!("#{} ", i+1)
|
||||||
|
@ -136,11 +153,15 @@ async fn main() -> Result<(), PostWomanError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// PostWomanActions::Save { name, url, method, headers, body } => {
|
|
||||||
// todo!();
|
|
||||||
// },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_results(res: String, name: String, before: chrono::DateTime<chrono::Local>, suffix: String) {
|
||||||
|
let after = chrono::Local::now();
|
||||||
|
let elapsed = (after - before).num_milliseconds();
|
||||||
|
let timestamp = after.format(TIMESTAMP_FMT);
|
||||||
|
eprintln!(" + [{timestamp}] {name} {suffix}done in {elapsed}ms", );
|
||||||
|
print!("{}", res);
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct PostWomanClient {
|
pub struct ClientConfig {
|
||||||
|
/// user agent for requests, defaults to 'postwoman/<version>'
|
||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<String>,
|
||||||
|
/// max total duration of each request, in seconds. defaults to 30
|
||||||
|
pub timeout: Option<u64>,
|
||||||
|
/// max number of redirects to allow, defaults to 0
|
||||||
|
pub redirects: Option<usize>,
|
||||||
|
/// accept invalid SSL certificates, defaults to false (be careful: this is dangerous!)
|
||||||
|
pub accept_invalid_certs: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use base64::{prelude::BASE64_STANDARD, Engine};
|
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||||
|
use http::header::{InvalidHeaderName, InvalidHeaderValue};
|
||||||
|
use http::method::InvalidMethod;
|
||||||
use http::{HeaderMap, HeaderName, HeaderValue};
|
use http::{HeaderMap, HeaderName, HeaderValue};
|
||||||
use jaq_interpret::FilterT;
|
use jaq_interpret::FilterT;
|
||||||
|
|
||||||
|
use crate::errors::InvalidHeaderError;
|
||||||
use crate::{PostWomanError, APP_USER_AGENT};
|
use crate::{PostWomanError, APP_USER_AGENT};
|
||||||
|
|
||||||
use crate::ext::{stringify_toml, stringify_json, StringOr};
|
use crate::ext::{stringify_toml, stringify_json, StringOr};
|
||||||
use super::{Extractor, PostWomanClient};
|
use super::{ExtractorConfig, ClientConfig};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Endpoint {
|
pub struct EndpointConfig {
|
||||||
/// endpoint url, required
|
/// endpoint url, required
|
||||||
pub url: String,
|
pub url: String,
|
||||||
/// http method for request, default GET
|
/// http method for request, default GET
|
||||||
|
@ -25,35 +28,45 @@ pub struct Endpoint {
|
||||||
/// expected error code, will fail if different
|
/// expected error code, will fail if different
|
||||||
pub expect: Option<u16>,
|
pub expect: Option<u16>,
|
||||||
/// response extractor
|
/// response extractor
|
||||||
pub extract: Option<StringOr<Extractor>>,
|
pub extract: Option<StringOr<ExtractorConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_recursive(element: toml::Value, from: &str, to: &str) -> toml::Value {
|
impl EndpointConfig {
|
||||||
match element {
|
pub fn body(&mut self) -> Result<String, serde_json::Error> {
|
||||||
toml::Value::Float(x) => toml::Value::Float(x),
|
match self.body.take().unwrap_or_default() {
|
||||||
toml::Value::Integer(x) => toml::Value::Integer(x),
|
StringOr::Str(x) => Ok(x.clone()),
|
||||||
toml::Value::Boolean(x) => toml::Value::Boolean(x),
|
StringOr::T(json) => Ok(serde_json::to_string(&json)?),
|
||||||
toml::Value::Datetime(x) => toml::Value::Datetime(x),
|
}
|
||||||
toml::Value::String(x) => toml::Value::String(x.replace(from, to)),
|
}
|
||||||
toml::Value::Array(x) => toml::Value::Array(
|
|
||||||
x.into_iter().map(|x| replace_recursive(x, from, to)).collect()
|
pub fn method(&mut self) -> Result<reqwest::Method, InvalidMethod> {
|
||||||
),
|
match self.method {
|
||||||
toml::Value::Table(map) => {
|
Some(ref m) => Ok(reqwest::Method::from_str(m)?),
|
||||||
let mut out = toml::map::Map::new();
|
None => Ok(reqwest::Method::GET),
|
||||||
for (k, v) in map {
|
}
|
||||||
let new_v = replace_recursive(v.clone(), from, to);
|
}
|
||||||
if k.contains(from) {
|
|
||||||
out.insert(k.replace(from, to), new_v);
|
pub fn headers(&mut self) -> Result<HeaderMap, InvalidHeaderError> {
|
||||||
} else {
|
let mut headers = HeaderMap::default();
|
||||||
out.insert(k.to_string(), new_v);
|
for header in self.headers.take().unwrap_or_default() {
|
||||||
}
|
let (k, v) = header.split_once(':')
|
||||||
}
|
.ok_or_else(|| InvalidHeaderError::Format(header.clone()))?;
|
||||||
toml::Value::Table(out)
|
headers.insert(
|
||||||
},
|
HeaderName::from_str(k)?,
|
||||||
|
HeaderValue::from_str(v)?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url(&mut self) -> String {
|
||||||
|
let mut url = self.url.clone();
|
||||||
|
if let Some(query) = self.query.take() {
|
||||||
|
url = format!("{url}?{}", query.join("&"));
|
||||||
|
}
|
||||||
|
url
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Endpoint {
|
|
||||||
pub fn fill(mut self, env: &toml::Table) -> Self {
|
pub fn fill(mut self, env: &toml::Table) -> Self {
|
||||||
let mut vars: HashMap<String, String> = HashMap::default();
|
let mut vars: HashMap<String, String> = HashMap::default();
|
||||||
|
|
||||||
|
@ -105,34 +118,20 @@ impl Endpoint {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(self, opts: &PostWomanClient) -> Result<String, PostWomanError> {
|
pub async fn execute(mut self, opts: &ClientConfig) -> Result<String, PostWomanError> {
|
||||||
let method = match self.method {
|
let url = self.url();
|
||||||
Some(m) => reqwest::Method::from_str(&m)?,
|
let body = self.body()?;
|
||||||
None => reqwest::Method::GET,
|
let method = self.method()?;
|
||||||
};
|
let headers = self.headers()?;
|
||||||
let mut headers = HeaderMap::default();
|
|
||||||
for header in self.headers.unwrap_or_default() {
|
|
||||||
let (k, v) = header.split_once(':')
|
|
||||||
.ok_or_else(|| PostWomanError::InvalidHeader(header.clone()))?;
|
|
||||||
headers.insert(
|
|
||||||
HeaderName::from_str(k)?,
|
|
||||||
HeaderValue::from_str(v)?
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let body = match self.body.unwrap_or_default() {
|
|
||||||
StringOr::Str(x) => x,
|
|
||||||
StringOr::T(json) => serde_json::to_string(&json)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut url = self.url;
|
|
||||||
if let Some(query) = self.query {
|
|
||||||
url = format!("{url}?{}", query.join("&"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.user_agent(opts.user_agent.as_deref().unwrap_or(APP_USER_AGENT))
|
.user_agent(opts.user_agent.as_deref().unwrap_or(APP_USER_AGENT))
|
||||||
|
.timeout(std::time::Duration::from_secs(opts.timeout.unwrap_or(30)))
|
||||||
|
.redirect(opts.redirects.map(reqwest::redirect::Policy::limited).unwrap_or(reqwest::redirect::Policy::none()))
|
||||||
|
.danger_accept_invalid_certs(opts.accept_invalid_certs.unwrap_or(false))
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
|
|
||||||
let res = client
|
let res = client
|
||||||
.request(method, url)
|
.request(method, url)
|
||||||
.headers(headers)
|
.headers(headers)
|
||||||
|
@ -145,22 +144,22 @@ impl Endpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(match self.extract.unwrap_or_default() {
|
Ok(match self.extract.unwrap_or_default() {
|
||||||
StringOr::T(Extractor::Discard) => "".to_string(),
|
StringOr::T(ExtractorConfig::Discard) => "".to_string(),
|
||||||
StringOr::T(Extractor::Body) => format_body(res).await?,
|
StringOr::T(ExtractorConfig::Body) => format_body(res).await?,
|
||||||
StringOr::T(Extractor::Debug) => {
|
StringOr::T(ExtractorConfig::Debug) => {
|
||||||
// TODO needless double format
|
// TODO needless double format
|
||||||
let res_dbg = format!("{res:#?}");
|
let res_dbg = format!("{res:#?}");
|
||||||
let body = format_body(res).await?;
|
let body = format_body(res).await?;
|
||||||
format!("{res_dbg}\nBody: {body}\n")
|
format!("{res_dbg}\nBody: {body}\n")
|
||||||
},
|
},
|
||||||
StringOr::T(Extractor::Header { key }) => res
|
StringOr::T(ExtractorConfig::Header { key }) => res
|
||||||
.headers()
|
.headers()
|
||||||
.get(&key)
|
.get(&key)
|
||||||
.ok_or(PostWomanError::HeaderNotFound(key))?
|
.ok_or(PostWomanError::HeaderNotFound(key))?
|
||||||
.to_str()?
|
.to_str()?
|
||||||
.to_string()
|
.to_string()
|
||||||
+ "\n",
|
+ "\n",
|
||||||
StringOr::T(Extractor::Regex { pattern }) => {
|
StringOr::T(ExtractorConfig::Regex { pattern }) => {
|
||||||
let pattern = regex::Regex::new(&pattern)?;
|
let pattern = regex::Regex::new(&pattern)?;
|
||||||
let body = format_body(res).await?;
|
let body = format_body(res).await?;
|
||||||
pattern.find(&body)
|
pattern.find(&body)
|
||||||
|
@ -170,7 +169,7 @@ impl Endpoint {
|
||||||
+ "\n"
|
+ "\n"
|
||||||
},
|
},
|
||||||
// bare string defaults to JQL query
|
// bare string defaults to JQL query
|
||||||
StringOr::T(Extractor::JQ { query }) | StringOr::Str(query) => {
|
StringOr::T(ExtractorConfig::JQ { query }) | StringOr::Str(query) => {
|
||||||
let json: serde_json::Value = res.json().await?;
|
let json: serde_json::Value = res.json().await?;
|
||||||
let selection = jq(&query, json)?;
|
let selection = jq(&query, json)?;
|
||||||
if selection.len() == 1 {
|
if selection.len() == 1 {
|
||||||
|
@ -183,6 +182,31 @@ impl Endpoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replace_recursive(element: toml::Value, from: &str, to: &str) -> toml::Value {
|
||||||
|
match element {
|
||||||
|
toml::Value::Float(x) => toml::Value::Float(x),
|
||||||
|
toml::Value::Integer(x) => toml::Value::Integer(x),
|
||||||
|
toml::Value::Boolean(x) => toml::Value::Boolean(x),
|
||||||
|
toml::Value::Datetime(x) => toml::Value::Datetime(x),
|
||||||
|
toml::Value::String(x) => toml::Value::String(x.replace(from, to)),
|
||||||
|
toml::Value::Array(x) => toml::Value::Array(
|
||||||
|
x.into_iter().map(|x| replace_recursive(x, from, to)).collect()
|
||||||
|
),
|
||||||
|
toml::Value::Table(map) => {
|
||||||
|
let mut out = toml::map::Map::new();
|
||||||
|
for (k, v) in map {
|
||||||
|
let new_v = replace_recursive(v.clone(), from, to);
|
||||||
|
if k.contains(from) {
|
||||||
|
out.insert(k.replace(from, to), new_v);
|
||||||
|
} else {
|
||||||
|
out.insert(k.to_string(), new_v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toml::Value::Table(out)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn format_body(res: reqwest::Response) -> Result<String, PostWomanError> {
|
async fn format_body(res: reqwest::Response) -> Result<String, PostWomanError> {
|
||||||
match res.headers().get("Content-Type") {
|
match res.headers().get("Content-Type") {
|
||||||
None => Ok(res.text().await? + "\n"),
|
None => Ok(res.text().await? + "\n"),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(tag = "type", rename_all = "lowercase")]
|
#[serde(tag = "type", rename_all = "lowercase")]
|
||||||
pub enum Extractor {
|
pub enum ExtractorConfig {
|
||||||
#[default]
|
#[default]
|
||||||
Body,
|
Body,
|
||||||
Debug,
|
Debug,
|
||||||
|
|
|
@ -2,14 +2,14 @@ mod client;
|
||||||
mod endpoint;
|
mod endpoint;
|
||||||
mod extractor;
|
mod extractor;
|
||||||
|
|
||||||
pub use client::PostWomanClient;
|
pub use client::ClientConfig;
|
||||||
pub use endpoint::Endpoint;
|
pub use endpoint::EndpointConfig;
|
||||||
pub use extractor::Extractor;
|
pub use extractor::ExtractorConfig;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct PostWomanConfig {
|
pub struct PostWomanCollection {
|
||||||
pub client: PostWomanClient,
|
pub client: ClientConfig,
|
||||||
pub env: toml::Table,
|
pub env: toml::Table,
|
||||||
// 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: indexmap::IndexMap<String, Endpoint>,
|
pub route: indexmap::IndexMap<String, EndpointConfig>,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue