2024-10-19 20:36:08 +02:00
|
|
|
use std::{collections::HashMap, str::FromStr};
|
2024-10-19 03:28:16 +02:00
|
|
|
|
2024-10-19 05:28:44 +02:00
|
|
|
use base64::{prelude::BASE64_STANDARD, Engine};
|
2024-10-20 01:47:17 +02:00
|
|
|
use http::method::InvalidMethod;
|
2024-10-19 18:02:04 +02:00
|
|
|
use http::{HeaderMap, HeaderName, HeaderValue};
|
2024-10-19 21:44:05 +02:00
|
|
|
use jaq_interpret::FilterT;
|
2024-10-19 04:14:31 +02:00
|
|
|
|
2024-10-20 01:47:17 +02:00
|
|
|
use crate::errors::InvalidHeaderError;
|
2024-10-19 18:02:04 +02:00
|
|
|
use crate::{PostWomanError, APP_USER_AGENT};
|
2024-10-19 03:28:16 +02:00
|
|
|
|
2024-10-19 22:27:58 +02:00
|
|
|
use crate::ext::{stringify_toml, stringify_json, StringOr};
|
2024-10-20 01:32:05 +02:00
|
|
|
use super::{ExtractorConfig, ClientConfig};
|
2024-10-19 03:28:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
2024-10-20 01:32:05 +02:00
|
|
|
pub struct EndpointConfig {
|
2024-10-19 03:28:16 +02:00
|
|
|
/// endpoint url, required
|
|
|
|
pub url: String,
|
|
|
|
/// http method for request, default GET
|
|
|
|
pub method: Option<String>,
|
|
|
|
/// query parameters, appended to base url
|
|
|
|
pub query: Option<Vec<String>>,
|
|
|
|
/// headers for request, array of "key: value" pairs
|
|
|
|
pub headers: Option<Vec<String>>,
|
|
|
|
/// body, optional string
|
|
|
|
pub body: Option<StringOr<toml::Table>>,
|
2024-10-19 22:02:05 +02:00
|
|
|
/// expected error code, will fail if different
|
|
|
|
pub expect: Option<u16>,
|
2024-10-19 03:28:16 +02:00
|
|
|
/// response extractor
|
2024-10-20 01:32:05 +02:00
|
|
|
pub extract: Option<StringOr<ExtractorConfig>>,
|
2024-10-19 03:28:16 +02:00
|
|
|
}
|
|
|
|
|
2024-10-20 01:32:05 +02:00
|
|
|
impl EndpointConfig {
|
2024-10-20 01:47:17 +02:00
|
|
|
pub fn body(&mut self) -> Result<String, serde_json::Error> {
|
2024-10-20 01:55:14 +02:00
|
|
|
match self.body.take() {
|
|
|
|
None => Ok("".to_string()),
|
|
|
|
Some(StringOr::Str(x)) => Ok(x.clone()),
|
|
|
|
Some(StringOr::T(json)) => Ok(serde_json::to_string(&json)?),
|
2024-10-20 01:47:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn method(&mut self) -> Result<reqwest::Method, InvalidMethod> {
|
|
|
|
match self.method {
|
|
|
|
Some(ref m) => Ok(reqwest::Method::from_str(m)?),
|
|
|
|
None => Ok(reqwest::Method::GET),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn headers(&mut self) -> Result<HeaderMap, InvalidHeaderError> {
|
|
|
|
let mut headers = HeaderMap::default();
|
|
|
|
for header in self.headers.take().unwrap_or_default() {
|
|
|
|
let (k, v) = header.split_once(':')
|
|
|
|
.ok_or_else(|| InvalidHeaderError::Format(header.clone()))?;
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-10-19 20:36:08 +02:00
|
|
|
pub fn fill(mut self, env: &toml::Table) -> Self {
|
2024-10-19 21:44:05 +02:00
|
|
|
let mut vars: HashMap<String, String> = HashMap::default();
|
|
|
|
|
|
|
|
vars.insert("POSTWOMAN_TIMESTAMP".to_string(), chrono::Local::now().timestamp().to_string());
|
|
|
|
|
|
|
|
for (k, v) in env {
|
2024-10-19 22:27:58 +02:00
|
|
|
vars.insert(k.to_string(), stringify_toml(v));
|
2024-10-19 21:44:05 +02:00
|
|
|
}
|
2024-10-19 20:36:08 +02:00
|
|
|
|
2024-10-19 03:28:16 +02:00
|
|
|
for (k, v) in std::env::vars() {
|
2024-10-19 20:36:08 +02:00
|
|
|
vars.insert(k, v);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (k, v) in vars {
|
2024-10-19 03:58:43 +02:00
|
|
|
let k_var = format!("${{{k}}}");
|
2024-10-19 03:28:16 +02:00
|
|
|
self.url = self.url.replace(&k_var, &v);
|
|
|
|
if let Some(method) = self.method {
|
|
|
|
self.method = Some(method.replace(&k_var, &v));
|
|
|
|
}
|
|
|
|
if let Some(b) = self.body {
|
|
|
|
match b {
|
|
|
|
StringOr::Str(body) => {
|
|
|
|
self.body = Some(StringOr::Str(body.replace(&k_var, &v)));
|
|
|
|
},
|
|
|
|
StringOr::T(json) => {
|
|
|
|
let wrap = toml::Value::Table(json.clone());
|
|
|
|
let toml::Value::Table(out) = replace_recursive(wrap, &k_var, &v)
|
|
|
|
else { unreachable!("we put in a table, we get out a table") };
|
|
|
|
self.body = Some(StringOr::T(out));
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2024-10-19 19:24:12 +02:00
|
|
|
if let Some(query) = self.query {
|
|
|
|
self.query = Some(
|
|
|
|
query.into_iter()
|
|
|
|
.map(|x| x.replace(&k_var, &v))
|
|
|
|
.collect()
|
|
|
|
);
|
|
|
|
}
|
2024-10-19 03:28:16 +02:00
|
|
|
if let Some(headers) = self.headers {
|
|
|
|
self.headers = Some(
|
|
|
|
headers.into_iter()
|
|
|
|
.map(|x| x.replace(&k_var, &v))
|
|
|
|
.collect()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2024-10-20 01:47:17 +02:00
|
|
|
pub async fn execute(mut self, opts: &ClientConfig) -> Result<String, PostWomanError> {
|
|
|
|
let url = self.url();
|
|
|
|
let body = self.body()?;
|
|
|
|
let method = self.method()?;
|
|
|
|
let headers = self.headers()?;
|
2024-10-19 19:24:12 +02:00
|
|
|
|
2024-10-19 04:14:31 +02:00
|
|
|
let client = reqwest::Client::builder()
|
|
|
|
.user_agent(opts.user_agent.as_deref().unwrap_or(APP_USER_AGENT))
|
2024-10-20 01:33:20 +02:00
|
|
|
.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))
|
2024-10-19 04:14:31 +02:00
|
|
|
.build()?;
|
|
|
|
|
2024-10-20 01:47:17 +02:00
|
|
|
|
2024-10-19 04:14:31 +02:00
|
|
|
let res = client
|
2024-10-19 19:24:12 +02:00
|
|
|
.request(method, url)
|
2024-10-19 03:28:16 +02:00
|
|
|
.headers(headers)
|
|
|
|
.body(body)
|
|
|
|
.send()
|
2024-10-19 22:02:05 +02:00
|
|
|
.await?;
|
|
|
|
|
|
|
|
if res.status().as_u16() != self.expect.unwrap_or(200) {
|
|
|
|
return Err(PostWomanError::UnexpectedStatusCode(res));
|
|
|
|
}
|
2024-10-19 03:28:16 +02:00
|
|
|
|
|
|
|
Ok(match self.extract.unwrap_or_default() {
|
2024-10-20 01:32:05 +02:00
|
|
|
StringOr::T(ExtractorConfig::Discard) => "".to_string(),
|
|
|
|
StringOr::T(ExtractorConfig::Body) => format_body(res).await?,
|
|
|
|
StringOr::T(ExtractorConfig::Debug) => {
|
2024-10-19 20:18:34 +02:00
|
|
|
// TODO needless double format
|
|
|
|
let res_dbg = format!("{res:#?}");
|
|
|
|
let body = format_body(res).await?;
|
|
|
|
format!("{res_dbg}\nBody: {body}\n")
|
|
|
|
},
|
2024-10-20 01:32:05 +02:00
|
|
|
StringOr::T(ExtractorConfig::Header { key }) => res
|
2024-10-19 03:28:16 +02:00
|
|
|
.headers()
|
|
|
|
.get(&key)
|
|
|
|
.ok_or(PostWomanError::HeaderNotFound(key))?
|
|
|
|
.to_str()?
|
2024-10-19 05:02:18 +02:00
|
|
|
.to_string()
|
|
|
|
+ "\n",
|
2024-10-20 01:32:05 +02:00
|
|
|
StringOr::T(ExtractorConfig::Regex { pattern }) => {
|
2024-10-19 20:18:34 +02:00
|
|
|
let pattern = regex::Regex::new(&pattern)?;
|
|
|
|
let body = format_body(res).await?;
|
|
|
|
pattern.find(&body)
|
|
|
|
.ok_or_else(|| PostWomanError::NoMatch(body.clone()))?
|
|
|
|
.as_str()
|
|
|
|
.to_string()
|
2024-10-19 22:05:29 +02:00
|
|
|
+ "\n"
|
2024-10-19 20:18:34 +02:00
|
|
|
},
|
2024-10-19 20:19:34 +02:00
|
|
|
// bare string defaults to JQL query
|
2024-10-20 01:32:05 +02:00
|
|
|
StringOr::T(ExtractorConfig::JQ { query }) | StringOr::Str(query) => {
|
2024-10-19 20:19:34 +02:00
|
|
|
let json: serde_json::Value = res.json().await?;
|
2024-10-19 21:44:05 +02:00
|
|
|
let selection = jq(&query, json)?;
|
|
|
|
if selection.len() == 1 {
|
2024-10-19 22:27:58 +02:00
|
|
|
stringify_json(&selection[0]) + "\n"
|
2024-10-19 21:44:05 +02:00
|
|
|
} else {
|
|
|
|
serde_json::to_string_pretty(&selection)? + "\n"
|
|
|
|
}
|
2024-10-19 20:19:34 +02:00
|
|
|
},
|
2024-10-19 03:28:16 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-20 00:53:49 +02:00
|
|
|
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)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-19 05:28:44 +02:00
|
|
|
async fn format_body(res: reqwest::Response) -> Result<String, PostWomanError> {
|
|
|
|
match res.headers().get("Content-Type") {
|
|
|
|
None => Ok(res.text().await? + "\n"),
|
|
|
|
Some(v) => match v.to_str()? {
|
|
|
|
"application/json" => Ok(serde_json::to_string_pretty(&res.json::<serde_json::Value>().await?)? + "\n"),
|
2024-10-19 22:02:18 +02:00
|
|
|
"text/plain" | "text/html" => Ok(res.text().await? + "\n"),
|
2024-10-19 05:28:44 +02:00
|
|
|
_ => Ok(format!("base64({})\n", BASE64_STANDARD.encode(res.bytes().await?))),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2024-10-19 21:44:05 +02:00
|
|
|
|
|
|
|
fn jq(query: &str, value: serde_json::Value) -> Result<Vec<serde_json::Value>, PostWomanError> {
|
|
|
|
// TODO am i not getting jaq api? or is it just this weird????
|
|
|
|
let mut defs = jaq_interpret::ParseCtx::new(Vec::new());
|
|
|
|
let (filter, errs) = jaq_parse::parse(query, jaq_parse::main());
|
|
|
|
let Some(filter) = filter else {
|
|
|
|
return Err(PostWomanError::JQError(
|
|
|
|
errs.into_iter().map(|x| format!("{x:?}")).collect::<Vec<String>>().join(", ")
|
|
|
|
));
|
|
|
|
};
|
|
|
|
let out: Vec<serde_json::Value> = defs
|
|
|
|
.compile(filter)
|
|
|
|
.run((
|
|
|
|
jaq_interpret::Ctx::new([], &jaq_interpret::RcIter::new(core::iter::empty())),
|
|
|
|
jaq_interpret::Val::from(value)
|
|
|
|
))
|
|
|
|
.filter_map(|x| Some(x.ok()?.into()))
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
Ok(out)
|
|
|
|
}
|