From 8fbf224506b10c4d4469b7cd57b226918f81124d Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 20 Oct 2024 15:28:24 +0200 Subject: [PATCH] feat: allow specifying path base, renamed url>path --- postwoman.toml | 24 +++++++++++++----------- src/fmt.rs | 6 ++++-- src/model/client.rs | 2 ++ src/model/endpoint.rs | 32 +++++++++++++++++++------------- 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/postwoman.toml b/postwoman.toml index 67c23fd..953d2e2 100644 --- a/postwoman.toml +++ b/postwoman.toml @@ -2,17 +2,18 @@ user_agent = "postwoman@sample/0.3.1" timeout = 60 # max time for each request to complete, in seconds redirects = 5 # allow up to five redirects, defaults to none +base = "https://api.alemi.dev" # all route urls will be appended to this base [env] # these will be replaced in routes options. environment vars overrule these PW_TOKEN = "set-me-as-and-environment-variable!" -[route.healthcheck] # the simplest possible route: just name and url -url = "https://api.alemi.dev/" +[route.healthcheck] # the simplest possible route: just name and path +path = "/" [route.debug] -url = "https://api.alemi.dev/debug" +path = "/debug" method = "PUT" # specify request method query = [ # specify query parameters in a more friendly way "body=json", @@ -23,20 +24,20 @@ headers = [ # add custom headers to request "Authorization: Bearer ${PW_TOKEN}", ] body = { hello = "world!", success = true } # body can be a bare string, or an inline table (will be converted to json) -extract = ".path" # extract from json responses with JQ syntax -# note that a bare extractor string is equivalent to `{ type = "jq", query = ".path" }` +extract = { type = "body" } # get the whole response body, this is the default extractor [route.benchmark] -url = "https://api.alemi.dev/look/into/the/void" +path = "/look/into/the/void" extract = { type = "discard" } # if you don't care about the output, discard it! [route.notfound] -url = "https://api.alemi.dev/not-found" -expect = 404 # it's possible to specify expected status code, will fail if doesn't match +path = "https://cdn.alemi.dev/does-not-exist" +absolute = true # mark as absolute to avoid composing with client base url +status = 404 # it's possible to specify expected status code, will fail if doesn't match extract = { type = "regex", pattern = 'nginx/[0-9\.]+' } # extract from response with regex [route.payload] -url = "https://api.alemi.dev/debug" +path = "/debug" method = "POST" body = '''{ "complex": { @@ -46,9 +47,10 @@ body = '''{ "way": true } }''' -extract = { type = "body" } # get the whole response body, this is the default extractor +extract = ".path" # extract from json responses with JQ syntax (default extractor), equivalent to `{ type = "jq", query = ".path" }` +expect = "/debug" # if extracted result doesn't match, this route will return an error [route.cookie] -url = "https://api.alemi.dev/getcookie" +path = "/getcookie" method = "GET" extract = { type = "header", key = "Set-Cookie" } # get a specific response header, ignoring body diff --git a/src/fmt.rs b/src/fmt.rs index f82b14c..09921f0 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -63,8 +63,10 @@ impl PrintableResult for ListResult { println!(" + {key}={}", crate::ext::stringify_toml(&value)); } - for (name, mut endpoint) in collection.route { - println!(" - {name} \t{} \t{}", endpoint.method.as_deref().unwrap_or("GET"), endpoint.url); + for (name, endpoint) in collection.route { + let url = endpoint.url(collection.client.as_ref().and_then(|x| x.base.as_deref())); + let method = endpoint.method.as_deref().unwrap_or("GET"); + println!(" - {name} \t{method} \t{url}"); if ! compact { if let Some(ref query) = endpoint.query { for query in query { diff --git a/src/model/client.rs b/src/model/client.rs index dfcf4ca..ad7bf04 100644 --- a/src/model/client.rs +++ b/src/model/client.rs @@ -1,6 +1,8 @@ #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] pub struct ClientConfig { + /// base url for composing endpoints + pub base: Option, /// user agent for requests, defaults to 'postwoman/' pub user_agent: Option, /// max total duration of each request, in seconds. defaults to 30 diff --git a/src/model/endpoint.rs b/src/model/endpoint.rs index 8428e11..1dbd6a7 100644 --- a/src/model/endpoint.rs +++ b/src/model/endpoint.rs @@ -14,8 +14,10 @@ use super::{ExtractorConfig, ClientConfig}; #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] pub struct EndpointConfig { - /// endpoint url, required - pub url: String, + /// endpoint path, composed from client base and query params + pub path: String, + /// absolute url, don't compose with client base url + pub absolute: Option, /// http method for request, default GET pub method: Option, /// query parameters, appended to base url @@ -31,24 +33,24 @@ pub struct EndpointConfig { } impl EndpointConfig { - pub fn body(&mut self) -> Result { - match self.body.take() { + pub fn body(&self) -> Result { + match &self.body { None => Ok("".to_string()), Some(StringOr::Str(x)) => Ok(x.clone()), Some(StringOr::T(json)) => Ok(serde_json::to_string(&json)?), } } - pub fn method(&mut self) -> Result { + pub fn method(&self) -> Result { match self.method { Some(ref m) => Ok(reqwest::Method::from_str(m)?), None => Ok(reqwest::Method::GET), } } - pub fn headers(&mut self) -> Result { + pub fn headers(&self) -> Result { let mut headers = HeaderMap::default(); - for header in self.headers.take().unwrap_or_default() { + for header in self.headers.as_deref().unwrap_or(&[]) { let (k, v) = header.split_once(':') .ok_or_else(|| InvalidHeaderError::Format(header.clone()))?; headers.insert( @@ -59,9 +61,13 @@ impl EndpointConfig { Ok(headers) } - pub fn url(&mut self) -> String { - let mut url = self.url.clone(); - if let Some(query) = self.query.take() { + pub fn url(&self, base: Option<&str>) -> String { + let mut url = if self.absolute.unwrap_or(false) { + self.path.clone() + } else { + format!("{}{}", base.unwrap_or_default(), self.path) + }; + if let Some(ref query) = self.query { url = format!("{url}?{}", query.join("&")); } url @@ -82,7 +88,7 @@ impl EndpointConfig { for (k, v) in vars { let k_var = format!("${{{k}}}"); - self.url = self.url.replace(&k_var, &v); + self.path = self.path.replace(&k_var, &v); if let Some(method) = self.method { self.method = Some(method.replace(&k_var, &v)); } @@ -118,11 +124,11 @@ impl EndpointConfig { self } - pub async fn execute(mut self, opts: &ClientConfig) -> Result { - let url = self.url(); + pub async fn execute(self, opts: &ClientConfig) -> Result { let body = self.body()?; let method = self.method()?; let headers = self.headers()?; + let url = self.url(opts.base.as_deref()); let client = reqwest::Client::builder() .user_agent(opts.user_agent.as_deref().unwrap_or(APP_USER_AGENT))