Compare commits
2 commits
8544d0d720
...
ac92c53799
Author | SHA1 | Date | |
---|---|---|---|
ac92c53799 | |||
afc554f497 |
8 changed files with 121 additions and 80 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -815,7 +815,7 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "postwoman"
|
name = "postwoman"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
|
include = ["other.toml", "api/auth.toml"] # include other postwoman collections relative to this one
|
||||||
|
|
||||||
|
[env] # these will be replaced in fields and inherited by includes. environment vars overrule these
|
||||||
|
PW_TOKEN = "set-me-as-and-environment-variable!"
|
||||||
|
|
||||||
[client] # HTTP client configuration
|
[client] # HTTP client configuration
|
||||||
user_agent = "postwoman@sample/0.4.1"
|
user_agent = "postwoman@sample/0.4.1"
|
||||||
timeout = 60 # max time for each request to complete, in seconds
|
timeout = 60 # max time for each request to complete, in seconds
|
||||||
redirects = 5 # allow up to five redirects, defaults to none
|
redirects = 5 # allow up to five redirects, defaults to none
|
||||||
base = "https://api.alemi.dev" # all route urls will be appended to this base
|
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 path
|
[route.healthcheck] # the simplest possible route: just name and path
|
||||||
path = "/"
|
path = "/"
|
||||||
|
|
20
src/ext.rs
20
src/ext.rs
|
@ -38,3 +38,23 @@ pub fn stringify_json(v: &serde_json::Value) -> String {
|
||||||
pub fn full_name(namespace: &str, name: &str) -> String {
|
pub fn full_name(namespace: &str, name: &str) -> String {
|
||||||
format!("{namespace}:{name}")
|
format!("{namespace}:{name}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait FillableFromEnvironment {
|
||||||
|
fn fill(self, env: &toml::Table) -> Self;
|
||||||
|
|
||||||
|
fn default_vars(env: &toml::Table) -> std::collections::HashMap<String, String> {
|
||||||
|
let mut vars: std::collections::HashMap<String, String> = std::collections::HashMap::default();
|
||||||
|
|
||||||
|
vars.insert("POSTWOMAN_TIMESTAMP".to_string(), chrono::Local::now().timestamp().to_string());
|
||||||
|
|
||||||
|
for (k, v) in env {
|
||||||
|
vars.insert(k.to_string(), stringify_toml(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (k, v) in std::env::vars() {
|
||||||
|
vars.insert(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
vars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -59,12 +59,12 @@ impl PrintableResult for ListResult {
|
||||||
for (namespace, collection) in collections {
|
for (namespace, collection) in collections {
|
||||||
println!("-> {namespace}");
|
println!("-> {namespace}");
|
||||||
|
|
||||||
for (key, value) in collection.env.unwrap_or_default() {
|
for (key, value) in collection.env {
|
||||||
println!(" + {key}={}", crate::ext::stringify_toml(&value));
|
println!(" + {key}={}", crate::ext::stringify_toml(&value));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (name, endpoint) in collection.route.unwrap_or_default() {
|
for (name, endpoint) in collection.route {
|
||||||
let url = endpoint.url(collection.client.as_ref().and_then(|x| x.base.as_deref()))
|
let url = endpoint.url(collection.client.base.as_deref())
|
||||||
.split('?')
|
.split('?')
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -7,6 +7,7 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
use ext::FillableFromEnvironment;
|
||||||
use fmt::{PrintableResult, ReportableResult};
|
use fmt::{PrintableResult, ReportableResult};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
pub use model::PostWomanCollection;
|
pub use model::PostWomanCollection;
|
||||||
|
@ -79,7 +80,7 @@ fn main() {
|
||||||
|
|
||||||
let mut collections = IndexMap::new();
|
let mut collections = IndexMap::new();
|
||||||
|
|
||||||
if !load_collections(&mut collections, args.collection.clone()) {
|
if !load_collections(&mut collections, args.collection.clone(), &toml::Table::default()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,10 +148,10 @@ async fn run_collection_endpoints(
|
||||||
) {
|
) {
|
||||||
// this is always safe to compile because we tested it beforehand
|
// this is always safe to compile because we tested it beforehand
|
||||||
let pattern = regex::Regex::new(&query).expect("tested it before and still failed here???");
|
let pattern = regex::Regex::new(&query).expect("tested it before and still failed here???");
|
||||||
let client = std::sync::Arc::new(collection.client.unwrap_or_default());
|
let env = std::sync::Arc::new(collection.env);
|
||||||
let env = std::sync::Arc::new(collection.env.unwrap_or_default());
|
let client = std::sync::Arc::new(collection.client.fill(&env));
|
||||||
|
|
||||||
for (name, mut endpoint) in collection.route.unwrap_or_default() {
|
for (name, mut endpoint) in collection.route {
|
||||||
let full_name = ext::full_name(&namespace, &name);
|
let full_name = ext::full_name(&namespace, &name);
|
||||||
if pattern.find(&full_name).is_none() { continue };
|
if pattern.find(&full_name).is_none() { continue };
|
||||||
|
|
||||||
|
@ -195,7 +196,7 @@ async fn run_collection_endpoints(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_collections(store: &mut IndexMap<String, PostWomanCollection>, mut path: std::path::PathBuf) -> bool {
|
fn load_collections(store: &mut IndexMap<String, PostWomanCollection>, mut path: std::path::PathBuf, parent_env: &toml::Table) -> bool {
|
||||||
let collection_raw = match std::fs::read_to_string(&path) {
|
let collection_raw = match std::fs::read_to_string(&path) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -204,7 +205,7 @@ fn load_collections(store: &mut IndexMap<String, PostWomanCollection>, mut path:
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let collection: PostWomanCollection = match toml::from_str(&collection_raw) {
|
let mut collection: PostWomanCollection = match toml::from_str(&collection_raw) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("! error parsing collection {path:?}: {e}");
|
eprintln!("! error parsing collection {path:?}: {e}");
|
||||||
|
@ -212,23 +213,24 @@ fn load_collections(store: &mut IndexMap<String, PostWomanCollection>, mut path:
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
collection.env.extend(parent_env.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||||
|
|
||||||
let name = path.to_string_lossy().replace(".toml", "");
|
let name = path.to_string_lossy().replace(".toml", "");
|
||||||
let mut to_include = Vec::new();
|
let mut to_include = Vec::new();
|
||||||
|
|
||||||
if let Some(ref includes) = collection.include {
|
|
||||||
path.pop();
|
path.pop();
|
||||||
for include in includes {
|
for include in &collection.include {
|
||||||
let mut base = path.clone();
|
let mut base = path.clone();
|
||||||
let new = std::path::PathBuf::from_str(include).expect("infallible");
|
let new = std::path::PathBuf::from_str(include).expect("infallible");
|
||||||
base.push(new);
|
base.push(new);
|
||||||
to_include.push(base);
|
to_include.push(base);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
let parent_env = collection.env.clone();
|
||||||
store.insert(name, collection);
|
store.insert(name, collection);
|
||||||
|
|
||||||
for base in to_include {
|
for base in to_include {
|
||||||
if !load_collections(store, base) {
|
if !load_collections(store, base, &parent_env) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::ext::FillableFromEnvironment;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct ClientConfig {
|
pub struct ClientConfig {
|
||||||
|
@ -12,3 +14,23 @@ pub struct ClientConfig {
|
||||||
/// accept invalid SSL certificates, defaults to false (be careful: this is dangerous!)
|
/// accept invalid SSL certificates, defaults to false (be careful: this is dangerous!)
|
||||||
pub accept_invalid_certs: Option<bool>,
|
pub accept_invalid_certs: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FillableFromEnvironment for ClientConfig {
|
||||||
|
fn fill(mut self, env: &toml::Table) -> Self {
|
||||||
|
let vars = Self::default_vars(env);
|
||||||
|
|
||||||
|
for (k, v) in vars {
|
||||||
|
let k_var = format!("${{{k}}}");
|
||||||
|
|
||||||
|
if let Some(base) = self.base {
|
||||||
|
self.base = Some(base.replace(&k_var, &v));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(user_agent) = self.user_agent {
|
||||||
|
self.user_agent = Some(user_agent.replace(&k_var, &v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
use base64::{prelude::BASE64_STANDARD, Engine};
|
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||||
use http::method::InvalidMethod;
|
use http::method::InvalidMethod;
|
||||||
|
@ -8,7 +8,7 @@ use jaq_interpret::FilterT;
|
||||||
use crate::errors::InvalidHeaderError;
|
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_json, FillableFromEnvironment, StringOr};
|
||||||
use super::{ExtractorConfig, ClientConfig};
|
use super::{ExtractorConfig, ClientConfig};
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,57 +75,6 @@ impl EndpointConfig {
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill(mut self, env: &toml::Table) -> Self {
|
|
||||||
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 {
|
|
||||||
vars.insert(k.to_string(), stringify_toml(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (k, v) in std::env::vars() {
|
|
||||||
vars.insert(k, v);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (k, v) in vars {
|
|
||||||
let k_var = format!("${{{k}}}");
|
|
||||||
self.path = self.path.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));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(query) = self.query {
|
|
||||||
self.query = Some(
|
|
||||||
query.into_iter()
|
|
||||||
.map(|x| x.replace(&k_var, &v))
|
|
||||||
.collect()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(headers) = self.headers {
|
|
||||||
self.headers = Some(
|
|
||||||
headers.into_iter()
|
|
||||||
.map(|x| x.replace(&k_var, &v))
|
|
||||||
.collect()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute(self, opts: &ClientConfig) -> Result<String, PostWomanError> {
|
pub async fn execute(self, opts: &ClientConfig) -> Result<String, PostWomanError> {
|
||||||
let body = self.body()?;
|
let body = self.body()?;
|
||||||
let method = self.method()?;
|
let method = self.method()?;
|
||||||
|
@ -196,6 +145,49 @@ impl EndpointConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FillableFromEnvironment for EndpointConfig {
|
||||||
|
fn fill(mut self, env: &toml::Table) -> Self {
|
||||||
|
let vars = Self::default_vars(env);
|
||||||
|
|
||||||
|
for (k, v) in vars {
|
||||||
|
let k_var = format!("${{{k}}}");
|
||||||
|
self.path = self.path.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));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(query) = self.query {
|
||||||
|
self.query = Some(
|
||||||
|
query.into_iter()
|
||||||
|
.map(|x| x.replace(&k_var, &v))
|
||||||
|
.collect()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(headers) = self.headers {
|
||||||
|
self.headers = Some(
|
||||||
|
headers.into_iter()
|
||||||
|
.map(|x| x.replace(&k_var, &v))
|
||||||
|
.collect()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn replace_recursive(element: toml::Value, from: &str, to: &str) -> toml::Value {
|
fn replace_recursive(element: toml::Value, from: &str, to: &str) -> toml::Value {
|
||||||
match element {
|
match element {
|
||||||
toml::Value::Float(x) => toml::Value::Float(x),
|
toml::Value::Float(x) => toml::Value::Float(x),
|
||||||
|
|
|
@ -8,9 +8,13 @@ pub use extractor::ExtractorConfig;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct PostWomanCollection {
|
pub struct PostWomanCollection {
|
||||||
pub client: Option<ClientConfig>,
|
#[serde(default)]
|
||||||
pub env: Option<toml::Table>,
|
pub client: ClientConfig,
|
||||||
pub include: Option<Vec<String>>,
|
#[serde(default)]
|
||||||
|
pub include: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub env: toml::Table,
|
||||||
|
#[serde(default)]
|
||||||
|
pub route: indexmap::IndexMap<String, EndpointConfig>,
|
||||||
// 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: Option<indexmap::IndexMap<String, EndpointConfig>>,
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue