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

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CallTemplateFunctionPurpose } from "./CallTemplateFunctionPurpose";
export type CallTemplateFunctionArgs = { purpose: CallTemplateFunctionPurpose, values: { [key: string]: string }, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CallTemplateFunctionPurpose = { "type": "send" } | { "type": "preview" };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CallTemplateFunctionArgs } from "./CallTemplateFunctionArgs";
export type CallTemplateFunctionRequest = { name: string, pluginRefId: string, args: CallTemplateFunctionArgs, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { TemplateFunction } from "./TemplateFunction";
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };

View File

@@ -2,6 +2,7 @@
import type { BootRequest } from "./BootRequest";
import type { BootResponse } from "./BootResponse";
import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest";
import type { CallTemplateFunctionRequest } from "./CallTemplateFunctionRequest";
import type { CopyTextRequest } from "./CopyTextRequest";
import type { EmptyResponse } from "./EmptyResponse";
import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest";
@@ -11,6 +12,7 @@ import type { FilterResponse } from "./FilterResponse";
import type { GetHttpRequestActionsResponse } from "./GetHttpRequestActionsResponse";
import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest";
import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
import type { GetTemplateFunctionsResponse } from "./GetTemplateFunctionsResponse";
import type { ImportRequest } from "./ImportRequest";
import type { ImportResponse } from "./ImportResponse";
import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest";
@@ -19,4 +21,4 @@ import type { SendHttpRequestRequest } from "./SendHttpRequestRequest";
import type { SendHttpRequestResponse } from "./SendHttpRequestResponse";
import type { ShowToastRequest } from "./ShowToastRequest";
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "empty_response" } & EmptyResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "empty_response" } & EmptyResponse;

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { TemplateFunctionArg } from "./TemplateFunctionArg";
export type TemplateFunction = { name: string, args: Array<TemplateFunctionArg>, };

View File

@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { TemplateFunctionHttpRequestArg } from "./TemplateFunctionHttpRequestArg";
import type { TemplateFunctionSelectArg } from "./TemplateFunctionSelectArg";
import type { TemplateFunctionTextArg } from "./TemplateFunctionTextArg";
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg;

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type TemplateFunctionBaseArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type TemplateFunctionHttpRequestArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { TemplateFunctionSelectOption } from "./TemplateFunctionSelectOption";
export type TemplateFunctionSelectArg = { options: Array<TemplateFunctionSelectOption>, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type TemplateFunctionSelectOption = { name: string, value: string, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type TemplateFunctionTextArg = { placeholder?: string | null, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };

View File

@@ -4,12 +4,16 @@ export type * from './themes';
// TODO: The next ts-rs release includes the ability to put everything in 1 file!
export * from './gen/BootRequest';
export * from './gen/BootResponse';
export * from './gen/CallHttpRequestActionRequest';
export * from './gen/CallHttpRequestActionArgs';
export * from './gen/CallTemplateFunctionPurpose';
export * from './gen/CallHttpRequestActionRequest';
export * from './gen/CallTemplateFunctionRequest';
export * from './gen/CallTemplateFunctionArgs';
export * from './gen/Cookie';
export * from './gen/CookieDomain';
export * from './gen/CookieExpires';
export * from './gen/CookieJar';
export * from './gen/CopyTextRequest';
export * from './gen/EmptyResponse';
export * from './gen/Environment';
export * from './gen/EnvironmentVariable';
@@ -20,8 +24,8 @@ export * from './gen/FilterResponse';
export * from './gen/Folder';
export * from './gen/GetHttpRequestActionsResponse';
export * from './gen/GetHttpRequestByIdRequest';
export * from './gen/CopyTextRequest';
export * from './gen/GetHttpRequestByIdResponse';
export * from './gen/GetTemplateFunctionsResponse';
export * from './gen/GrpcConnection';
export * from './gen/GrpcEvent';
export * from './gen/GrpcMetadataEntry';
@@ -39,12 +43,19 @@ export * from './gen/InternalEvent';
export * from './gen/InternalEventPayload';
export * from './gen/KeyValue';
export * from './gen/Model';
export * from './gen/SendHttpRequestRequest';
export * from './gen/ToastVariant';
export * from './gen/ShowToastRequest';
export * from './gen/RenderHttpRequestRequest';
export * from './gen/RenderHttpRequestResponse';
export * from './gen/SendHttpRequestRequest';
export * from './gen/SendHttpRequestResponse';
export * from './gen/SendHttpRequestResponse';
export * from './gen/Settings';
export * from './gen/ShowToastRequest';
export * from './gen/TemplateFunction';
export * from './gen/TemplateFunctionArg';
export * from './gen/TemplateFunctionBaseArg';
export * from './gen/TemplateFunctionHttpRequestArg';
export * from './gen/TemplateFunctionSelectArg';
export * from './gen/TemplateFunctionSelectOption';
export * from './gen/TemplateFunctionTextArg';
export * from './gen/ToastVariant';
export * from './gen/Workspace';

View File

@@ -6,7 +6,7 @@ import { SendHttpRequestRequest } from '../gen/SendHttpRequestRequest';
import { SendHttpRequestResponse } from '../gen/SendHttpRequestResponse';
import { ShowToastRequest } from '../gen/ShowToastRequest';
export type YaakContext = {
export type Context = {
clipboard: {
copyText(text: string): void;
};

View File

@@ -1,13 +1,13 @@
import { YaakContext } from './context';
import { Context } from './Context';
export type FilterPluginResponse = string[];
export type FilterPlugin = {
name: string;
description?: string;
canFilter(ctx: YaakContext, args: { mimeType: string }): Promise<boolean>;
canFilter(ctx: Context, args: { mimeType: string }): Promise<boolean>;
onFilter(
ctx: YaakContext,
ctx: Context,
args: { payload: string; mimeType: string },
): Promise<FilterPluginResponse>;
};

View File

@@ -1,7 +1,7 @@
import { CallHttpRequestActionArgs } from '../gen/CallHttpRequestActionArgs';
import { HttpRequestAction } from '../gen/HttpRequestAction';
import { YaakContext } from './context';
import { Context } from './Context';
export type HttpRequestActionPlugin = HttpRequestAction & {
onSelect(ctx: YaakContext, args: CallHttpRequestActionArgs): Promise<void> | void;
onSelect(ctx: Context, args: CallHttpRequestActionArgs): Promise<void> | void;
};

View File

@@ -3,7 +3,7 @@ import { Folder } from '../gen/Folder';
import { HttpRequest } from '../gen/HttpRequest';
import { Workspace } from '../gen/Workspace';
import { AtLeast } from '../helpers';
import { YaakContext } from './context';
import { Context } from './Context';
export type ImportPluginResponse = null | {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
@@ -15,5 +15,5 @@ export type ImportPluginResponse = null | {
export type ImporterPlugin = {
name: string;
description?: string;
onImport(ctx: YaakContext, args: { text: string }): Promise<ImportPluginResponse>;
onImport(ctx: Context, args: { text: string }): Promise<ImportPluginResponse>;
};

View File

@@ -0,0 +1,7 @@
import { CallTemplateFunctionArgs } from '../gen/CallTemplateFunctionArgs';
import { TemplateFunction } from '../gen/TemplateFunction';
import { Context } from './Context';
export type TemplateFunctionPlugin = TemplateFunction & {
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string>;
};

View File

@@ -0,0 +1,8 @@
import { Theme } from '../themes';
import { Context } from './Context';
export type ThemePlugin = {
name: string;
description?: string;
getTheme(ctx: Context, fileContents: string): Promise<Theme>;
};

View File

@@ -1,16 +1,18 @@
import { FilterPlugin } from './filter';
import { HttpRequestActionPlugin } from './httpRequestAction';
import { ImporterPlugin } from './import';
import { ThemePlugin } from './theme';
import { FilterPlugin } from './FilterPlugin';
import { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import { ImporterPlugin } from './ImporterPlugin';
import { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
import { ThemePlugin } from './ThemePlugin';
export type { YaakContext } from './context';
export type { Context } from './Context';
/**
* The global structure of a Yaak plugin
*/
export type YaakPlugin = {
export type Plugin = {
importer?: ImporterPlugin;
theme?: ThemePlugin;
filter?: FilterPlugin;
httpRequestActions?: HttpRequestActionPlugin[];
templateFunctions?: TemplateFunctionPlugin[];
};

View File

@@ -1,8 +0,0 @@
import { Theme } from '../themes';
import { YaakContext } from './context';
export type ThemePlugin = {
name: string;
description?: string;
getTheme(ctx: YaakContext, fileContents: string): Promise<Theme>;
};

View File

@@ -6,9 +6,11 @@ import {
InternalEventPayload,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
} from '@yaakapp/api';
import { YaakContext } from '@yaakapp/api/lib/plugins/context';
import { Context } from '@yaakapp/api';
import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/httpRequestAction';
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import interceptStdout from 'intercept-stdout';
import * as console from 'node:console';
import { readFileSync } from 'node:fs';
@@ -88,7 +90,7 @@ new Promise<void>(async (resolve, reject) => {
return promise as unknown as Promise<T>;
}
const ctx: YaakContext = {
const ctx: Context = {
clipboard: {
async copyText(text) {
await sendAndWaitForReply({ type: 'copy_text_request', text });
@@ -181,8 +183,8 @@ new Promise<void>(async (resolve, reject) => {
const reply: HttpRequestAction[] = mod.plugin.httpRequestActions.map(
(a: HttpRequestActionPlugin) => ({
...a,
onSelect: undefined,
// Add everything except onSelect
onSelect: undefined,
}),
);
const replyPayload: InternalEventPayload = {
@@ -194,6 +196,26 @@ new Promise<void>(async (resolve, reject) => {
return;
}
if (
payload.type === 'get_template_functions_request' &&
Array.isArray(mod.plugin?.templateFunctions)
) {
const reply: TemplateFunction[] = mod.plugin.templateFunctions.map(
(a: TemplateFunctionPlugin) => ({
...a,
// Add everything except render
onRender: undefined,
}),
);
const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response',
pluginRefId,
functions: reply,
};
sendPayload(replyPayload, replyId);
return;
}
if (
payload.type === 'call_http_request_action_request' &&
Array.isArray(mod.plugin?.httpRequestActions)
@@ -205,6 +227,18 @@ new Promise<void>(async (resolve, reject) => {
return;
}
}
if (
payload.type === 'call_template_function_request' &&
Array.isArray(mod.plugin?.templateFunctions)
) {
const action = mod.plugin.templateFunctions.find((a) => a.name === payload.name);
if (typeof action?.onRender() === 'function') {
await action.onRender(ctx, payload.args);
sendEmpty(replyId);
return;
}
}
} catch (err) {
console.log('Plugin call threw exception', payload.type, err);
// TODO: Return errors to server

5
src-tauri/Cargo.lock generated
View File

@@ -6218,9 +6218,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.39.2"
version = "1.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
dependencies = [
"backtrace",
"bytes",
@@ -7651,6 +7651,7 @@ dependencies = [
"log",
"serde",
"serde_json",
"tokio",
"ts-rs",
]

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}")),
}
}
}

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -17,8 +18,7 @@ pub struct InternalEvent {
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export)]
pub enum InternalEventPayload {
BootRequest(BootRequest),
@@ -40,6 +40,10 @@ pub enum InternalEventPayload {
GetHttpRequestActionsResponse(GetHttpRequestActionsResponse),
CallHttpRequestActionRequest(CallHttpRequestActionRequest),
GetTemplateFunctionsRequest,
GetTemplateFunctionsResponse(GetTemplateFunctionsResponse),
CallTemplateFunctionRequest(CallTemplateFunctionRequest),
CopyTextRequest(CopyTextRequest),
RenderHttpRequestRequest(RenderHttpRequestRequest),
@@ -180,6 +184,110 @@ impl Default for ToastVariant {
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct GetTemplateFunctionsResponse {
pub functions: Vec<TemplateFunction>,
pub plugin_ref_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunction {
pub name: String,
pub args: Vec<TemplateFunctionArg>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export)]
pub enum TemplateFunctionArg {
Text(TemplateFunctionTextArg),
Select(TemplateFunctionSelectArg),
HttpRequest(TemplateFunctionHttpRequestArg),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionBaseArg {
pub name: String,
#[ts(optional = nullable)]
pub optional: Option<bool>,
#[ts(optional = nullable)]
pub label: Option<String>,
#[ts(optional = nullable)]
pub default_value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionTextArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
#[ts(optional = nullable)]
pub placeholder: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionHttpRequestArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionSelectArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
pub options: Vec<TemplateFunctionSelectOption>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionSelectOption {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallTemplateFunctionRequest {
pub name: String,
pub plugin_ref_id: String,
pub args: CallTemplateFunctionArgs,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallTemplateFunctionArgs {
pub purpose: CallTemplateFunctionPurpose,
pub values: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export)]
pub enum CallTemplateFunctionPurpose {
Send,
Preview,
}
impl Default for CallTemplateFunctionPurpose{
fn default() -> Self {
CallTemplateFunctionPurpose::Preview
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]

View File

@@ -1,5 +1,5 @@
use crate::error::Result;
use crate::events::{CallHttpRequestActionRequest, FilterRequest, FilterResponse, GetHttpRequestActionsResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload};
use crate::events::{CallHttpRequestActionRequest, CallTemplateFunctionRequest, FilterRequest, FilterResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload};
use crate::error::Error::PluginErr;
use crate::nodejs::start_nodejs_plugin_runtime;
@@ -74,6 +74,22 @@ impl PluginManager {
Ok(all_actions)
}
pub async fn run_template_functions(&self) -> Result<Vec<GetTemplateFunctionsResponse>> {
let reply_events = self
.server
.send_and_wait(&InternalEventPayload::GetTemplateFunctionsRequest)
.await?;
let mut all_actions = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetTemplateFunctionsResponse(resp) = event.payload {
all_actions.push(resp.clone());
}
}
Ok(all_actions)
}
pub async fn call_http_request_action(&self, req: CallHttpRequestActionRequest) -> Result<()> {
let plugin = self.server.plugin_by_ref_id(req.plugin_ref_id.as_str()).await?;
let event = plugin.build_event_to_send(&InternalEventPayload::CallHttpRequestActionRequest(req), None);
@@ -81,6 +97,13 @@ impl PluginManager {
Ok(())
}
pub async fn call_template_function(&self, req: CallTemplateFunctionRequest) -> Result<()> {
let plugin = self.server.plugin_by_ref_id(req.plugin_ref_id.as_str()).await?;
let event = plugin.build_event_to_send(&InternalEventPayload::CallTemplateFunctionRequest(req), None);
plugin.send(&event).await?;
Ok(())
}
pub async fn run_import(&self, content: &str) -> Result<(ImportResponse, String)> {
let reply_events = self
.server

View File

@@ -8,3 +8,4 @@ log = "0.4.22"
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
ts-rs = { version = "9.0.1" }
tokio = { version = "1.39.3", features = ["macros", "rt"] }

View File

@@ -1,30 +1,35 @@
use crate::{FnArg, Parser, Token, Tokens, Val};
use log::warn;
use std::collections::HashMap;
use std::future::Future;
type TemplateCallback = fn(name: &str, args: HashMap<String, String>) -> Result<String, String>;
pub fn parse_and_render(
template: &str,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
let mut p = Parser::new(template);
let tokens = p.parse();
render(tokens, vars, cb)
pub trait TemplateCallback {
fn run(&self, fn_name: &str, args: HashMap<String, String>) -> impl Future<Output = Result<String, String>> + Send;
}
pub fn render(
tokens: Tokens,
pub async fn parse_and_render<T>(
template: &str,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
cb: &Box<T>,
) -> String
where
T: TemplateCallback,
{
let mut p = Parser::new(template);
let tokens = p.parse();
render(tokens, vars, cb).await
}
pub async fn render<T>(tokens: Tokens, vars: &HashMap<String, String>, cb: &Box<T>) -> String
where
T: TemplateCallback,
{
let mut doc_str: Vec<String> = Vec::new();
for t in tokens.tokens {
match t {
Token::Raw { text } => doc_str.push(text),
Token::Tag { val } => doc_str.push(render_tag(val, &vars, cb)),
Token::Tag { val } => doc_str.push(render_tag(val, &vars, cb).await),
Token::Eof => {}
}
}
@@ -32,7 +37,10 @@ pub fn render(
doc_str.join("")
}
fn render_tag(val: Val, vars: &HashMap<String, String>, cb: Option<TemplateCallback>) -> String {
async fn render_tag<T>(val: Val, vars: &HashMap<String, String>, cb: &Box<T>) -> String
where
T: TemplateCallback,
{
match val {
Val::Str { text } => text.into(),
Val::Var { name } => match vars.get(name.as_str()) {
@@ -41,9 +49,9 @@ fn render_tag(val: Val, vars: &HashMap<String, String>, cb: Option<TemplateCallb
},
Val::Fn { name, args } => {
let empty = "".to_string();
let resolved_args = args
.iter()
.map(|a| match a {
let mut resolved_args: HashMap<String, String> = HashMap::new();
for a in args {
let (k, v) = match a {
FnArg {
name,
value: Val::Str { text },
@@ -56,113 +64,161 @@ fn render_tag(val: Val, vars: &HashMap<String, String>, cb: Option<TemplateCallb
vars.get(var_name.as_str()).unwrap_or(&empty).to_string(),
),
FnArg { name, value: val } => {
(name.to_string(), render_tag(val.clone(), vars, cb))
let r = Box::pin(render_tag(val.clone(), vars, cb)).await;
(name.to_string(), r)
}
})
.collect::<HashMap<String, String>>();
match cb {
Some(cb) => match cb(name.as_str(), resolved_args.clone()) {
Ok(s) => s,
Err(e) => {
warn!(
"Failed to run template callback {}({:?}): {}",
name, resolved_args, e
);
"".to_string()
}
},
None => "".into(),
};
resolved_args.insert(k, v);
}
match cb.run(name.as_str(), resolved_args.clone()).await {
Ok(s) => s,
Err(e) => {
warn!(
"Failed to run template callback {}({:?}): {}",
name, resolved_args, e
);
"".to_string()
}
}
}
Val::Null => "".into()
Val::Null => "".into(),
}
}
#[cfg(test)]
mod tests {
use crate::renderer::TemplateCallback;
use crate::*;
use std::collections::HashMap;
use crate::*;
struct EmptyCB {}
#[test]
fn render_empty() {
impl TemplateCallback for EmptyCB {
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String, String>{
todo!()
}
}
#[tokio::test]
async fn render_empty() {
let empty_cb = Box::new(EmptyCB {});
let template = "";
let vars = HashMap::new();
let result = "";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
result.to_string()
);
}
#[test]
fn render_text_only() {
#[tokio::test]
async fn render_text_only() {
let empty_cb = Box::new(EmptyCB {});
let template = "Hello World!";
let vars = HashMap::new();
let result = "Hello World!";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
result.to_string()
);
}
#[test]
fn render_simple() {
#[tokio::test]
async fn render_simple() {
let empty_cb = Box::new(EmptyCB {});
let template = "${[ foo ]}";
let vars = HashMap::from([("foo".to_string(), "bar".to_string())]);
let result = "bar";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
result.to_string()
);
}
#[test]
fn render_surrounded() {
#[tokio::test]
async fn render_surrounded() {
let empty_cb = Box::new(EmptyCB {});
let template = "hello ${[ word ]} world!";
let vars = HashMap::from([("word".to_string(), "cruel".to_string())]);
let result = "hello cruel world!";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
result.to_string()
);
}
#[test]
fn render_valid_fn() {
#[tokio::test]
async fn render_valid_fn() {
let vars = HashMap::new();
let template = r#"${[ say_hello(a="John", b="Kate") ]}"#;
let result = r#"say_hello: 2, Some("John") Some("Kate")"#;
fn cb(name: &str, args: HashMap<String, String>) -> Result<String, String> {
Ok(format!(
"{name}: {}, {:?} {:?}",
args.len(),
args.get("a"),
args.get("b")
))
struct CB {}
impl TemplateCallback for CB {
async fn run(
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> Result<String, String> {
Ok(format!(
"{fn_name}: {}, {:?} {:?}",
args.len(),
args.get("a"),
args.get("b")
))
}
}
assert_eq!(parse_and_render(template, &vars, Some(cb)), result);
assert_eq!(
parse_and_render(template, &vars, &Box::new(CB {})).await,
result
);
}
#[test]
fn render_nested_fn() {
#[tokio::test]
async fn render_nested_fn() {
let vars = HashMap::new();
let template = r#"${[ upper(foo=secret()) ]}"#;
let result = r#"ABC"#;
fn cb(name: &str, args: HashMap<String, String>) -> Result<String, String> {
Ok(match name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
_ => "".to_string(),
})
struct CB {}
impl TemplateCallback for CB {
async fn run(
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> Result<String, String> {
Ok(match fn_name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
_ => "".to_string(),
})
}
}
assert_eq!(
parse_and_render(template, &vars, Some(cb)),
parse_and_render(template, &vars, &Box::new(CB {})).await,
result.to_string()
);
}
#[test]
fn render_fn_err() {
#[tokio::test]
async fn render_fn_err() {
let vars = HashMap::new();
let template = r#"${[ error() ]}"#;
let result = r#""#;
fn cb(_name: &str, _args: HashMap<String, String>) -> Result<String, String> {
Err("Failed to do it!".to_string())
struct CB {}
impl TemplateCallback for CB {
async fn run(
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
Err("Failed to do it!".to_string())
}
}
assert_eq!(
parse_and_render(template, &vars, Some(cb)),
parse_and_render(template, &vars, &Box::new(CB {})).await,
result.to_string()
);
}

View File

@@ -1,88 +1,19 @@
import type { HttpRequest } from '@yaakapp/api';
export interface TemplateFunctionArgBase {
name: string;
optional?: boolean;
label?: string;
}
export interface TemplateFunctionSelectArg extends TemplateFunctionArgBase {
type: 'select';
defaultValue?: string;
options: readonly { name: string; value: string }[];
}
export interface TemplateFunctionTextArg extends TemplateFunctionArgBase {
type: 'text';
defaultValue?: string;
placeholder?: string;
}
export interface TemplateFunctionHttpRequestArg extends TemplateFunctionArgBase {
type: HttpRequest['model'];
}
export type TemplateFunctionArg =
| TemplateFunctionSelectArg
| TemplateFunctionTextArg
| TemplateFunctionHttpRequestArg;
export interface TemplateFunction {
name: string;
args: TemplateFunctionArg[];
}
import { useQuery } from '@tanstack/react-query';
import type { GetTemplateFunctionsResponse } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
export function useTemplateFunctions() {
const fns: TemplateFunction[] = [
{
name: 'timestamp',
args: [
{
type: 'text',
name: 'from',
label: 'From',
placeholder: '2023-23-12T04:03:03',
optional: true,
},
{
type: 'select',
label: 'Format',
name: 'format',
options: [
{ name: 'RFC3339', value: 'rfc3339' },
{ name: 'Unix', value: 'unix' },
{ name: 'Unix (ms)', value: 'unix_millis' },
],
optional: true,
defaultValue: 'rfc3339',
},
],
const result = useQuery({
queryKey: ['template_functions'],
refetchOnWindowFocus: false,
queryFn: async () => {
const responses = (await invokeCmd(
'cmd_template_functions',
)) as GetTemplateFunctionsResponse[];
return responses;
},
{
name: 'response',
args: [
{
type: 'http_request',
name: 'request',
label: 'Request',
},
{
type: 'select',
name: 'attribute',
label: 'Attribute',
options: [
{ name: 'Body', value: 'body' },
{ name: 'Header', value: 'header' },
],
},
{
type: 'text',
name: 'filter',
label: 'Filter',
placeholder: 'JSONPath or XPath expression',
},
],
},
];
});
const fns = result.data?.flatMap((r) => r.functions) ?? [];
return fns;
}

View File

@@ -58,6 +58,7 @@ type TauriCmd =
| 'cmd_send_http_request'
| 'cmd_set_key_value'
| 'cmd_set_update_mode'
| 'cmd_template_functions'
| 'cmd_track_event'
| 'cmd_update_cookie_jar'
| 'cmd_update_environment'