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 { BootRequest } from "./BootRequest";
import type { BootResponse } from "./BootResponse"; import type { BootResponse } from "./BootResponse";
import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest"; import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest";
import type { CallTemplateFunctionRequest } from "./CallTemplateFunctionRequest";
import type { CopyTextRequest } from "./CopyTextRequest"; import type { CopyTextRequest } from "./CopyTextRequest";
import type { EmptyResponse } from "./EmptyResponse"; import type { EmptyResponse } from "./EmptyResponse";
import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest"; import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest";
@@ -11,6 +12,7 @@ import type { FilterResponse } from "./FilterResponse";
import type { GetHttpRequestActionsResponse } from "./GetHttpRequestActionsResponse"; import type { GetHttpRequestActionsResponse } from "./GetHttpRequestActionsResponse";
import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest"; import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest";
import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse"; import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
import type { GetTemplateFunctionsResponse } from "./GetTemplateFunctionsResponse";
import type { ImportRequest } from "./ImportRequest"; import type { ImportRequest } from "./ImportRequest";
import type { ImportResponse } from "./ImportResponse"; import type { ImportResponse } from "./ImportResponse";
import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest"; import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest";
@@ -19,4 +21,4 @@ import type { SendHttpRequestRequest } from "./SendHttpRequestRequest";
import type { SendHttpRequestResponse } from "./SendHttpRequestResponse"; import type { SendHttpRequestResponse } from "./SendHttpRequestResponse";
import type { ShowToastRequest } from "./ShowToastRequest"; 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! // TODO: The next ts-rs release includes the ability to put everything in 1 file!
export * from './gen/BootRequest'; export * from './gen/BootRequest';
export * from './gen/BootResponse'; export * from './gen/BootResponse';
export * from './gen/CallHttpRequestActionRequest';
export * from './gen/CallHttpRequestActionArgs'; 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/Cookie';
export * from './gen/CookieDomain'; export * from './gen/CookieDomain';
export * from './gen/CookieExpires'; export * from './gen/CookieExpires';
export * from './gen/CookieJar'; export * from './gen/CookieJar';
export * from './gen/CopyTextRequest';
export * from './gen/EmptyResponse'; export * from './gen/EmptyResponse';
export * from './gen/Environment'; export * from './gen/Environment';
export * from './gen/EnvironmentVariable'; export * from './gen/EnvironmentVariable';
@@ -20,8 +24,8 @@ export * from './gen/FilterResponse';
export * from './gen/Folder'; export * from './gen/Folder';
export * from './gen/GetHttpRequestActionsResponse'; export * from './gen/GetHttpRequestActionsResponse';
export * from './gen/GetHttpRequestByIdRequest'; export * from './gen/GetHttpRequestByIdRequest';
export * from './gen/CopyTextRequest';
export * from './gen/GetHttpRequestByIdResponse'; export * from './gen/GetHttpRequestByIdResponse';
export * from './gen/GetTemplateFunctionsResponse';
export * from './gen/GrpcConnection'; export * from './gen/GrpcConnection';
export * from './gen/GrpcEvent'; export * from './gen/GrpcEvent';
export * from './gen/GrpcMetadataEntry'; export * from './gen/GrpcMetadataEntry';
@@ -39,12 +43,19 @@ export * from './gen/InternalEvent';
export * from './gen/InternalEventPayload'; export * from './gen/InternalEventPayload';
export * from './gen/KeyValue'; export * from './gen/KeyValue';
export * from './gen/Model'; export * from './gen/Model';
export * from './gen/SendHttpRequestRequest';
export * from './gen/ToastVariant';
export * from './gen/ShowToastRequest';
export * from './gen/RenderHttpRequestRequest'; export * from './gen/RenderHttpRequestRequest';
export * from './gen/RenderHttpRequestResponse'; export * from './gen/RenderHttpRequestResponse';
export * from './gen/SendHttpRequestRequest';
export * from './gen/SendHttpRequestResponse'; export * from './gen/SendHttpRequestResponse';
export * from './gen/SendHttpRequestResponse'; export * from './gen/SendHttpRequestResponse';
export * from './gen/Settings'; 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'; export * from './gen/Workspace';

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { CallHttpRequestActionArgs } from '../gen/CallHttpRequestActionArgs'; import { CallHttpRequestActionArgs } from '../gen/CallHttpRequestActionArgs';
import { HttpRequestAction } from '../gen/HttpRequestAction'; import { HttpRequestAction } from '../gen/HttpRequestAction';
import { YaakContext } from './context'; import { Context } from './Context';
export type HttpRequestActionPlugin = HttpRequestAction & { 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 { HttpRequest } from '../gen/HttpRequest';
import { Workspace } from '../gen/Workspace'; import { Workspace } from '../gen/Workspace';
import { AtLeast } from '../helpers'; import { AtLeast } from '../helpers';
import { YaakContext } from './context'; import { Context } from './Context';
export type ImportPluginResponse = null | { export type ImportPluginResponse = null | {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[]; workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
@@ -15,5 +15,5 @@ export type ImportPluginResponse = null | {
export type ImporterPlugin = { export type ImporterPlugin = {
name: string; name: string;
description?: 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 { FilterPlugin } from './FilterPlugin';
import { HttpRequestActionPlugin } from './httpRequestAction'; import { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import { ImporterPlugin } from './import'; import { ImporterPlugin } from './ImporterPlugin';
import { ThemePlugin } from './theme'; 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 * The global structure of a Yaak plugin
*/ */
export type YaakPlugin = { export type Plugin = {
importer?: ImporterPlugin; importer?: ImporterPlugin;
theme?: ThemePlugin; theme?: ThemePlugin;
filter?: FilterPlugin; filter?: FilterPlugin;
httpRequestActions?: HttpRequestActionPlugin[]; 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, InternalEventPayload,
RenderHttpRequestResponse, RenderHttpRequestResponse,
SendHttpRequestResponse, SendHttpRequestResponse,
TemplateFunction,
} from '@yaakapp/api'; } 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 { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/httpRequestAction';
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import interceptStdout from 'intercept-stdout'; import interceptStdout from 'intercept-stdout';
import * as console from 'node:console'; import * as console from 'node:console';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
@@ -88,7 +90,7 @@ new Promise<void>(async (resolve, reject) => {
return promise as unknown as Promise<T>; return promise as unknown as Promise<T>;
} }
const ctx: YaakContext = { const ctx: Context = {
clipboard: { clipboard: {
async copyText(text) { async copyText(text) {
await sendAndWaitForReply({ type: 'copy_text_request', 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( const reply: HttpRequestAction[] = mod.plugin.httpRequestActions.map(
(a: HttpRequestActionPlugin) => ({ (a: HttpRequestActionPlugin) => ({
...a, ...a,
onSelect: undefined,
// Add everything except onSelect // Add everything except onSelect
onSelect: undefined,
}), }),
); );
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
@@ -194,6 +196,26 @@ new Promise<void>(async (resolve, reject) => {
return; 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 ( if (
payload.type === 'call_http_request_action_request' && payload.type === 'call_http_request_action_request' &&
Array.isArray(mod.plugin?.httpRequestActions) Array.isArray(mod.plugin?.httpRequestActions)
@@ -205,6 +227,18 @@ new Promise<void>(async (resolve, reject) => {
return; 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) { } catch (err) {
console.log('Plugin call threw exception', payload.type, err); console.log('Plugin call threw exception', payload.type, err);
// TODO: Return errors to server // TODO: Return errors to server

5
src-tauri/Cargo.lock generated
View File

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

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fs; use std::fs;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::io::Write; use std::io::Write;
@@ -6,8 +7,8 @@ use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use crate::render::variables_from_environment; use crate::render::render_request;
use crate::{render, response_err}; use crate::response_err;
use base64::Engine; use base64::Engine;
use http::header::{ACCEPT, USER_AGENT}; use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue}; use http::{HeaderMap, HeaderName, HeaderValue};
@@ -16,6 +17,7 @@ use mime_guess::Mime;
use reqwest::redirect::Policy; use reqwest::redirect::Policy;
use reqwest::Method; use reqwest::Method;
use reqwest::{multipart, Url}; use reqwest::{multipart, Url};
use serde_json::Value;
use tauri::{Manager, Runtime, WebviewWindow}; use tauri::{Manager, Runtime, WebviewWindow};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tokio::sync::watch::Receiver; 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>( pub async fn send_http_request<R: Runtime>(
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
request: HttpRequest, request: &HttpRequest,
response: &HttpResponse, response: &HttpResponse,
environment: Option<Environment>, environment: Option<Environment>,
cookie_jar: Option<CookieJar>, cookie_jar: Option<CookieJar>,
cancel_rx: &mut Receiver<bool>, cancel_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> { ) -> Result<HttpResponse, String> {
let environment_ref = environment.as_ref();
let workspace = get_workspace(window, &request.workspace_id) let workspace = get_workspace(window, &request.workspace_id)
.await .await
.expect("Failed to get Workspace"); .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); url_string = ensure_proto(&url_string);
if !url_string.starts_with("http://") && !url_string.starts_with("https://") { 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"); .expect("Failed to create method");
let mut request_builder = client.request(m, url); 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() { if h.name.is_empty() && h.value.is_empty() {
continue; continue;
} }
@@ -147,17 +148,14 @@ pub async fn send_http_request<R: Runtime>(
continue; continue;
} }
let name = render::render(&h.name, &vars); let header_name = match HeaderName::from_bytes(h.name.as_bytes()) {
let value = render::render(&h.value, &vars);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n, Ok(n) => n,
Err(e) => { Err(e) => {
error!("Failed to create header name: {}", e); error!("Failed to create header name: {}", e);
continue; 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, Ok(n) => n,
Err(e) => { Err(e) => {
error!("Failed to create header value: {}", 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); 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 empty_value = &serde_json::to_value("").unwrap();
let a = request.authentication; let a = rendered_request.authentication;
if b == "basic" { if b == "basic" {
let raw_username = a let username = a
.get("username") .get("username")
.unwrap_or(empty_value) .unwrap_or(empty_value)
.as_str() .as_str()
.unwrap_or(""); .unwrap_or_default();
let raw_password = a let password = a
.get("password") .get("password")
.unwrap_or(empty_value) .unwrap_or(empty_value)
.as_str() .as_str()
.unwrap_or(""); .unwrap_or_default();
let username = render::render(raw_username, &vars);
let password = render::render(raw_password, &vars);
let auth = format!("{username}:{password}"); let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth); 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(), HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
); );
} else if b == "bearer" { } else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or(""); let token = a
let token = render::render(raw_token, &vars); .get("token")
.unwrap_or(empty_value)
.as_str()
.unwrap_or_default();
headers.insert( headers.insert(
"Authorization", "Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(), 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(); 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() { if !p.enabled || p.name.is_empty() {
continue; continue;
} }
query_params.push(( query_params.push((p.name, p.value));
render::render(&p.name, &vars),
render::render(&p.value, &vars),
));
} }
request_builder = request_builder.query(&query_params); request_builder = request_builder.query(&query_params);
if let Some(body_type) = &request.body_type { let request_body = rendered_request.body;
let empty_string = &serde_json::to_value("").unwrap(); if let Some(body_type) = &rendered_request.body_type {
let empty_bool = &serde_json::to_value(false).unwrap();
let request_body = request.body;
if request_body.contains_key("text") { if request_body.contains_key("text") {
let raw_text = request_body let body = get_str_h(&request_body, "text");
.get("text") request_builder = request_builder.body(body.to_owned());
.unwrap_or(empty_string)
.as_str()
.unwrap_or("");
let body = render::render(raw_text, &vars);
request_builder = request_builder.body(body);
} else if body_type == "application/x-www-form-urlencoded" } else if body_type == "application/x-www-form-urlencoded"
&& request_body.contains_key("form") && request_body.contains_key("form")
{ {
let mut form_params = Vec::new(); let mut form_params = Vec::new();
let form = request_body.get("form"); let form = request_body.get("form");
if let Some(f) = form { if let Some(f) = form {
for p in f.as_array().unwrap_or(&Vec::new()) { match f.as_array() {
let enabled = p None => {}
.get("enabled") Some(a) => {
.unwrap_or(empty_bool) for p in a {
.as_bool() let enabled = get_bool(p, "enabled");
.unwrap_or(false); let name = get_str(p, "name");
let name = p if !enabled || name.is_empty() {
.get("name") continue;
.unwrap_or(empty_string) }
.as_str() let value = get_str(p, "value");
.unwrap_or_default(); form_params.push((name, value));
if !enabled || name.is_empty() { }
continue;
} }
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); 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") { } else if body_type == "multipart/form-data" && request_body.contains_key("form") {
let mut multipart_form = multipart::Form::new(); let mut multipart_form = multipart::Form::new();
if let Some(form_definition) = request_body.get("form") { if let Some(form_definition) = request_body.get("form") {
for p in form_definition.as_array().unwrap_or(&Vec::new()) { match form_definition.as_array() {
let enabled = p None => {}
.get("enabled") Some(fd) => {
.unwrap_or(empty_bool) for p in fd {
.as_bool() let enabled = get_bool(p, "enabled");
.unwrap_or(false); let name = get_str(p, "name").to_string();
let name_raw = p
.get("name")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
if !enabled || name_raw.is_empty() { if !enabled || name.is_empty() {
continue; 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;
} }
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 headers.remove("Content-Type"); // reqwest will add this automatically
@@ -496,3 +459,24 @@ fn ensure_proto(url_str: &str) -> String {
format!("http://{url_str}") 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::{ use yaak_plugin_runtime::events::{
CallHttpRequestActionRequest, FilterResponse, GetHttpRequestActionsResponse, CallHttpRequestActionRequest, FilterResponse, GetHttpRequestActionsResponse,
GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload, RenderHttpRequestResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload,
SendHttpRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse,
}; };
use yaak_templates::{parse_and_render, Parser, Tokens}; use yaak_templates::{Parser, Tokens};
mod analytics; mod analytics;
mod export_resources; mod export_resources;
@@ -128,7 +128,7 @@ async fn cmd_render_template(
let workspace = get_workspace(&window, &workspace_id) let workspace = get_workspace(&window, &workspace_id)
.await .await
.map_err(|e| e.to_string())?; .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) Ok(rendered)
} }
@@ -195,7 +195,7 @@ async fn cmd_grpc_go(
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let mut metadata = HashMap::new(); 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 // Add rest of metadata
for h in req.clone().metadata { for h in req.clone().metadata {
@@ -207,8 +207,8 @@ async fn cmd_grpc_go(
continue; continue;
} }
let name = render::render(&h.name, &vars); let name = render::render(&h.name, &vars).await;
let value = render::render(&h.value, &vars); let value = render::render(&h.value, &vars).await;
metadata.insert(name, value); metadata.insert(name, value);
} }
@@ -229,15 +229,15 @@ async fn cmd_grpc_go(
.unwrap_or(empty_value) .unwrap_or(empty_value)
.as_str() .as_str()
.unwrap_or(""); .unwrap_or("");
let username = render::render(raw_username, &vars); let username = render::render(raw_username, &vars).await;
let password = render::render(raw_password, &vars); let password = render::render(raw_password, &vars).await;
let auth = format!("{username}:{password}"); let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth); let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
metadata.insert("Authorization".to_string(), format!("Basic {}", encoded)); metadata.insert("Authorization".to_string(), format!("Basic {}", encoded));
} else if b == "bearer" { } else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or(""); 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}")); metadata.insert("Authorization".to_string(), format!("Bearer {token}"));
} }
} }
@@ -355,7 +355,10 @@ async fn cmd_grpc_go(
let w = w.clone(); let w = w.clone();
let base_msg = base_msg.clone(); let base_msg = base_msg.clone();
let method_desc = method_desc.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) let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc)
{ {
Ok(d_msg) => d_msg, Ok(d_msg) => d_msg,
@@ -413,7 +416,7 @@ async fn cmd_grpc_go(
} else { } else {
req.message req.message
}; };
let msg = render::render(&raw_msg, &vars); let msg = render::render(&raw_msg, &vars).await;
upsert_grpc_event( upsert_grpc_event(
&w, &w,
@@ -733,7 +736,7 @@ async fn cmd_send_ephemeral_request(
send_http_request( send_http_request(
&window, &window,
request, &request,
&response, &response,
environment, environment,
cookie_jar, cookie_jar,
@@ -914,6 +917,16 @@ async fn cmd_http_request_actions(
.map_err(|e| e.to_string()) .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] #[tauri::command]
async fn cmd_call_http_request_action( async fn cmd_call_http_request_action(
req: CallHttpRequestActionRequest, req: CallHttpRequestActionRequest,
@@ -1057,7 +1070,7 @@ async fn cmd_send_http_request(
send_http_request( send_http_request(
&window, &window,
request.clone(), &request,
&response, &response,
environment, environment,
cookie_jar, cookie_jar,
@@ -1692,6 +1705,7 @@ pub fn run() {
cmd_grpc_go, cmd_grpc_go,
cmd_grpc_reflect, cmd_grpc_reflect,
cmd_http_request_actions, cmd_http_request_actions,
cmd_template_functions,
cmd_import_data, cmd_import_data,
cmd_list_cookie_jars, cmd_list_cookie_jars,
cmd_list_environments, cmd_list_environments,
@@ -1986,7 +2000,7 @@ async fn handle_plugin_event<R: Runtime>(
Some(id) => get_environment(w, id.as_str()).await.ok(), Some(id) => get_environment(w, id.as_str()).await.ok(),
}; };
let rendered_http_request = 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( Some(InternalEventPayload::RenderHttpRequestResponse(
RenderHttpRequestResponse { RenderHttpRequestResponse {
http_request: rendered_http_request, http_request: rendered_http_request,
@@ -2025,7 +2039,7 @@ async fn handle_plugin_event<R: Runtime>(
let result = send_http_request( let result = send_http_request(
&w, &w,
req.http_request, &req.http_request,
&resp, &resp,
environment, environment,
cookie_jar, cookie_jar,

View File

@@ -4,73 +4,77 @@ use std::collections::HashMap;
use yaak_models::models::{ use yaak_models::models::{
Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace, 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 { pub async fn render_template(template: &str, w: &Workspace, e: Option<&Environment>) -> String {
let vars = &variables_from_environment(w, e); let vars = &variables_from_environment(w, e).await;
render(template, vars) 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 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 { HttpRequest {
url: render(r.url.as_str(), vars), url: render(r.url.as_str(), vars).await,
url_parameters: r url_parameters,
.url_parameters headers,
.iter() body,
.map(|p| HttpUrlParameter { authentication,
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>>(),
..r ..r
} }
} }
pub fn recursively_render_variables<'s>( pub async fn recursively_render_variables<'s>(
m: &HashMap<String, String>, m: &HashMap<String, String>,
render_count: usize, render_count: usize,
) -> HashMap<String, String> { ) -> HashMap<String, String> {
let mut did_render = false; let mut did_render = false;
let mut new_map = m.clone(); let mut new_map = m.clone();
for (k, v) in 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 { if rendered != v {
did_render = true did_render = true
} }
@@ -78,13 +82,13 @@ pub fn recursively_render_variables<'s>(
} }
if did_render && render_count <= 3 { 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 new_map
} }
pub fn variables_from_environment( pub async fn variables_from_environment(
workspace: &Workspace, workspace: &Workspace,
environment: Option<&Environment>, environment: Option<&Environment>,
) -> HashMap<String, String> { ) -> HashMap<String, String> {
@@ -95,17 +99,22 @@ pub fn variables_from_environment(
variables = add_variable_to_map(variables, &e.variables); 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 { pub async fn render(template: &str, vars: &HashMap<String, String>) -> String {
parse_and_render(template, vars, Some(template_callback)) parse_and_render(template, vars, &Box::new(PluginTemplateCallback::default())).await
} }
fn template_callback(name: &str, args: HashMap<String, String>) -> Result<String, String> { #[derive(Default)]
match name { struct PluginTemplateCallback {}
"timestamp" => timestamp(args),
_ => Err(format!("Unknown template function {name}")), 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 serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
@@ -17,8 +18,7 @@ pub struct InternalEvent {
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(tag = "type")] #[serde(rename_all = "snake_case", tag = "type")]
#[serde(rename_all = "snake_case")]
#[ts(export)] #[ts(export)]
pub enum InternalEventPayload { pub enum InternalEventPayload {
BootRequest(BootRequest), BootRequest(BootRequest),
@@ -40,6 +40,10 @@ pub enum InternalEventPayload {
GetHttpRequestActionsResponse(GetHttpRequestActionsResponse), GetHttpRequestActionsResponse(GetHttpRequestActionsResponse),
CallHttpRequestActionRequest(CallHttpRequestActionRequest), CallHttpRequestActionRequest(CallHttpRequestActionRequest),
GetTemplateFunctionsRequest,
GetTemplateFunctionsResponse(GetTemplateFunctionsResponse),
CallTemplateFunctionRequest(CallTemplateFunctionRequest),
CopyTextRequest(CopyTextRequest), CopyTextRequest(CopyTextRequest),
RenderHttpRequestRequest(RenderHttpRequestRequest), 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)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export)] #[ts(export)]

View File

@@ -1,5 +1,5 @@
use crate::error::Result; 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::error::Error::PluginErr;
use crate::nodejs::start_nodejs_plugin_runtime; use crate::nodejs::start_nodejs_plugin_runtime;
@@ -74,6 +74,22 @@ impl PluginManager {
Ok(all_actions) 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<()> { 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 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); let event = plugin.build_event_to_send(&InternalEventPayload::CallHttpRequestActionRequest(req), None);
@@ -81,6 +97,13 @@ impl PluginManager {
Ok(()) 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)> { pub async fn run_import(&self, content: &str) -> Result<(ImportResponse, String)> {
let reply_events = self let reply_events = self
.server .server

View File

@@ -8,3 +8,4 @@ log = "0.4.22"
serde = { version = "1.0.208", features = ["derive"] } serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125" serde_json = "1.0.125"
ts-rs = { version = "9.0.1" } 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 crate::{FnArg, Parser, Token, Tokens, Val};
use log::warn; use log::warn;
use std::collections::HashMap; use std::collections::HashMap;
use std::future::Future;
type TemplateCallback = fn(name: &str, args: HashMap<String, String>) -> Result<String, String>; pub trait TemplateCallback {
fn run(&self, fn_name: &str, args: HashMap<String, String>) -> impl Future<Output = Result<String, String>> + Send;
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 fn render( pub async fn parse_and_render<T>(
tokens: Tokens, template: &str,
vars: &HashMap<String, String>, vars: &HashMap<String, String>,
cb: Option<TemplateCallback>, cb: &Box<T>,
) -> String { ) -> 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(); let mut doc_str: Vec<String> = Vec::new();
for t in tokens.tokens { for t in tokens.tokens {
match t { match t {
Token::Raw { text } => doc_str.push(text), 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 => {} Token::Eof => {}
} }
} }
@@ -32,7 +37,10 @@ pub fn render(
doc_str.join("") 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 { match val {
Val::Str { text } => text.into(), Val::Str { text } => text.into(),
Val::Var { name } => match vars.get(name.as_str()) { 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 } => { Val::Fn { name, args } => {
let empty = "".to_string(); let empty = "".to_string();
let resolved_args = args let mut resolved_args: HashMap<String, String> = HashMap::new();
.iter() for a in args {
.map(|a| match a { let (k, v) = match a {
FnArg { FnArg {
name, name,
value: Val::Str { text }, 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(), vars.get(var_name.as_str()).unwrap_or(&empty).to_string(),
), ),
FnArg { name, value: val } => { 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>>(); resolved_args.insert(k, v);
match cb { }
Some(cb) => match cb(name.as_str(), resolved_args.clone()) { match cb.run(name.as_str(), resolved_args.clone()).await {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
warn!( warn!(
"Failed to run template callback {}({:?}): {}", "Failed to run template callback {}({:?}): {}",
name, resolved_args, e name, resolved_args, e
); );
"".to_string() "".to_string()
} }
},
None => "".into(),
} }
} }
Val::Null => "".into() Val::Null => "".into(),
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::renderer::TemplateCallback;
use crate::*;
use std::collections::HashMap; use std::collections::HashMap;
use crate::*; struct EmptyCB {}
#[test] impl TemplateCallback for EmptyCB {
fn render_empty() { 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 template = "";
let vars = HashMap::new(); let vars = HashMap::new();
let result = ""; 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] #[tokio::test]
fn render_text_only() { async fn render_text_only() {
let empty_cb = Box::new(EmptyCB {});
let template = "Hello World!"; let template = "Hello World!";
let vars = HashMap::new(); let vars = HashMap::new();
let result = "Hello World!"; 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] #[tokio::test]
fn render_simple() { async fn render_simple() {
let empty_cb = Box::new(EmptyCB {});
let template = "${[ foo ]}"; let template = "${[ foo ]}";
let vars = HashMap::from([("foo".to_string(), "bar".to_string())]); let vars = HashMap::from([("foo".to_string(), "bar".to_string())]);
let result = "bar"; 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] #[tokio::test]
fn render_surrounded() { async fn render_surrounded() {
let empty_cb = Box::new(EmptyCB {});
let template = "hello ${[ word ]} world!"; let template = "hello ${[ word ]} world!";
let vars = HashMap::from([("word".to_string(), "cruel".to_string())]); let vars = HashMap::from([("word".to_string(), "cruel".to_string())]);
let result = "hello cruel world!"; 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] #[tokio::test]
fn render_valid_fn() { async fn render_valid_fn() {
let vars = HashMap::new(); let vars = HashMap::new();
let template = r#"${[ say_hello(a="John", b="Kate") ]}"#; let template = r#"${[ say_hello(a="John", b="Kate") ]}"#;
let result = r#"say_hello: 2, Some("John") Some("Kate")"#; let result = r#"say_hello: 2, Some("John") Some("Kate")"#;
fn cb(name: &str, args: HashMap<String, String>) -> Result<String, String> { struct CB {}
Ok(format!( impl TemplateCallback for CB {
"{name}: {}, {:?} {:?}", async fn run(
args.len(), &self,
args.get("a"), fn_name: &str,
args.get("b") 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] #[tokio::test]
fn render_nested_fn() { async fn render_nested_fn() {
let vars = HashMap::new(); let vars = HashMap::new();
let template = r#"${[ upper(foo=secret()) ]}"#; let template = r#"${[ upper(foo=secret()) ]}"#;
let result = r#"ABC"#; let result = r#"ABC"#;
fn cb(name: &str, args: HashMap<String, String>) -> Result<String, String> { struct CB {}
Ok(match name { impl TemplateCallback for CB {
"secret" => "abc".to_string(), async fn run(
"upper" => args["foo"].to_string().to_uppercase(), &self,
_ => "".to_string(), 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!( assert_eq!(
parse_and_render(template, &vars, Some(cb)), parse_and_render(template, &vars, &Box::new(CB {})).await,
result.to_string() result.to_string()
); );
} }
#[test] #[tokio::test]
fn render_fn_err() { async fn render_fn_err() {
let vars = HashMap::new(); let vars = HashMap::new();
let template = r#"${[ error() ]}"#; let template = r#"${[ error() ]}"#;
let result = r#""#; 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!( assert_eq!(
parse_and_render(template, &vars, Some(cb)), parse_and_render(template, &vars, &Box::new(CB {})).await,
result.to_string() result.to_string()
); );
} }

View File

@@ -1,88 +1,19 @@
import type { HttpRequest } from '@yaakapp/api'; import { useQuery } from '@tanstack/react-query';
import type { GetTemplateFunctionsResponse } from '@yaakapp/api';
export interface TemplateFunctionArgBase { import { invokeCmd } from '../lib/tauri';
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[];
}
export function useTemplateFunctions() { export function useTemplateFunctions() {
const fns: TemplateFunction[] = [ const result = useQuery({
{ queryKey: ['template_functions'],
name: 'timestamp', refetchOnWindowFocus: false,
args: [ queryFn: async () => {
{ const responses = (await invokeCmd(
type: 'text', 'cmd_template_functions',
name: 'from', )) as GetTemplateFunctionsResponse[];
label: 'From', return responses;
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',
},
],
}, },
{ });
name: 'response',
args: [ const fns = result.data?.flatMap((r) => r.functions) ?? [];
{
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',
},
],
},
];
return fns; return fns;
} }

View File

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