Async template functions working

This commit is contained in:
Gregory Schier
2024-08-19 06:21:03 -07:00
parent ec22191409
commit 1fbcfeaa30
32 changed files with 618 additions and 393 deletions

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fs;
use std::fs::{create_dir_all, File};
use std::io::Write;
@@ -6,8 +7,8 @@ use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use crate::render::variables_from_environment;
use crate::{render, response_err};
use crate::render::render_request;
use crate::response_err;
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue};
@@ -16,6 +17,7 @@ use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::Method;
use reqwest::{multipart, Url};
use serde_json::Value;
use tauri::{Manager, Runtime, WebviewWindow};
use tokio::sync::oneshot;
use tokio::sync::watch::Receiver;
@@ -26,19 +28,18 @@ use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_j
pub async fn send_http_request<R: Runtime>(
window: &WebviewWindow<R>,
request: HttpRequest,
request: &HttpRequest,
response: &HttpResponse,
environment: Option<Environment>,
cookie_jar: Option<CookieJar>,
cancel_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> {
let environment_ref = environment.as_ref();
let workspace = get_workspace(window, &request.workspace_id)
.await
.expect("Failed to get Workspace");
let vars = variables_from_environment(&workspace, environment_ref);
let rendered_request = render_request(&request, &workspace, environment.as_ref()).await;
let mut url_string = render::render(&request.url, &vars);
let mut url_string = rendered_request.url;
url_string = ensure_proto(&url_string);
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
@@ -115,7 +116,7 @@ pub async fn send_http_request<R: Runtime>(
}
};
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
let m = Method::from_bytes(rendered_request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let mut request_builder = client.request(m, url);
@@ -138,7 +139,7 @@ pub async fn send_http_request<R: Runtime>(
// );
// }
for h in request.headers {
for h in rendered_request.headers {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
@@ -147,17 +148,14 @@ pub async fn send_http_request<R: Runtime>(
continue;
}
let name = render::render(&h.name, &vars);
let value = render::render(&h.value, &vars);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
let header_name = match HeaderName::from_bytes(h.name.as_bytes()) {
Ok(n) => n,
Err(e) => {
error!("Failed to create header name: {}", e);
continue;
}
};
let header_value = match HeaderValue::from_str(value.as_str()) {
let header_value = match HeaderValue::from_str(h.value.as_str()) {
Ok(n) => n,
Err(e) => {
error!("Failed to create header value: {}", e);
@@ -168,23 +166,21 @@ pub async fn send_http_request<R: Runtime>(
headers.insert(header_name, header_value);
}
if let Some(b) = &request.authentication_type {
if let Some(b) = &rendered_request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap();
let a = request.authentication;
let a = rendered_request.authentication;
if b == "basic" {
let raw_username = a
let username = a
.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let raw_password = a
.unwrap_or_default();
let password = a
.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &vars);
let password = render::render(raw_password, &vars);
.unwrap_or_default();
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
@@ -193,8 +189,11 @@ pub async fn send_http_request<R: Runtime>(
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
);
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &vars);
let token = a
.get("token")
.unwrap_or(empty_value)
.as_str()
.unwrap_or_default();
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
@@ -203,56 +202,38 @@ pub async fn send_http_request<R: Runtime>(
}
let mut query_params = Vec::new();
for p in request.url_parameters {
for p in rendered_request.url_parameters {
if !p.enabled || p.name.is_empty() {
continue;
}
query_params.push((
render::render(&p.name, &vars),
render::render(&p.value, &vars),
));
query_params.push((p.name, p.value));
}
request_builder = request_builder.query(&query_params);
if let Some(body_type) = &request.body_type {
let empty_string = &serde_json::to_value("").unwrap();
let empty_bool = &serde_json::to_value(false).unwrap();
let request_body = request.body;
let request_body = rendered_request.body;
if let Some(body_type) = &rendered_request.body_type {
if request_body.contains_key("text") {
let raw_text = request_body
.get("text")
.unwrap_or(empty_string)
.as_str()
.unwrap_or("");
let body = render::render(raw_text, &vars);
request_builder = request_builder.body(body);
let body = get_str_h(&request_body, "text");
request_builder = request_builder.body(body.to_owned());
} else if body_type == "application/x-www-form-urlencoded"
&& request_body.contains_key("form")
{
let mut form_params = Vec::new();
let form = request_body.get("form");
if let Some(f) = form {
for p in f.as_array().unwrap_or(&Vec::new()) {
let enabled = p
.get("enabled")
.unwrap_or(empty_bool)
.as_bool()
.unwrap_or(false);
let name = p
.get("name")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
if !enabled || name.is_empty() {
continue;
match f.as_array() {
None => {}
Some(a) => {
for p in a {
let enabled = get_bool(p, "enabled");
let name = get_str(p, "name");
if !enabled || name.is_empty() {
continue;
}
let value = get_str(p, "value");
form_params.push((name, value));
}
}
let value = p
.get("value")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
form_params.push((render::render(name, &vars), render::render(value, &vars)));
}
}
request_builder = request_builder.form(&form_params);
@@ -274,77 +255,59 @@ pub async fn send_http_request<R: Runtime>(
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
let mut multipart_form = multipart::Form::new();
if let Some(form_definition) = request_body.get("form") {
for p in form_definition.as_array().unwrap_or(&Vec::new()) {
let enabled = p
.get("enabled")
.unwrap_or(empty_bool)
.as_bool()
.unwrap_or(false);
let name_raw = p
.get("name")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
match form_definition.as_array() {
None => {}
Some(fd) => {
for p in fd {
let enabled = get_bool(p, "enabled");
let name = get_str(p, "name").to_string();
if !enabled || name_raw.is_empty() {
continue;
}
let file_path = p
.get("file")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
let value_raw = p
.get("value")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
let name = render::render(name_raw, &vars);
let mut part = if file_path.is_empty() {
multipart::Part::text(render::render(value_raw, &vars))
} else {
match fs::read(file_path) {
Ok(f) => multipart::Part::bytes(f),
Err(e) => {
return response_err(response, e.to_string(), window).await;
if !enabled || name.is_empty() {
continue;
}
let file_path = get_str(p, "file").to_owned();
let value = get_str(p, "value").to_owned();
let mut part = if file_path.is_empty() {
multipart::Part::text(value.clone())
} else {
match fs::read(file_path.clone()) {
Ok(f) => multipart::Part::bytes(f),
Err(e) => {
return response_err(response, e.to_string(), window).await;
}
}
};
let content_type = get_str(p, "contentType");
// Set or guess mimetype
if !content_type.is_empty() {
part = part.mime_str(content_type).map_err(|e| e.to_string())?;
} else if !file_path.is_empty() {
let default_mime =
Mime::from_str("application/octet-stream").unwrap();
let mime =
mime_guess::from_path(file_path.clone()).first_or(default_mime);
part = part
.mime_str(mime.essence_str())
.map_err(|e| e.to_string())?;
}
// Set file path if not empty
if !file_path.is_empty() {
let filename = PathBuf::from(file_path)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
part = part.file_name(filename);
}
multipart_form = multipart_form.part(name, part);
}
};
let ct_raw = p
.get("contentType")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
// Set or guess mimetype
if !ct_raw.is_empty() {
let content_type = render::render(ct_raw, &vars);
part = part
.mime_str(content_type.as_str())
.map_err(|e| e.to_string())?;
} else if !file_path.is_empty() {
let default_mime = Mime::from_str("application/octet-stream").unwrap();
let mime = mime_guess::from_path(file_path).first_or(default_mime);
part = part
.mime_str(mime.essence_str())
.map_err(|e| e.to_string())?;
}
// Set fil path if not empty
if !file_path.is_empty() {
let filename = PathBuf::from(file_path)
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string();
part = part.file_name(filename);
}
multipart_form = multipart_form.part(name, part);
}
}
headers.remove("Content-Type"); // reqwest will add this automatically
@@ -496,3 +459,24 @@ fn ensure_proto(url_str: &str) -> String {
format!("http://{url_str}")
}
fn get_bool(v: &Value, key: &str) -> bool {
match v.get(key) {
None => false,
Some(v) => v.as_bool().unwrap_or_default(),
}
}
fn get_str<'a>(v: &'a Value, key: &str) -> &'a str {
match v.get(key) {
None => "",
Some(v) => v.as_str().unwrap_or_default(),
}
}
fn get_str_h<'a>(v: &'a HashMap<String, Value>, key: &str) -> &'a str {
match v.get(key) {
None => "",
Some(v) => v.as_str().unwrap_or_default(),
}
}

View File

@@ -57,10 +57,10 @@ use yaak_models::queries::{
};
use yaak_plugin_runtime::events::{
CallHttpRequestActionRequest, FilterResponse, GetHttpRequestActionsResponse,
GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload, RenderHttpRequestResponse,
SendHttpRequestResponse,
GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload,
RenderHttpRequestResponse, SendHttpRequestResponse,
};
use yaak_templates::{parse_and_render, Parser, Tokens};
use yaak_templates::{Parser, Tokens};
mod analytics;
mod export_resources;
@@ -128,7 +128,7 @@ async fn cmd_render_template(
let workspace = get_workspace(&window, &workspace_id)
.await
.map_err(|e| e.to_string())?;
let rendered = render_template(template, &workspace, environment.as_ref());
let rendered = render_template(template, &workspace, environment.as_ref()).await;
Ok(rendered)
}
@@ -195,7 +195,7 @@ async fn cmd_grpc_go(
.await
.map_err(|e| e.to_string())?;
let mut metadata = HashMap::new();
let vars = variables_from_environment(&workspace, environment.as_ref());
let vars = variables_from_environment(&workspace, environment.as_ref()).await;
// Add rest of metadata
for h in req.clone().metadata {
@@ -207,8 +207,8 @@ async fn cmd_grpc_go(
continue;
}
let name = render::render(&h.name, &vars);
let value = render::render(&h.value, &vars);
let name = render::render(&h.name, &vars).await;
let value = render::render(&h.value, &vars).await;
metadata.insert(name, value);
}
@@ -229,15 +229,15 @@ async fn cmd_grpc_go(
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &vars);
let password = render::render(raw_password, &vars);
let username = render::render(raw_username, &vars).await;
let password = render::render(raw_password, &vars).await;
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
metadata.insert("Authorization".to_string(), format!("Basic {}", encoded));
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &vars);
let token = render::render(raw_token, &vars).await;
metadata.insert("Authorization".to_string(), format!("Bearer {token}"));
}
}
@@ -355,7 +355,10 @@ async fn cmd_grpc_go(
let w = w.clone();
let base_msg = base_msg.clone();
let method_desc = method_desc.clone();
let msg = render::render(raw_msg.as_str(), &vars);
let vars = vars.clone();
let msg = tauri::async_runtime::block_on(async move {
render::render(raw_msg.as_str(), &vars).await
});
let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc)
{
Ok(d_msg) => d_msg,
@@ -413,7 +416,7 @@ async fn cmd_grpc_go(
} else {
req.message
};
let msg = render::render(&raw_msg, &vars);
let msg = render::render(&raw_msg, &vars).await;
upsert_grpc_event(
&w,
@@ -733,7 +736,7 @@ async fn cmd_send_ephemeral_request(
send_http_request(
&window,
request,
&request,
&response,
environment,
cookie_jar,
@@ -914,6 +917,16 @@ async fn cmd_http_request_actions(
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_template_functions(
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetTemplateFunctionsResponse>, String> {
plugin_manager
.run_template_functions()
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_call_http_request_action(
req: CallHttpRequestActionRequest,
@@ -1057,7 +1070,7 @@ async fn cmd_send_http_request(
send_http_request(
&window,
request.clone(),
&request,
&response,
environment,
cookie_jar,
@@ -1692,6 +1705,7 @@ pub fn run() {
cmd_grpc_go,
cmd_grpc_reflect,
cmd_http_request_actions,
cmd_template_functions,
cmd_import_data,
cmd_list_cookie_jars,
cmd_list_environments,
@@ -1986,7 +2000,7 @@ async fn handle_plugin_event<R: Runtime>(
Some(id) => get_environment(w, id.as_str()).await.ok(),
};
let rendered_http_request =
render_request(&req.http_request, &workspace, environment.as_ref());
render_request(&req.http_request, &workspace, environment.as_ref()).await;
Some(InternalEventPayload::RenderHttpRequestResponse(
RenderHttpRequestResponse {
http_request: rendered_http_request,
@@ -2025,7 +2039,7 @@ async fn handle_plugin_event<R: Runtime>(
let result = send_http_request(
&w,
req.http_request,
&req.http_request,
&resp,
environment,
cookie_jar,

View File

@@ -4,73 +4,77 @@ use std::collections::HashMap;
use yaak_models::models::{
Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace,
};
use yaak_templates::parse_and_render;
use yaak_templates::{parse_and_render, TemplateCallback};
pub fn render_template(template: &str, w: &Workspace, e: Option<&Environment>) -> String {
let vars = &variables_from_environment(w, e);
render(template, vars)
pub async fn render_template(template: &str, w: &Workspace, e: Option<&Environment>) -> String {
let vars = &variables_from_environment(w, e).await;
render(template, vars).await
}
pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -> HttpRequest {
pub async fn render_request(
r: &HttpRequest,
w: &Workspace,
e: Option<&Environment>,
) -> HttpRequest {
let r = r.clone();
let vars = &variables_from_environment(w, e);
let vars = &variables_from_environment(w, e).await;
let mut url_parameters = Vec::new();
for p in r.url_parameters {
url_parameters.push(HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), vars).await,
value: render(p.value.as_str(), vars).await,
})
}
let mut headers = Vec::new();
for p in r.headers {
headers.push(HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), vars).await,
value: render(p.value.as_str(), vars).await,
})
}
let mut body = HashMap::new();
for (k, v) in r.body {
let v = if v.is_string() {
render(v.as_str().unwrap(), vars).await
} else {
v.to_string()
};
body.insert(render(k.as_str(), vars).await, Value::from(v));
}
let mut authentication = HashMap::new();
for (k, v) in r.authentication {
let v = if v.is_string() {
render(v.as_str().unwrap(), vars).await
} else {
v.to_string()
};
authentication.insert(render(k.as_str(), vars).await, Value::from(v));
}
HttpRequest {
url: render(r.url.as_str(), vars),
url_parameters: r
.url_parameters
.iter()
.map(|p| HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), vars),
value: render(p.value.as_str(), vars),
})
.collect::<Vec<HttpUrlParameter>>(),
headers: r
.headers
.iter()
.map(|p| HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), vars),
value: render(p.value.as_str(), vars),
})
.collect::<Vec<HttpRequestHeader>>(),
body: r
.body
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), vars)
} else {
v.to_string()
};
(render(k, vars), Value::from(v))
})
.collect::<HashMap<String, Value>>(),
authentication: r
.authentication
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), vars)
} else {
v.to_string()
};
(render(k, vars), Value::from(v))
})
.collect::<HashMap<String, Value>>(),
url: render(r.url.as_str(), vars).await,
url_parameters,
headers,
body,
authentication,
..r
}
}
pub fn recursively_render_variables<'s>(
pub async fn recursively_render_variables<'s>(
m: &HashMap<String, String>,
render_count: usize,
) -> HashMap<String, String> {
let mut did_render = false;
let mut new_map = m.clone();
for (k, v) in m.clone() {
let rendered = render(v.as_str(), m);
let rendered = Box::pin(render(v.as_str(), m)).await;
if rendered != v {
did_render = true
}
@@ -78,13 +82,13 @@ pub fn recursively_render_variables<'s>(
}
if did_render && render_count <= 3 {
new_map = recursively_render_variables(&new_map, render_count + 1);
new_map = Box::pin(recursively_render_variables(&new_map, render_count + 1)).await;
}
new_map
}
pub fn variables_from_environment(
pub async fn variables_from_environment(
workspace: &Workspace,
environment: Option<&Environment>,
) -> HashMap<String, String> {
@@ -95,17 +99,22 @@ pub fn variables_from_environment(
variables = add_variable_to_map(variables, &e.variables);
}
recursively_render_variables(&variables, 0)
recursively_render_variables(&variables, 0).await
}
pub fn render(template: &str, vars: &HashMap<String, String>) -> String {
parse_and_render(template, vars, Some(template_callback))
pub async fn render(template: &str, vars: &HashMap<String, String>) -> String {
parse_and_render(template, vars, &Box::new(PluginTemplateCallback::default())).await
}
fn template_callback(name: &str, args: HashMap<String, String>) -> Result<String, String> {
match name {
"timestamp" => timestamp(args),
_ => Err(format!("Unknown template function {name}")),
#[derive(Default)]
struct PluginTemplateCallback {}
impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
match fn_name {
"timestamp" => timestamp(args),
_ => Err(format!("Unknown template function {fn_name}")),
}
}
}