diff --git a/plugin-runtime-types/src/gen/CallTemplateFunctionArgs.ts b/plugin-runtime-types/src/gen/CallTemplateFunctionArgs.ts new file mode 100644 index 00000000..fd7597f1 --- /dev/null +++ b/plugin-runtime-types/src/gen/CallTemplateFunctionArgs.ts @@ -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 }, }; diff --git a/plugin-runtime-types/src/gen/CallTemplateFunctionPurpose.ts b/plugin-runtime-types/src/gen/CallTemplateFunctionPurpose.ts new file mode 100644 index 00000000..32e47e97 --- /dev/null +++ b/plugin-runtime-types/src/gen/CallTemplateFunctionPurpose.ts @@ -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" }; diff --git a/plugin-runtime-types/src/gen/CallTemplateFunctionRequest.ts b/plugin-runtime-types/src/gen/CallTemplateFunctionRequest.ts new file mode 100644 index 00000000..9c33bd35 --- /dev/null +++ b/plugin-runtime-types/src/gen/CallTemplateFunctionRequest.ts @@ -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, }; diff --git a/plugin-runtime-types/src/gen/GetTemplateFunctionsResponse.ts b/plugin-runtime-types/src/gen/GetTemplateFunctionsResponse.ts new file mode 100644 index 00000000..144c2109 --- /dev/null +++ b/plugin-runtime-types/src/gen/GetTemplateFunctionsResponse.ts @@ -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, pluginRefId: string, }; diff --git a/plugin-runtime-types/src/gen/InternalEventPayload.ts b/plugin-runtime-types/src/gen/InternalEventPayload.ts index f0269bed..6d8f45cd 100644 --- a/plugin-runtime-types/src/gen/InternalEventPayload.ts +++ b/plugin-runtime-types/src/gen/InternalEventPayload.ts @@ -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; diff --git a/plugin-runtime-types/src/gen/TemplateFunction.ts b/plugin-runtime-types/src/gen/TemplateFunction.ts new file mode 100644 index 00000000..474a1cd8 --- /dev/null +++ b/plugin-runtime-types/src/gen/TemplateFunction.ts @@ -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, }; diff --git a/plugin-runtime-types/src/gen/TemplateFunctionArg.ts b/plugin-runtime-types/src/gen/TemplateFunctionArg.ts new file mode 100644 index 00000000..decd4234 --- /dev/null +++ b/plugin-runtime-types/src/gen/TemplateFunctionArg.ts @@ -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; diff --git a/plugin-runtime-types/src/gen/TemplateFunctionBaseArg.ts b/plugin-runtime-types/src/gen/TemplateFunctionBaseArg.ts new file mode 100644 index 00000000..0dc8ba0a --- /dev/null +++ b/plugin-runtime-types/src/gen/TemplateFunctionBaseArg.ts @@ -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, }; diff --git a/plugin-runtime-types/src/gen/TemplateFunctionHttpRequestArg.ts b/plugin-runtime-types/src/gen/TemplateFunctionHttpRequestArg.ts new file mode 100644 index 00000000..8c21b9e0 --- /dev/null +++ b/plugin-runtime-types/src/gen/TemplateFunctionHttpRequestArg.ts @@ -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, }; diff --git a/plugin-runtime-types/src/gen/TemplateFunctionSelectArg.ts b/plugin-runtime-types/src/gen/TemplateFunctionSelectArg.ts new file mode 100644 index 00000000..8e1ceb89 --- /dev/null +++ b/plugin-runtime-types/src/gen/TemplateFunctionSelectArg.ts @@ -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, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, }; diff --git a/plugin-runtime-types/src/gen/TemplateFunctionSelectOption.ts b/plugin-runtime-types/src/gen/TemplateFunctionSelectOption.ts new file mode 100644 index 00000000..27a13f42 --- /dev/null +++ b/plugin-runtime-types/src/gen/TemplateFunctionSelectOption.ts @@ -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, }; diff --git a/plugin-runtime-types/src/gen/TemplateFunctionTextArg.ts b/plugin-runtime-types/src/gen/TemplateFunctionTextArg.ts new file mode 100644 index 00000000..c99edcfe --- /dev/null +++ b/plugin-runtime-types/src/gen/TemplateFunctionTextArg.ts @@ -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, }; diff --git a/plugin-runtime-types/src/index.ts b/plugin-runtime-types/src/index.ts index a1d0e837..3d496ed3 100644 --- a/plugin-runtime-types/src/index.ts +++ b/plugin-runtime-types/src/index.ts @@ -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'; diff --git a/plugin-runtime-types/src/plugins/context.ts b/plugin-runtime-types/src/plugins/Context.ts similarity index 97% rename from plugin-runtime-types/src/plugins/context.ts rename to plugin-runtime-types/src/plugins/Context.ts index 94bffa01..00344104 100644 --- a/plugin-runtime-types/src/plugins/context.ts +++ b/plugin-runtime-types/src/plugins/Context.ts @@ -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; }; diff --git a/plugin-runtime-types/src/plugins/filter.ts b/plugin-runtime-types/src/plugins/FilterPlugin.ts similarity index 60% rename from plugin-runtime-types/src/plugins/filter.ts rename to plugin-runtime-types/src/plugins/FilterPlugin.ts index 80a223c2..32cd2f26 100644 --- a/plugin-runtime-types/src/plugins/filter.ts +++ b/plugin-runtime-types/src/plugins/FilterPlugin.ts @@ -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; + canFilter(ctx: Context, args: { mimeType: string }): Promise; onFilter( - ctx: YaakContext, + ctx: Context, args: { payload: string; mimeType: string }, ): Promise; }; diff --git a/plugin-runtime-types/src/plugins/httpRequestAction.ts b/plugin-runtime-types/src/plugins/HttpRequestActionPlugin.ts similarity index 61% rename from plugin-runtime-types/src/plugins/httpRequestAction.ts rename to plugin-runtime-types/src/plugins/HttpRequestActionPlugin.ts index 759b20cb..6c4eaf2d 100644 --- a/plugin-runtime-types/src/plugins/httpRequestAction.ts +++ b/plugin-runtime-types/src/plugins/HttpRequestActionPlugin.ts @@ -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; + onSelect(ctx: Context, args: CallHttpRequestActionArgs): Promise | void; }; diff --git a/plugin-runtime-types/src/plugins/import.ts b/plugin-runtime-types/src/plugins/ImporterPlugin.ts similarity index 83% rename from plugin-runtime-types/src/plugins/import.ts rename to plugin-runtime-types/src/plugins/ImporterPlugin.ts index c5e3c70d..296edbed 100644 --- a/plugin-runtime-types/src/plugins/import.ts +++ b/plugin-runtime-types/src/plugins/ImporterPlugin.ts @@ -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[]; @@ -15,5 +15,5 @@ export type ImportPluginResponse = null | { export type ImporterPlugin = { name: string; description?: string; - onImport(ctx: YaakContext, args: { text: string }): Promise; + onImport(ctx: Context, args: { text: string }): Promise; }; diff --git a/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts b/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts new file mode 100644 index 00000000..78ffec7c --- /dev/null +++ b/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts @@ -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; +}; diff --git a/plugin-runtime-types/src/plugins/ThemePlugin.ts b/plugin-runtime-types/src/plugins/ThemePlugin.ts new file mode 100644 index 00000000..ab832e43 --- /dev/null +++ b/plugin-runtime-types/src/plugins/ThemePlugin.ts @@ -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; +}; diff --git a/plugin-runtime-types/src/plugins/index.ts b/plugin-runtime-types/src/plugins/index.ts index c3587414..6bfe7f35 100644 --- a/plugin-runtime-types/src/plugins/index.ts +++ b/plugin-runtime-types/src/plugins/index.ts @@ -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[]; }; diff --git a/plugin-runtime-types/src/plugins/theme.ts b/plugin-runtime-types/src/plugins/theme.ts deleted file mode 100644 index 3b1efb7f..00000000 --- a/plugin-runtime-types/src/plugins/theme.ts +++ /dev/null @@ -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; -}; diff --git a/plugin-runtime/src/index.worker.ts b/plugin-runtime/src/index.worker.ts index dbbc350d..75c3f1e4 100644 --- a/plugin-runtime/src/index.worker.ts +++ b/plugin-runtime/src/index.worker.ts @@ -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(async (resolve, reject) => { return promise as unknown as Promise; } - const ctx: YaakContext = { + const ctx: Context = { clipboard: { async copyText(text) { await sendAndWaitForReply({ type: 'copy_text_request', text }); @@ -181,8 +183,8 @@ new Promise(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(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(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 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index aa1f301a..8952f95c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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", ] diff --git a/src-tauri/src/http_request.rs b/src-tauri/src/http_request.rs index 8210409f..1984f574 100644 --- a/src-tauri/src/http_request.rs +++ b/src-tauri/src/http_request.rs @@ -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( window: &WebviewWindow, - request: HttpRequest, + request: &HttpRequest, response: &HttpResponse, environment: Option, cookie_jar: Option, cancel_rx: &mut Receiver, ) -> Result { - 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( } }; - 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( // ); // } - 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( 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( 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( 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( } 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( } 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, key: &str) -> &'a str { + match v.get(key) { + None => "", + Some(v) => v.as_str().unwrap_or_default(), + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 91360a1d..554ca776 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, 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( 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( let result = send_http_request( &w, - req.http_request, + &req.http_request, &resp, environment, cookie_jar, diff --git a/src-tauri/src/render.rs b/src-tauri/src/render.rs index b2713f03..13086c2b 100644 --- a/src-tauri/src/render.rs +++ b/src-tauri/src/render.rs @@ -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::>(), - 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::>(), - 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::>(), - 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::>(), + 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, render_count: usize, ) -> HashMap { 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 { @@ -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 { - parse_and_render(template, vars, Some(template_callback)) +pub async fn render(template: &str, vars: &HashMap) -> String { + parse_and_render(template, vars, &Box::new(PluginTemplateCallback::default())).await } -fn template_callback(name: &str, args: HashMap) -> Result { - 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) -> Result { + match fn_name { + "timestamp" => timestamp(args), + _ => Err(format!("Unknown template function {fn_name}")), + } } } diff --git a/src-tauri/yaak_plugin_runtime/src/events.rs b/src-tauri/yaak_plugin_runtime/src/events.rs index a5740b22..66933771 100644 --- a/src-tauri/yaak_plugin_runtime/src/events.rs +++ b/src-tauri/yaak_plugin_runtime/src/events.rs @@ -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, + 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, +} + +#[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, + #[ts(optional = nullable)] + pub label: Option, + #[ts(optional = nullable)] + pub default_value: Option, +} + +#[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, +} + +#[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, +} + +#[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, +} + +#[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)] diff --git a/src-tauri/yaak_plugin_runtime/src/manager.rs b/src-tauri/yaak_plugin_runtime/src/manager.rs index 19d05faf..cfc04436 100644 --- a/src-tauri/yaak_plugin_runtime/src/manager.rs +++ b/src-tauri/yaak_plugin_runtime/src/manager.rs @@ -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> { + 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 diff --git a/src-tauri/yaak_templates/Cargo.toml b/src-tauri/yaak_templates/Cargo.toml index d01dda6c..82f7d291 100644 --- a/src-tauri/yaak_templates/Cargo.toml +++ b/src-tauri/yaak_templates/Cargo.toml @@ -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"] } diff --git a/src-tauri/yaak_templates/src/renderer.rs b/src-tauri/yaak_templates/src/renderer.rs index ce7b079b..9ada68ff 100644 --- a/src-tauri/yaak_templates/src/renderer.rs +++ b/src-tauri/yaak_templates/src/renderer.rs @@ -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) -> Result; - -pub fn parse_and_render( - template: &str, - vars: &HashMap, - cb: Option, -) -> 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) -> impl Future> + Send; } -pub fn render( - tokens: Tokens, +pub async fn parse_and_render( + template: &str, vars: &HashMap, - cb: Option, -) -> String { + cb: &Box, +) -> String +where + T: TemplateCallback, +{ + let mut p = Parser::new(template); + let tokens = p.parse(); + render(tokens, vars, cb).await +} + +pub async fn render(tokens: Tokens, vars: &HashMap, cb: &Box) -> String +where + T: TemplateCallback, +{ let mut doc_str: Vec = 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, cb: Option) -> String { +async fn render_tag(val: Val, vars: &HashMap, cb: &Box) -> 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, cb: Option { let empty = "".to_string(); - let resolved_args = args - .iter() - .map(|a| match a { + let mut resolved_args: HashMap = 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, cb: Option { - (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::>(); - 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) -> Result{ + 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) -> Result { - 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, + ) -> Result { + 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) -> Result { - 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, + ) -> Result { + 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) -> Result { - Err("Failed to do it!".to_string()) + + struct CB {} + impl TemplateCallback for CB { + async fn run( + &self, + _fn_name: &str, + _args: HashMap, + ) -> Result { + 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() ); } diff --git a/src-web/hooks/useTemplateFunctions.ts b/src-web/hooks/useTemplateFunctions.ts index c1602e23..519cfc53 100644 --- a/src-web/hooks/useTemplateFunctions.ts +++ b/src-web/hooks/useTemplateFunctions.ts @@ -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; } diff --git a/src-web/lib/tauri.ts b/src-web/lib/tauri.ts index cf5b3612..20731935 100644 --- a/src-web/lib/tauri.ts +++ b/src-web/lib/tauri.ts @@ -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'