diff --git a/package-lock.json b/package-lock.json index a46edb70..d8a226b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@tauri-apps/plugin-log": "^2.0.0-rc.0", "@tauri-apps/plugin-os": "^2.0.0-rc.0", "@tauri-apps/plugin-shell": "^2.0.0-rc.0", - "@yaakapp/api": "^0.1.6", + "@yaakapp/api": "^0.1.7", "buffer": "^6.0.3", "classnames": "^2.3.2", "cm6-graphql": "^0.0.9", @@ -2990,9 +2990,9 @@ "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" }, "node_modules/@yaakapp/api": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.6.tgz", - "integrity": "sha512-5lYXKcOVmLzVUrkfU4JOCbz+CBV5Dm/cALoZvfbelvZWOVu3sTrBxS9cbNVQQq2B6WwLInSevk7pMq58GqIj5Q==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.7.tgz", + "integrity": "sha512-6dheXUuhX3o1KwZ7ngtB/OcQ4qCiXxg3LN2t1jA28nnZBlGCek+h9yndiF/PGWl5yReMcXy8rn4pq4W9zAGHLg==", "dependencies": { "@types/node": "^22.0.0" } diff --git a/package.json b/package.json index 946929c9..4f78e09a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@tauri-apps/plugin-log": "^2.0.0-rc.0", "@tauri-apps/plugin-os": "^2.0.0-rc.0", "@tauri-apps/plugin-shell": "^2.0.0-rc.0", - "@yaakapp/api": "^0.1.6", + "@yaakapp/api": "^0.1.7", "buffer": "^6.0.3", "classnames": "^2.3.2", "cm6-graphql": "^0.0.9", diff --git a/plugin-runtime-types/package-lock.json b/plugin-runtime-types/package-lock.json index 03a3cfc4..0c060be2 100644 --- a/plugin-runtime-types/package-lock.json +++ b/plugin-runtime-types/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yaakapp/api", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@yaakapp/api", - "version": "0.1.6", + "version": "0.1.7", "dependencies": { "@types/node": "^22.0.0" }, diff --git a/plugin-runtime-types/package.json b/plugin-runtime-types/package.json index 0e1a641b..97222c40 100644 --- a/plugin-runtime-types/package.json +++ b/plugin-runtime-types/package.json @@ -1,6 +1,6 @@ { "name": "@yaakapp/api", - "version": "0.1.6", + "version": "0.1.7", "main": "lib/index.js", "typings": "./lib/index.d.ts", "files": [ diff --git a/plugin-runtime-types/src/gen/CallTemplateFunctionRequest.ts b/plugin-runtime-types/src/gen/CallTemplateFunctionRequest.ts index 9c33bd35..0e1e640e 100644 --- a/plugin-runtime-types/src/gen/CallTemplateFunctionRequest.ts +++ b/plugin-runtime-types/src/gen/CallTemplateFunctionRequest.ts @@ -1,4 +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, }; +export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, }; diff --git a/plugin-runtime-types/src/gen/CallTemplateFunctionResponse.ts b/plugin-runtime-types/src/gen/CallTemplateFunctionResponse.ts new file mode 100644 index 00000000..f89ef42f --- /dev/null +++ b/plugin-runtime-types/src/gen/CallTemplateFunctionResponse.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 CallTemplateFunctionResponse = { value: string | null, }; diff --git a/plugin-runtime-types/src/gen/GetHttpRequestActionsRequest.ts b/plugin-runtime-types/src/gen/GetHttpRequestActionsRequest.ts new file mode 100644 index 00000000..570d7b77 --- /dev/null +++ b/plugin-runtime-types/src/gen/GetHttpRequestActionsRequest.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 GetHttpRequestActionsRequest = Record; diff --git a/plugin-runtime-types/src/gen/InternalEventPayload.ts b/plugin-runtime-types/src/gen/InternalEventPayload.ts index 6d8f45cd..e619fb24 100644 --- a/plugin-runtime-types/src/gen/InternalEventPayload.ts +++ b/plugin-runtime-types/src/gen/InternalEventPayload.ts @@ -3,12 +3,14 @@ import type { BootRequest } from "./BootRequest"; import type { BootResponse } from "./BootResponse"; import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest"; import type { CallTemplateFunctionRequest } from "./CallTemplateFunctionRequest"; +import type { CallTemplateFunctionResponse } from "./CallTemplateFunctionResponse"; import type { CopyTextRequest } from "./CopyTextRequest"; import type { EmptyResponse } from "./EmptyResponse"; import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest"; import type { ExportHttpRequestResponse } from "./ExportHttpRequestResponse"; import type { FilterRequest } from "./FilterRequest"; import type { FilterResponse } from "./FilterResponse"; +import type { GetHttpRequestActionsRequest } from "./GetHttpRequestActionsRequest"; import type { GetHttpRequestActionsResponse } from "./GetHttpRequestActionsResponse"; import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest"; import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse"; @@ -21,4 +23,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": "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; +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" } & GetHttpRequestActionsRequest | { "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": "call_template_function_response" } & CallTemplateFunctionResponse | { "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/index.ts b/plugin-runtime-types/src/index.ts index 3d496ed3..aaf02a52 100644 --- a/plugin-runtime-types/src/index.ts +++ b/plugin-runtime-types/src/index.ts @@ -8,6 +8,7 @@ export * from './gen/CallHttpRequestActionArgs'; export * from './gen/CallTemplateFunctionPurpose'; export * from './gen/CallHttpRequestActionRequest'; export * from './gen/CallTemplateFunctionRequest'; +export * from './gen/CallTemplateFunctionResponse'; export * from './gen/CallTemplateFunctionArgs'; export * from './gen/Cookie'; export * from './gen/CookieDomain'; diff --git a/plugin-runtime/src/index.worker.ts b/plugin-runtime/src/index.worker.ts index 75c3f1e4..2a8ee9c8 100644 --- a/plugin-runtime/src/index.worker.ts +++ b/plugin-runtime/src/index.worker.ts @@ -56,7 +56,7 @@ new Promise(async (resolve, reject) => { return sendPayload({ type: 'empty_response' }, replyId); } - function sendPayload(payload: InternalEventPayload, replyId: string | null = null): string { + function sendPayload(payload: InternalEventPayload, replyId: string | null): string { const event = buildEventToSend(payload, replyId); sendEvent(event); return event.id; @@ -233,9 +233,10 @@ new Promise(async (resolve, reject) => { 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); + if (typeof action?.onRender === 'function') { + const result = await action.onRender(ctx, payload.args); + console.log('GOT VALUE', result); + sendPayload({ type: 'call_template_function_response', value: result ?? null }, replyId); return; } } diff --git a/src-tauri/src/http_request.rs b/src-tauri/src/http_request.rs index 1984f574..52e6d6ac 100644 --- a/src-tauri/src/http_request.rs +++ b/src-tauri/src/http_request.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use crate::render::render_request; +use crate::render::render_http_request; use crate::response_err; use base64::Engine; use http::header::{ACCEPT, USER_AGENT}; @@ -37,7 +37,13 @@ pub async fn send_http_request( let workspace = get_workspace(window, &request.workspace_id) .await .expect("Failed to get Workspace"); - let rendered_request = render_request(&request, &workspace, environment.as_ref()).await; + let rendered_request = render_http_request( + window.app_handle(), + &request, + &workspace, + environment.as_ref(), + ) + .await; let mut url_string = rendered_request.url; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 554ca776..7b53d6e0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -35,7 +35,8 @@ use crate::export_resources::{get_workspace_export_resources, WorkspaceExportRes use crate::grpc::metadata_to_map; use crate::http_request::send_http_request; use crate::notifications::YaakNotifier; -use crate::render::{render_request, render_template, variables_from_environment}; +use crate::render::{render_grpc_request, render_http_request, render_template}; +use crate::template_callback::PluginTemplateCallback; use crate::updates::{UpdateMode, YaakUpdater}; use crate::window_menu::app_menu; use yaak_models::models::{ @@ -70,7 +71,7 @@ mod notifications; mod render; #[cfg(target_os = "macos")] mod tauri_plugin_mac_window; -mod template_fns; +mod template_callback; mod updates; mod window_menu; @@ -128,7 +129,13 @@ 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()).await; + let rendered = render_template( + window.app_handle(), + template, + &workspace, + environment.as_ref(), + ) + .await; Ok(rendered) } @@ -180,9 +187,6 @@ async fn cmd_grpc_go( window: WebviewWindow, grpc_handle: State<'_, Mutex>, ) -> Result { - let req = get_grpc_request(&window, request_id) - .await - .map_err(|e| e.to_string())?; let environment = match environment_id { Some(id) => Some( get_environment(&window, id) @@ -191,13 +195,17 @@ async fn cmd_grpc_go( ), None => None, }; + let req = get_grpc_request(&window, request_id) + .await + .map_err(|e| e.to_string())?; let workspace = get_workspace(&window, &req.workspace_id) .await .map_err(|e| e.to_string())?; + let req = + render_grpc_request(window.app_handle(), &req, &workspace, environment.as_ref()).await; let mut metadata = HashMap::new(); - let vars = variables_from_environment(&workspace, environment.as_ref()).await; - // Add rest of metadata + // Add the rest of metadata for h in req.clone().metadata { if h.name.is_empty() && h.value.is_empty() { continue; @@ -207,10 +215,7 @@ async fn cmd_grpc_go( continue; } - let name = render::render(&h.name, &vars).await; - let value = render::render(&h.value, &vars).await; - - metadata.insert(name, value); + metadata.insert(h.name, h.value); } if let Some(b) = &req.authentication_type { @@ -219,25 +224,22 @@ async fn cmd_grpc_go( let a = req.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 + let password = a .get("password") .unwrap_or(empty_value) .as_str() .unwrap_or(""); - 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).await; + let token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or(""); metadata.insert("Authorization".to_string(), format!("Bearer {token}")); } } @@ -331,7 +333,6 @@ async fn cmd_grpc_go( let w = window.clone(); let base_msg = base_msg.clone(); let method_desc = method_desc.clone(); - let vars = vars.clone(); move |ev: tauri::Event| { if *cancelled_rx.borrow() { @@ -351,14 +352,10 @@ async fn cmd_grpc_go( }; match serde_json::from_str::(ev.payload()) { - Ok(IncomingMsg::Message(raw_msg)) => { + Ok(IncomingMsg::Message(msg)) => { let w = w.clone(); let base_msg = base_msg.clone(); let method_desc = method_desc.clone(); - 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, @@ -410,13 +407,11 @@ async fn cmd_grpc_go( let w = window.clone(); let base_event = base_msg.clone(); let req = req.clone(); - let vars = vars.clone(); - let raw_msg = if req.message.is_empty() { + let msg = if req.message.is_empty() { "{}".to_string() } else { req.message }; - let msg = render::render(&raw_msg, &vars).await; upsert_grpc_event( &w, @@ -772,7 +767,7 @@ async fn cmd_filter_response( // TODO: Have plugins register their own content type (regex?) plugin_manager - .run_filter(filter, &body, &content_type) + .filter_data(filter, &body, &content_type) .await .map_err(|e| e.to_string()) } @@ -787,7 +782,7 @@ async fn cmd_import_data( read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path)); let file_contents = file.as_str(); let (import_result, plugin_name) = plugin_manager - .run_import(file_contents) + .import_data(file_contents) .await .map_err(|e| e.to_string())?; @@ -912,7 +907,7 @@ async fn cmd_http_request_actions( plugin_manager: State<'_, PluginManager>, ) -> Result, String> { plugin_manager - .run_http_request_actions() + .get_http_request_actions() .await .map_err(|e| e.to_string()) } @@ -922,7 +917,7 @@ async fn cmd_template_functions( plugin_manager: State<'_, PluginManager>, ) -> Result, String> { plugin_manager - .run_template_functions() + .get_template_functions() .await .map_err(|e| e.to_string()) } @@ -947,7 +942,7 @@ async fn cmd_curl_to_request( ) -> Result { let (import_result, plugin_name) = { plugin_manager - .run_import(command) + .import_data(command) .await .map_err(|e| e.to_string())? }; @@ -1661,6 +1656,10 @@ pub fn run() { let grpc_handle = GrpcHandle::new(&app.app_handle()); app.manage(Mutex::new(grpc_handle)); + // Plugin template callback + let plugin_cb = PluginTemplateCallback::new(app.app_handle().clone()); + app.manage(plugin_cb); + let app_handle = app.app_handle().clone(); monitor_plugin_events(&app_handle); @@ -1999,8 +1998,13 @@ async fn handle_plugin_event( None => None, Some(id) => get_environment(w, id.as_str()).await.ok(), }; - let rendered_http_request = - render_request(&req.http_request, &workspace, environment.as_ref()).await; + let rendered_http_request = render_http_request( + app_handle, + &req.http_request, + &workspace, + environment.as_ref(), + ) + .await; Some(InternalEventPayload::RenderHttpRequestResponse( RenderHttpRequestResponse { http_request: rendered_http_request, diff --git a/src-tauri/src/render.rs b/src-tauri/src/render.rs index 13086c2b..e2456fc1 100644 --- a/src-tauri/src/render.rs +++ b/src-tauri/src/render.rs @@ -1,80 +1,126 @@ -use crate::template_fns::timestamp; +use crate::template_callback::PluginTemplateCallback; use serde_json::Value; use std::collections::HashMap; -use yaak_models::models::{ - Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace, -}; +use tauri::{AppHandle, Manager, Runtime}; +use yaak_models::models::{Environment, EnvironmentVariable, GrpcMetadataEntry, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace}; use yaak_templates::{parse_and_render, TemplateCallback}; -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 async fn render_template( + app_handle: &AppHandle, + template: &str, + w: &Workspace, + e: Option<&Environment>, +) -> String { + let cb = &*app_handle.state::(); + let vars = &variables_from_environment(w, e, cb).await; + render(template, vars, cb).await } -pub async fn render_request( +pub async fn render_grpc_request( + app_handle: &AppHandle, + r: &GrpcRequest, + w: &Workspace, + e: Option<&Environment>, +) -> GrpcRequest { + let cb = &*app_handle.state::(); + let vars = &variables_from_environment(w, e, cb).await; + + let mut metadata = Vec::new(); + for p in r.metadata.clone() { + metadata.push(GrpcMetadataEntry { + enabled: p.enabled, + name: render(p.name.as_str(), vars, cb).await, + value: render(p.value.as_str(), vars, cb).await, + }) + } + + let mut authentication = HashMap::new(); + for (k, v) in r.authentication.clone() { + let v = if v.is_string() { + render(v.as_str().unwrap(), vars, cb).await + } else { + v.to_string() + }; + authentication.insert(render(k.as_str(), vars, cb).await, Value::from(v)); + } + + let url = render(r.url.as_str(), vars, cb).await; + + GrpcRequest{ + url, + metadata, + authentication, + ..r.to_owned() + } +} + +pub async fn render_http_request( + app_handle: &AppHandle, r: &HttpRequest, w: &Workspace, e: Option<&Environment>, ) -> HttpRequest { - let r = r.clone(); - let vars = &variables_from_environment(w, e).await; + let cb = &*app_handle.state::(); + let vars = &variables_from_environment(w, e, cb).await; let mut url_parameters = Vec::new(); - for p in r.url_parameters { + for p in r.url_parameters.clone() { url_parameters.push(HttpUrlParameter { enabled: p.enabled, - name: render(p.name.as_str(), vars).await, - value: render(p.value.as_str(), vars).await, + name: render(p.name.as_str(), vars, cb).await, + value: render(p.value.as_str(), vars, cb).await, }) } let mut headers = Vec::new(); - for p in r.headers { + for p in r.headers.clone() { headers.push(HttpRequestHeader { enabled: p.enabled, - name: render(p.name.as_str(), vars).await, - value: render(p.value.as_str(), vars).await, + name: render(p.name.as_str(), vars, cb).await, + value: render(p.value.as_str(), vars, cb).await, }) } let mut body = HashMap::new(); - for (k, v) in r.body { + for (k, v) in r.body.clone() { let v = if v.is_string() { - render(v.as_str().unwrap(), vars).await + render(v.as_str().unwrap(), vars, cb).await } else { v.to_string() }; - body.insert(render(k.as_str(), vars).await, Value::from(v)); + body.insert(render(k.as_str(), vars, cb).await, Value::from(v)); } let mut authentication = HashMap::new(); - for (k, v) in r.authentication { + for (k, v) in r.authentication.clone() { let v = if v.is_string() { - render(v.as_str().unwrap(), vars).await + render(v.as_str().unwrap(), vars, cb).await } else { v.to_string() }; - authentication.insert(render(k.as_str(), vars).await, Value::from(v)); + authentication.insert(render(k.as_str(), vars, cb).await, Value::from(v)); } + let url = render(r.url.clone().as_str(), vars, cb).await; HttpRequest { - url: render(r.url.as_str(), vars).await, + url, url_parameters, headers, body, authentication, - ..r + ..r.to_owned() } } -pub async fn recursively_render_variables<'s>( +pub async fn recursively_render_variables<'s, T: TemplateCallback>( m: &HashMap, render_count: usize, + cb: &T, ) -> HashMap { let mut did_render = false; let mut new_map = m.clone(); for (k, v) in m.clone() { - let rendered = Box::pin(render(v.as_str(), m)).await; + let rendered = Box::pin(render(v.as_str(), m, cb)).await; if rendered != v { did_render = true } @@ -82,15 +128,16 @@ pub async fn recursively_render_variables<'s>( } if did_render && render_count <= 3 { - new_map = Box::pin(recursively_render_variables(&new_map, render_count + 1)).await; + new_map = Box::pin(recursively_render_variables(&new_map, render_count + 1, cb)).await; } new_map } -pub async fn variables_from_environment( +pub async fn variables_from_environment( workspace: &Workspace, environment: Option<&Environment>, + cb: &T, ) -> HashMap { let mut variables = HashMap::new(); variables = add_variable_to_map(variables, &workspace.variables); @@ -99,23 +146,15 @@ pub async fn variables_from_environment( variables = add_variable_to_map(variables, &e.variables); } - recursively_render_variables(&variables, 0).await + recursively_render_variables(&variables, 0, cb).await } -pub async fn render(template: &str, vars: &HashMap) -> String { - parse_and_render(template, vars, &Box::new(PluginTemplateCallback::default())).await -} - -#[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}")), - } - } +pub async fn render( + template: &str, + vars: &HashMap, + cb: &T, +) -> String { + parse_and_render(template, vars, cb).await } fn add_variable_to_map( diff --git a/src-tauri/src/template_callback.rs b/src-tauri/src/template_callback.rs new file mode 100644 index 00000000..f9b174f2 --- /dev/null +++ b/src-tauri/src/template_callback.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; +use tauri::{AppHandle, Manager}; +use yaak_plugin_runtime::manager::PluginManager; +use yaak_templates::TemplateCallback; + +pub struct PluginTemplateCallback { + app_handle: AppHandle, +} + +impl PluginTemplateCallback { + pub fn new(app_handle: AppHandle) -> PluginTemplateCallback { + PluginTemplateCallback { app_handle } + } +} + +impl TemplateCallback for PluginTemplateCallback { + async fn run(&self, fn_name: &str, args: HashMap) -> Result { + let plugin_manager = self.app_handle.state::(); + let resp = plugin_manager + .call_template_function(fn_name, args) + .await + .map_err(|e| e.to_string())?; + Ok(resp.unwrap_or_default()) + } +} diff --git a/src-tauri/src/template_fns.rs b/src-tauri/src/template_fns.rs deleted file mode 100644 index 94b7434d..00000000 --- a/src-tauri/src/template_fns.rs +++ /dev/null @@ -1,70 +0,0 @@ -use chrono::{DateTime, Utc}; -use std::collections::HashMap; - -pub fn timestamp(args: HashMap) -> Result { - let from = args.get("from").map(|v| v.as_str()).unwrap_or("now"); - let format = args.get("format").map(|v| v.as_str()).unwrap_or("rfc3339"); - - let dt = match from { - "now" => { - let now = Utc::now(); - now - } - _ => { - let json_from = serde_json::to_string(from).unwrap_or_default(); - let now: DateTime = match serde_json::from_str(json_from.as_str()) { - Ok(r) => r, - Err(e) => return Err(e.to_string()), - }; - now - } - }; - - let result = match format { - "rfc3339" => dt.to_rfc3339(), - "unix" => dt.timestamp().to_string(), - "unix_millis" => dt.timestamp_millis().to_string(), - _ => "".to_string(), - }; - - Ok(result) -} - -// Test it -#[cfg(test)] -mod tests { - use crate::template_fns::timestamp; - use std::collections::HashMap; - - #[test] - fn timestamp_empty() { - let args = HashMap::new(); - assert_ne!(timestamp(args), Ok("".to_string())); - } - - #[test] - fn timestamp_from() { - let mut args = HashMap::new(); - args.insert("from".to_string(), "2024-07-31T14:16:41.983Z".to_string()); - assert_eq!( - timestamp(args), - Ok("2024-07-31T14:16:41.983+00:00".to_string()) - ); - } - - #[test] - fn timestamp_format_unix() { - let mut args = HashMap::new(); - args.insert("from".to_string(), "2024-07-31T14:16:41.983Z".to_string()); - args.insert("format".to_string(), "unix".to_string()); - assert_eq!(timestamp(args), Ok("1722435401".to_string())); - } - - #[test] - fn timestamp_format_unix_millis() { - let mut args = HashMap::new(); - args.insert("from".to_string(), "2024-07-31T14:16:41.983Z".to_string()); - args.insert("format".to_string(), "unix_millis".to_string()); - assert_eq!(timestamp(args), Ok("1722435401983".to_string())); - } -} diff --git a/src-tauri/yaak_plugin_runtime/src/events.rs b/src-tauri/yaak_plugin_runtime/src/events.rs index 66933771..0f834346 100644 --- a/src-tauri/yaak_plugin_runtime/src/events.rs +++ b/src-tauri/yaak_plugin_runtime/src/events.rs @@ -1,5 +1,5 @@ -use std::collections::HashMap; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use ts_rs::TS; use yaak_models::models::{ @@ -36,13 +36,14 @@ pub enum InternalEventPayload { SendHttpRequestRequest(SendHttpRequestRequest), SendHttpRequestResponse(SendHttpRequestResponse), - GetHttpRequestActionsRequest, + GetHttpRequestActionsRequest(GetHttpRequestActionsRequest), GetHttpRequestActionsResponse(GetHttpRequestActionsResponse), CallHttpRequestActionRequest(CallHttpRequestActionRequest), GetTemplateFunctionsRequest, GetTemplateFunctionsResponse(GetTemplateFunctionsResponse), CallTemplateFunctionRequest(CallTemplateFunctionRequest), + CallTemplateFunctionResponse(CallTemplateFunctionResponse), CopyTextRequest(CopyTextRequest), @@ -262,10 +263,16 @@ pub struct TemplateFunctionSelectOption { #[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 CallTemplateFunctionResponse { + pub value: Option, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] #[ts(export)] @@ -282,12 +289,17 @@ pub enum CallTemplateFunctionPurpose { Preview, } -impl Default for CallTemplateFunctionPurpose{ +impl Default for CallTemplateFunctionPurpose { fn default() -> Self { CallTemplateFunctionPurpose::Preview } } +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default)] +#[ts(export)] +pub struct GetHttpRequestActionsRequest {} + #[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 cfc04436..5245bbcf 100644 --- a/src-tauri/yaak_plugin_runtime/src/manager.rs +++ b/src-tauri/yaak_plugin_runtime/src/manager.rs @@ -1,5 +1,11 @@ use crate::error::Result; -use crate::events::{CallHttpRequestActionRequest, CallTemplateFunctionRequest, FilterRequest, FilterResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload}; +use crate::events::{ + CallHttpRequestActionRequest, CallTemplateFunctionArgs, CallTemplateFunctionPurpose, + CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse, + GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, + ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, +}; +use std::collections::HashMap; use crate::error::Error::PluginErr; use crate::nodejs::start_nodejs_plugin_runtime; @@ -57,11 +63,13 @@ impl PluginManager { .send(&payload, source_event.plugin_ref_id.as_str(), reply_id) .await } - - pub async fn run_http_request_actions(&self) -> Result> { + + pub async fn get_http_request_actions(&self) -> Result> { let reply_events = self .server - .send_and_wait(&InternalEventPayload::GetHttpRequestActionsRequest) + .send_and_wait(&InternalEventPayload::GetHttpRequestActionsRequest( + GetHttpRequestActionsRequest {}, + )) .await?; let mut all_actions = Vec::new(); @@ -74,7 +82,7 @@ impl PluginManager { Ok(all_actions) } - pub async fn run_template_functions(&self) -> Result> { + pub async fn get_template_functions(&self) -> Result> { let reply_events = self .server .send_and_wait(&InternalEventPayload::GetTemplateFunctionsRequest) @@ -91,20 +99,47 @@ impl PluginManager { } 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); + 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, + ); plugin.send(&event).await?; 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 call_template_function( + &self, + fn_name: &str, + args: HashMap, + ) -> Result> { + let req = CallTemplateFunctionRequest { + name: fn_name.to_string(), + args: CallTemplateFunctionArgs { + purpose: CallTemplateFunctionPurpose::Preview, + values: args, + }, + }; + + let events = self + .server + .send_and_wait(&InternalEventPayload::CallTemplateFunctionRequest(req)) + .await?; + + let value = events.into_iter().find_map(|e| match e.payload { + InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse { + value, + }) => value, + _ => None, + }); + + Ok(value) } - pub async fn run_import(&self, content: &str) -> Result<(ImportResponse, String)> { + pub async fn import_data(&self, content: &str) -> Result<(ImportResponse, String)> { let reply_events = self .server .send_and_wait(&InternalEventPayload::ImportRequest(ImportRequest { @@ -113,19 +148,22 @@ impl PluginManager { .await?; // TODO: Don't just return the first valid response - for event in reply_events { - if let InternalEventPayload::ImportResponse(resp) = event.payload { - let ref_id = event.plugin_ref_id.as_str(); - let plugin = self.server.plugin_by_ref_id(ref_id).await?; + let result = reply_events.into_iter().find_map(|e| match e.payload { + InternalEventPayload::ImportResponse(resp) => Some((resp, e.plugin_ref_id)), + _ => None, + }); + + match result { + None => Err(PluginErr("No import responses found".to_string())), + Some((resp, ref_id)) => { + let plugin = self.server.plugin_by_ref_id(ref_id.as_str()).await?; let plugin_name = plugin.name().await; - return Ok((resp, plugin_name)); + Ok((resp, plugin_name)) } } - - Err(PluginErr("No import responses found".to_string())) } - pub async fn run_filter( + pub async fn filter_data( &self, filter: &str, content: &str, diff --git a/src-tauri/yaak_templates/src/renderer.rs b/src-tauri/yaak_templates/src/renderer.rs index 9ada68ff..fa9f7f78 100644 --- a/src-tauri/yaak_templates/src/renderer.rs +++ b/src-tauri/yaak_templates/src/renderer.rs @@ -4,26 +4,28 @@ use std::collections::HashMap; use std::future::Future; pub trait TemplateCallback { - fn run(&self, fn_name: &str, args: HashMap) -> impl Future> + Send; + fn run( + &self, + fn_name: &str, + args: HashMap, + ) -> impl Future> + Send; } -pub async fn parse_and_render( +pub async fn parse_and_render( template: &str, vars: &HashMap, - cb: &Box, -) -> String -where - T: TemplateCallback, -{ + cb: &T, +) -> String { 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, -{ +pub async fn render( + tokens: Tokens, + vars: &HashMap, + cb: &T, +) -> String { let mut doc_str: Vec = Vec::new(); for t in tokens.tokens { @@ -37,10 +39,11 @@ where doc_str.join("") } -async fn render_tag(val: Val, vars: &HashMap, cb: &Box) -> String -where - T: TemplateCallback, -{ +async fn render_tag( + val: Val, + vars: &HashMap, + cb: &T, +) -> String { match val { Val::Str { text } => text.into(), Val::Var { name } => match vars.get(name.as_str()) { @@ -94,14 +97,18 @@ mod tests { struct EmptyCB {} impl TemplateCallback for EmptyCB { - async fn run(&self, _fn_name: &str, _args: HashMap) -> Result{ + async fn run( + &self, + _fn_name: &str, + _args: HashMap, + ) -> Result { todo!() } } #[tokio::test] async fn render_empty() { - let empty_cb = Box::new(EmptyCB {}); + let empty_cb = EmptyCB {}; let template = ""; let vars = HashMap::new(); let result = ""; @@ -113,7 +120,7 @@ mod tests { #[tokio::test] async fn render_text_only() { - let empty_cb = Box::new(EmptyCB {}); + let empty_cb = EmptyCB {}; let template = "Hello World!"; let vars = HashMap::new(); let result = "Hello World!"; @@ -125,7 +132,7 @@ mod tests { #[tokio::test] async fn render_simple() { - let empty_cb = Box::new(EmptyCB {}); + let empty_cb = EmptyCB {}; let template = "${[ foo ]}"; let vars = HashMap::from([("foo".to_string(), "bar".to_string())]); let result = "bar"; @@ -137,7 +144,7 @@ mod tests { #[tokio::test] async fn render_surrounded() { - let empty_cb = Box::new(EmptyCB {}); + let empty_cb = EmptyCB {}; let template = "hello ${[ word ]} world!"; let vars = HashMap::from([("word".to_string(), "cruel".to_string())]); let result = "hello cruel world!"; @@ -168,10 +175,7 @@ mod tests { )) } } - assert_eq!( - parse_and_render(template, &vars, &Box::new(CB {})).await, - result - ); + assert_eq!(parse_and_render(template, &vars, &CB {}).await, result); } #[tokio::test] @@ -195,7 +199,7 @@ mod tests { } assert_eq!( - parse_and_render(template, &vars, &Box::new(CB {})).await, + parse_and_render(template, &vars, &CB {}).await, result.to_string() ); } @@ -218,7 +222,7 @@ mod tests { } assert_eq!( - parse_and_render(template, &vars, &Box::new(CB {})).await, + parse_and_render(template, &vars, &CB {}).await, result.to_string() ); } diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 6b173423..4e2ccc3d 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -1,7 +1,7 @@ import { defaultKeymap } from '@codemirror/commands'; import { Compartment, EditorState, type Extension } from '@codemirror/state'; import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view'; -import type { EnvironmentVariable } from '@yaakapp/api'; +import type { EnvironmentVariable, TemplateFunction } from '@yaakapp/api'; import classNames from 'classnames'; import { EditorView } from 'codemirror'; import type { MutableRefObject, ReactNode } from 'react'; @@ -19,7 +19,7 @@ import { import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables'; import { parseTemplate } from '../../../hooks/useParseTemplate'; import { useSettings } from '../../../hooks/useSettings'; -import { type TemplateFunction, useTemplateFunctions } from '../../../hooks/useTemplateFunctions'; +import { useTemplateFunctions } from '../../../hooks/useTemplateFunctions'; import { useDialog } from '../../DialogContext'; import { TemplateFunctionDialog } from '../../TemplateFunctionDialog'; import { TemplateVariableDialog } from '../../TemplateVariableDialog';