Make rendering return Result, and handle infinite recursion

This commit is contained in:
Gregory Schier
2025-03-05 13:49:45 -08:00
parent 7a1a0689b0
commit fbf4d3c11e
25 changed files with 348 additions and 335 deletions

93
src-tauri/Cargo.lock generated
View File

@@ -31,7 +31,7 @@ checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [ dependencies = [
"getrandom 0.2.15", "getrandom 0.2.15",
"once_cell", "once_cell",
"version_check 0.9.5", "version_check",
] ]
[[package]] [[package]]
@@ -42,7 +42,7 @@ checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
"version_check 0.9.5", "version_check",
"zerocopy 0.7.35", "zerocopy 0.7.35",
] ]
@@ -932,7 +932,7 @@ checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [ dependencies = [
"percent-encoding", "percent-encoding",
"time", "time",
"version_check 0.9.5", "version_check",
] ]
[[package]] [[package]]
@@ -1171,20 +1171,6 @@ version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
[[package]]
name = "datetime"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c3f7a77f3e57fedf80e09136f2d8777ebf621207306f6d96d610af048354bc"
dependencies = [
"iso8601",
"libc",
"locale",
"pad",
"redox_syscall 0.1.57",
"winapi",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.9" version = "0.7.9"
@@ -1914,7 +1900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check 0.9.5", "version_check",
] ]
[[package]] [[package]]
@@ -2584,15 +2570,6 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "iso8601"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43e86914a73535f3f541a765adea0a9fafcf53fa6adb73662c4988fd9233766f"
dependencies = [
"nom 4.2.3",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@@ -2882,15 +2859,6 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "locale"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@@ -2903,9 +2871,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.22" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
dependencies = [ dependencies = [
"value-bag", "value-bag",
] ]
@@ -3155,16 +3123,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
dependencies = [
"memchr",
"version_check 0.1.5",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@@ -3666,15 +3624,6 @@ dependencies = [
"thiserror 1.0.63", "thiserror 1.0.63",
] ]
[[package]]
name = "pad"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@@ -4066,7 +4015,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 1.0.109",
"version_check 0.9.5", "version_check",
] ]
[[package]] [[package]]
@@ -4077,7 +4026,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"version_check 0.9.5", "version_check",
] ]
[[package]] [[package]]
@@ -4410,12 +4359,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@@ -5320,7 +5263,7 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f"
dependencies = [ dependencies = [
"nom 7.1.3", "nom",
"unicode_categories", "unicode_categories",
] ]
@@ -6733,7 +6676,7 @@ version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [ dependencies = [
"version_check 0.9.5", "version_check",
] ]
[[package]] [[package]]
@@ -6769,12 +6712,6 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]] [[package]]
name = "unicode_categories" name = "unicode_categories"
version = "0.1.1" version = "0.1.1"
@@ -6864,12 +6801,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@@ -7710,7 +7641,6 @@ version = "0.0.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"cocoa 0.26.0", "cocoa 0.26.0",
"datetime",
"encoding_rs", "encoding_rs",
"eventsource-client", "eventsource-client",
"hex_color", "hex_color",
@@ -7740,9 +7670,9 @@ dependencies = [
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-window-state", "tauri-plugin-window-state",
"thiserror 2.0.11",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"ts-rs",
"uuid", "uuid",
"yaak-git", "yaak-git",
"yaak-grpc", "yaak-grpc",
@@ -7905,6 +7835,7 @@ dependencies = [
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.11",
"tokio", "tokio",
"ts-rs", "ts-rs",
] ]

View File

@@ -42,7 +42,6 @@ openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu inst
[dependencies] [dependencies]
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
datetime = "0.5.2"
encoding_rs = "0.8.35" encoding_rs = "0.8.35"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" } eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
hex_color = "3.0.0" hex_color = "3.0.0"
@@ -71,7 +70,7 @@ tauri-plugin-updater = "2.5.0"
tauri-plugin-window-state = "2.2.1" tauri-plugin-window-state = "2.2.1"
tokio = { version = "1.43.0", features = ["sync"] } tokio = { version = "1.43.0", features = ["sync"] }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
ts-rs = { workspace = true } thiserror = { workspace = true }
uuid = "1.12.1" uuid = "1.12.1"
yaak-git = { path = "yaak-git" } yaak-git = { path = "yaak-git" }
yaak-grpc = { path = "yaak-grpc" } yaak-grpc = { path = "yaak-grpc" }

43
src-tauri/src/error.rs Normal file
View File

@@ -0,0 +1,43 @@
use serde::{Serialize, Serializer};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Render error: {0}")]
TemplateError(#[from] yaak_templates::error::Error),
#[error("Model error: {0}")]
ModelError(#[from] yaak_models::error::Error),
#[error("Sync error: {0}")]
SyncError(#[from] yaak_sync::error::Error),
#[error("Git error: {0}")]
GitError(#[from] yaak_git::error::Error),
#[error("Websocket error: {0}")]
WebsocketError(#[from] yaak_ws::error::Error),
#[error("License error: {0}")]
LicenseError(#[from] yaak_license::error::Error),
#[error("Plugin error: {0}")]
PluginError(#[from] yaak_plugins::error::Error),
#[error("Request error: {0}")]
RequestError(#[from] reqwest::Error),
#[error("Generic error: {0}")]
GenericError(String),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -1,3 +1,5 @@
use crate::error::Error::GenericError;
use crate::error::Result;
use crate::render::render_http_request; use crate::render::render_http_request;
use crate::response_err; use crate::response_err;
use http::header::{ACCEPT, USER_AGENT}; use http::header::{ACCEPT, USER_AGENT};
@@ -43,14 +45,10 @@ pub async fn send_http_request<R: Runtime>(
environment: Option<Environment>, environment: Option<Environment>,
cookie_jar: Option<CookieJar>, cookie_jar: Option<CookieJar>,
cancelled_rx: &mut Receiver<bool>, cancelled_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> { ) -> Result<HttpResponse> {
let plugin_manager = window.state::<PluginManager>(); let plugin_manager = window.state::<PluginManager>();
let workspace = get_workspace(window, &unrendered_request.workspace_id) let workspace = get_workspace(window, &unrendered_request.workspace_id).await?;
.await let base_environment = get_base_environment(window, &unrendered_request.workspace_id).await?;
.expect("Failed to get Workspace");
let base_environment = get_base_environment(window, &unrendered_request.workspace_id)
.await
.expect("Failed to get base environment");
let settings = get_or_create_settings(window).await; let settings = get_or_create_settings(window).await;
let cb = PluginTemplateCallback::new( let cb = PluginTemplateCallback::new(
window.app_handle(), window.app_handle(),
@@ -61,9 +59,17 @@ pub async fn send_http_request<R: Runtime>(
let response_id = og_response.id.clone(); let response_id = og_response.id.clone();
let response = Arc::new(Mutex::new(og_response.clone())); let response = Arc::new(Mutex::new(og_response.clone()));
let request = let request = match render_http_request(
render_http_request(&unrendered_request, &base_environment, environment.as_ref(), &cb) &unrendered_request,
.await; &base_environment,
environment.as_ref(),
&cb,
)
.await
{
Ok(r) => r,
Err(e) => return Ok(response_err(&*response.lock().await, e.to_string(), window).await),
};
let mut url_string = request.url; let mut url_string = request.url;
@@ -139,10 +145,9 @@ pub async fn send_http_request<R: Runtime>(
serde_json::from_value(json_cookie).expect("Failed to deserialize cookie") serde_json::from_value(json_cookie).expect("Failed to deserialize cookie")
}) })
.map(|c| Ok(c)) .map(|c| Ok(c))
.collect::<Vec<Result<_, ()>>>(); .collect::<Vec<Result<_>>>();
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true) let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)?;
.expect("Failed to create cookie store");
let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(store); let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(store);
let cookie_store = Arc::new(cookie_store); let cookie_store = Arc::new(cookie_store);
client_builder = client_builder.cookie_provider(Arc::clone(&cookie_store)); client_builder = client_builder.cookie_provider(Arc::clone(&cookie_store));
@@ -158,7 +163,7 @@ pub async fn send_http_request<R: Runtime>(
)); ));
} }
let client = client_builder.build().expect("Failed to build client"); let client = client_builder.build()?;
// Render query parameters // Render query parameters
let mut query_params = Vec::new(); let mut query_params = Vec::new();
@@ -193,8 +198,8 @@ pub async fn send_http_request<R: Runtime>(
} }
}; };
let m = Method::from_bytes(request.method.to_uppercase().as_bytes()) let m = Method::from_str(&request.method.to_uppercase())
.expect("Failed to create method"); .map_err(|e| GenericError(e.to_string()))?;
let mut request_builder = client.request(m, url).query(&query_params); let mut request_builder = client.request(m, url).query(&query_params);
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
@@ -282,7 +287,7 @@ pub async fn send_http_request<R: Runtime>(
} else if body_type == "binary" && request_body.contains_key("filePath") { } else if body_type == "binary" && request_body.contains_key("filePath") {
let file_path = request_body let file_path = request_body
.get("filePath") .get("filePath")
.ok_or("filePath not set")? .ok_or(GenericError("filePath not set".to_string()))?
.as_str() .as_str()
.unwrap_or_default(); .unwrap_or_default();
@@ -431,7 +436,7 @@ pub async fn send_http_request<R: Runtime>(
} }
} }
let (resp_tx, resp_rx) = oneshot::channel::<Result<Response, reqwest::Error>>(); let (resp_tx, resp_rx) = oneshot::channel::<std::result::Result<Response, reqwest::Error>>();
let (done_tx, done_rx) = oneshot::channel::<HttpResponse>(); let (done_tx, done_rx) = oneshot::channel::<HttpResponse>();
let start = std::time::Instant::now(); let start = std::time::Instant::now();

View File

@@ -2,11 +2,13 @@ extern crate core;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
extern crate objc; extern crate objc;
use crate::encoding::read_response_body; use crate::encoding::read_response_body;
use crate::error::Error::GenericError;
use crate::grpc::metadata_to_map; use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request; use crate::http_request::send_http_request;
use crate::notifications::YaakNotifier; use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_template}; use crate::render::{render_grpc_request, render_template};
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater}; use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
use error::Result as YaakResult;
use eventsource_client::{EventParser, SSE}; use eventsource_client::{EventParser, SSE};
use log::{debug, error, warn}; use log::{debug, error, warn};
use rand::random; use rand::random;
@@ -65,6 +67,7 @@ use yaak_templates::format::format_json;
use yaak_templates::{Parser, Tokens}; use yaak_templates::{Parser, Tokens};
mod encoding; mod encoding;
mod error;
mod grpc; mod grpc;
mod history; mod history;
mod http_request; mod http_request;
@@ -126,14 +129,13 @@ async fn cmd_render_template<R: Runtime>(
template: &str, template: &str,
workspace_id: &str, workspace_id: &str,
environment_id: Option<&str>, environment_id: Option<&str>,
) -> Result<String, String> { ) -> YaakResult<String> {
let environment = match environment_id { let environment = match environment_id {
Some(id) => Some(get_environment(&window, id).await.map_err(|e| e.to_string())?), Some(id) => get_environment(&window, id).await.ok(),
None => None, None => None,
}; };
let base_environment = let base_environment = get_base_environment(&window, &workspace_id).await?;
get_base_environment(&window, &workspace_id).await.map_err(|e| e.to_string())?; let result = render_template(
let rendered = render_template(
template, template,
&base_environment, &base_environment,
environment.as_ref(), environment.as_ref(),
@@ -143,8 +145,8 @@ async fn cmd_render_template<R: Runtime>(
RenderPurpose::Preview, RenderPurpose::Preview,
), ),
) )
.await; .await?;
Ok(rendered) Ok(result)
} }
#[tauri::command] #[tauri::command]
@@ -189,18 +191,15 @@ async fn cmd_grpc_go<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
grpc_handle: State<'_, Mutex<GrpcHandle>>, grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> Result<String, String> { ) -> YaakResult<String> {
let environment = match environment_id { let environment = match environment_id {
Some(id) => Some(get_environment(&window, id).await.map_err(|e| e.to_string())?), Some(id) => get_environment(&window, id).await.ok(),
None => None, None => None,
}; };
let unrendered_request = get_grpc_request(&window, request_id) let unrendered_request = get_grpc_request(&window, request_id)
.await .await?
.map_err(|e| e.to_string())? .ok_or(GenericError("Failed to get GRPC request".to_string()))?;
.ok_or("Failed to find GRPC request")?; let base_environment = get_base_environment(&window, &unrendered_request.workspace_id).await?;
let base_environment = get_base_environment(&window, &unrendered_request.workspace_id)
.await
.map_err(|e| e.to_string())?;
let request = render_grpc_request( let request = render_grpc_request(
&unrendered_request, &unrendered_request,
&base_environment, &base_environment,
@@ -211,7 +210,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send, RenderPurpose::Send,
), ),
) )
.await; .await?;
let mut metadata = BTreeMap::new(); let mut metadata = BTreeMap::new();
// Add the rest of metadata // Add the rest of metadata
@@ -242,33 +241,27 @@ async fn cmd_grpc_go<R: Runtime>(
}) })
.collect(), .collect(),
}; };
let plugin_result = plugin_manager let plugin_result =
.call_http_authentication(&window, &auth_name, plugin_req) plugin_manager.call_http_authentication(&window, &auth_name, plugin_req).await?;
.await
.map_err(|e| e.to_string())?;
for header in plugin_result.set_headers { for header in plugin_result.set_headers {
metadata.insert(header.name, header.value); metadata.insert(header.name, header.value);
} }
} }
let conn = { let conn = upsert_grpc_connection(
let req = request.clone(); &window,
upsert_grpc_connection( &GrpcConnection {
&window, workspace_id: request.workspace_id.clone(),
&GrpcConnection { request_id: request.id.clone(),
workspace_id: req.workspace_id, status: -1,
request_id: req.id, elapsed: 0,
status: -1, state: GrpcConnectionState::Initialized,
elapsed: 0, url: request.url.clone(),
state: GrpcConnectionState::Initialized, ..Default::default()
url: req.url.clone(), },
..Default::default() &UpdateSource::Window,
}, )
&UpdateSource::Window, .await?;
)
.await
.map_err(|e| e.to_string())?
};
let conn_id = conn.id.clone(); let conn_id = conn.id.clone();
@@ -291,7 +284,7 @@ async fn cmd_grpc_go<R: Runtime>(
let req = request.clone(); let req = request.clone();
match (req.service, req.method) { match (req.service, req.method) {
(Some(service), Some(method)) => (service, method), (Some(service), Some(method)) => (service, method),
_ => return Err("Service and method are required".to_string()), _ => return Err(GenericError("Service and method are required".to_string())),
} }
}; };
@@ -319,13 +312,13 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::Window, &UpdateSource::Window,
) )
.await .await?;
.map_err(|e| e.to_string())?;
return Ok(conn_id); return Ok(conn_id);
} }
}; };
let method_desc = connection.method(&service, &method).map_err(|e| e.to_string())?; let method_desc =
connection.method(&service, &method).map_err(|e| GenericError(e.to_string()))?;
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
enum IncomingMsg { enum IncomingMsg {
@@ -362,23 +355,21 @@ async fn cmd_grpc_go<R: Runtime>(
let window = window.clone(); let window = window.clone();
let base_msg = base_msg.clone(); let base_msg = base_msg.clone();
let method_desc = method_desc.clone(); let method_desc = method_desc.clone();
let msg = { let msg = block_in_place(|| {
block_in_place(|| { tauri::async_runtime::block_on(async {
tauri::async_runtime::block_on(async { render_template(
render_template( msg.as_str(),
msg.as_str(), &workspace,
&workspace, environment.as_ref(),
environment.as_ref(), &PluginTemplateCallback::new(
&PluginTemplateCallback::new( window.app_handle(),
window.app_handle(), &WindowContext::from_window(&window),
&WindowContext::from_window(&window), RenderPurpose::Send,
RenderPurpose::Send, ),
), )
) .await.expect("Failed to render template")
.await
})
}) })
}; });
let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc) let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc)
{ {
Ok(d_msg) => d_msg, Ok(d_msg) => d_msg,
@@ -443,7 +434,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send, RenderPurpose::Send,
), ),
) )
.await; .await?;
upsert_grpc_event( upsert_grpc_event(
&window, &window,
@@ -455,8 +446,7 @@ async fn cmd_grpc_go<R: Runtime>(
}, },
&UpdateSource::Window, &UpdateSource::Window,
) )
.await .await?;
.unwrap();
async move { async move {
let (maybe_stream, maybe_msg) = let (maybe_stream, maybe_msg) =
@@ -738,7 +728,7 @@ async fn cmd_send_ephemeral_request(
environment_id: Option<&str>, environment_id: Option<&str>,
cookie_jar_id: Option<&str>, cookie_jar_id: Option<&str>,
window: WebviewWindow, window: WebviewWindow,
) -> Result<HttpResponse, String> { ) -> YaakResult<HttpResponse> {
let response = HttpResponse::new(); let response = HttpResponse::new();
request.id = "".to_string(); request.id = "".to_string();
let environment = match environment_id { let environment = match environment_id {
@@ -1087,10 +1077,9 @@ async fn cmd_send_http_request(
// condition where the user may have just edited a field before sending // condition where the user may have just edited a field before sending
// that has not yet been saved in the DB. // that has not yet been saved in the DB.
request: HttpRequest, request: HttpRequest,
) -> Result<HttpResponse, String> { ) -> YaakResult<HttpResponse> {
let response = create_default_http_response(&window, &request.id, &UpdateSource::Window) let response =
.await create_default_http_response(&window, &request.id, &UpdateSource::Window).await?;
.map_err(|e| e.to_string())?;
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false); let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
window.listen_any(format!("cancel_http_response_{}", response.id), move |_event| { window.listen_any(format!("cancel_http_response_{}", response.id), move |_event| {

View File

@@ -90,7 +90,8 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
environment.as_ref(), environment.as_ref(),
&cb, &cb,
) )
.await; .await
.expect("Failed to render http request");
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse { Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request, http_request,
})) }))
@@ -107,8 +108,9 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
.await .await
.expect("Failed to get base environment"); .expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data = let data = render_json_value(req.data, &base_environment, environment.as_ref(), &cb)
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await; .await
.expect("Failed to render template");
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data })) Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
} }
InternalEventPayload::ErrorResponse(resp) => { InternalEventPayload::ErrorResponse(resp) => {

View File

@@ -12,7 +12,7 @@ pub async fn render_template<T: TemplateCallback>(
base_environment: &Environment, base_environment: &Environment,
environment: Option<&Environment>, environment: Option<&Environment>,
cb: &T, cb: &T,
) -> String { ) -> yaak_templates::error::Result<String> {
let vars = &make_vars_hashmap(base_environment, environment); let vars = &make_vars_hashmap(base_environment, environment);
render(template, vars, cb).await render(template, vars, cb).await
} }
@@ -22,7 +22,7 @@ pub async fn render_json_value<T: TemplateCallback>(
base_environment: &Environment, base_environment: &Environment,
environment: Option<&Environment>, environment: Option<&Environment>,
cb: &T, cb: &T,
) -> Value { ) -> yaak_templates::error::Result<Value> {
let vars = &make_vars_hashmap(base_environment, environment); let vars = &make_vars_hashmap(base_environment, environment);
render_json_value_raw(value, vars, cb).await render_json_value_raw(value, vars, cb).await
} }
@@ -32,32 +32,32 @@ pub async fn render_grpc_request<T: TemplateCallback>(
base_environment: &Environment, base_environment: &Environment,
environment: Option<&Environment>, environment: Option<&Environment>,
cb: &T, cb: &T,
) -> GrpcRequest { ) -> yaak_templates::error::Result<GrpcRequest> {
let vars = &make_vars_hashmap(base_environment, environment); let vars = &make_vars_hashmap(base_environment, environment);
let mut metadata = Vec::new(); let mut metadata = Vec::new();
for p in r.metadata.clone() { for p in r.metadata.clone() {
metadata.push(GrpcMetadataEntry { metadata.push(GrpcMetadataEntry {
enabled: p.enabled, enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await, name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await, value: render(p.value.as_str(), vars, cb).await?,
id: p.id, id: p.id,
}) })
} }
let mut authentication = BTreeMap::new(); let mut authentication = BTreeMap::new();
for (k, v) in r.authentication.clone() { for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value_raw(v, vars, cb).await); authentication.insert(k, render_json_value_raw(v, vars, cb).await?);
} }
let url = render(r.url.as_str(), vars, cb).await; let url = render(r.url.as_str(), vars, cb).await?;
GrpcRequest { Ok(GrpcRequest {
url, url,
metadata, metadata,
authentication, authentication,
..r.to_owned() ..r.to_owned()
} })
} }
pub async fn render_http_request<T: TemplateCallback>( pub async fn render_http_request<T: TemplateCallback>(
@@ -65,15 +65,15 @@ pub async fn render_http_request<T: TemplateCallback>(
base_environment: &Environment, base_environment: &Environment,
environment: Option<&Environment>, environment: Option<&Environment>,
cb: &T, cb: &T,
) -> HttpRequest { ) -> yaak_templates::error::Result<HttpRequest> {
let vars = &make_vars_hashmap(base_environment, environment); let vars = &make_vars_hashmap(base_environment, environment);
let mut url_parameters = Vec::new(); let mut url_parameters = Vec::new();
for p in r.url_parameters.clone() { for p in r.url_parameters.clone() {
url_parameters.push(HttpUrlParameter { url_parameters.push(HttpUrlParameter {
enabled: p.enabled, enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await, name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await, value: render(p.value.as_str(), vars, cb).await?,
id: p.id, id: p.id,
}) })
} }
@@ -82,41 +82,41 @@ pub async fn render_http_request<T: TemplateCallback>(
for p in r.headers.clone() { for p in r.headers.clone() {
headers.push(HttpRequestHeader { headers.push(HttpRequestHeader {
enabled: p.enabled, enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await, name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await, value: render(p.value.as_str(), vars, cb).await?,
id: p.id, id: p.id,
}) })
} }
let mut body = BTreeMap::new(); let mut body = BTreeMap::new();
for (k, v) in r.body.clone() { for (k, v) in r.body.clone() {
body.insert(k, render_json_value_raw(v, vars, cb).await); body.insert(k, render_json_value_raw(v, vars, cb).await?);
} }
let mut authentication = BTreeMap::new(); let mut authentication = BTreeMap::new();
for (k, v) in r.authentication.clone() { for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value_raw(v, vars, cb).await); authentication.insert(k, render_json_value_raw(v, vars, cb).await?);
} }
let url = render(r.url.clone().as_str(), vars, cb).await; let url = render(r.url.clone().as_str(), vars, cb).await?;
// This doesn't fit perfectly with the concept of "rendering" but it kind of does // This doesn't fit perfectly with the concept of "rendering" but it kind of does
let (url, url_parameters) = apply_path_placeholders(&url, url_parameters); let (url, url_parameters) = apply_path_placeholders(&url, url_parameters);
HttpRequest { Ok(HttpRequest {
url, url,
url_parameters, url_parameters,
headers, headers,
body, body,
authentication, authentication,
..r.to_owned() ..r.to_owned()
} })
} }
pub async fn render<T: TemplateCallback>( pub async fn render<T: TemplateCallback>(
template: &str, template: &str,
vars: &HashMap<String, String>, vars: &HashMap<String, String>,
cb: &T, cb: &T,
) -> String { ) -> yaak_templates::error::Result<String> {
parse_and_render(template, vars, cb).await parse_and_render(template, vars, cb).await
} }

View File

@@ -8,7 +8,7 @@ use tauri::{
mod branch; mod branch;
mod callbacks; mod callbacks;
mod commands; mod commands;
mod error; pub mod error;
mod fetch; mod fetch;
mod git; mod git;
mod merge; mod merge;

View File

@@ -6,15 +6,15 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
chrono = "0.4.38"
log = "0.4.26"
reqwest = { workspace = true, features = ["json"] } reqwest = { workspace = true, features = ["json"] }
serde = { version = "1.0.208", features = ["derive"] } serde = { version = "1.0.208", features = ["derive"] }
ts-rs = { workspace = true }
thiserror = { workspace = true }
tauri = { workspace = true }
yaak-models = { workspace = true }
chrono = "0.4.38"
log = "0.4.22"
serde_json = "1.0.132" serde_json = "1.0.132"
tauri = { workspace = true }
thiserror = { workspace = true }
ts-rs = { workspace = true }
yaak-models = { workspace = true }
[build-dependencies] [build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] } tauri-plugin = { workspace = true, features = ["build"] }

View File

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

View File

@@ -8,6 +8,6 @@ export type ActivateLicenseResponsePayload = { activationId: string, };
export type CheckActivationResponsePayload = { active: boolean, }; export type CheckActivationResponsePayload = { active: boolean, };
export type DeactivateLicenseRequestPayload = { licenseKey: string, appVersion: string, appPlatform: string, }; export type DeactivateLicenseRequestPayload = { appVersion: string, appPlatform: string, };
export type LicenseCheckStatus = { "type": "personal_use", trial_ended: string, } | { "type": "commercial_use" } | { "type": "invalid_license" } | { "type": "trialing", end: string, }; export type LicenseCheckStatus = { "type": "personal_use", trial_ended: string, } | { "type": "commercial_use" } | { "type": "invalid_license" } | { "type": "trialing", end: string, };

View File

@@ -1,5 +1,8 @@
use crate::errors::Result; use crate::error::Result;
use crate::{activate_license, check_license, deactivate_license, get_activation_id, ActivateLicenseRequestPayload, CheckActivationRequestPayload, DeactivateLicenseRequestPayload, LicenseCheckStatus}; use crate::{
activate_license, check_license, deactivate_license, ActivateLicenseRequestPayload,
CheckActivationRequestPayload, DeactivateLicenseRequestPayload, LicenseCheckStatus,
};
use log::{debug, info}; use log::{debug, info};
use std::string::ToString; use std::string::ToString;
use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow}; use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
@@ -7,10 +10,14 @@ use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
#[command] #[command]
pub async fn check<R: Runtime>(app_handle: AppHandle<R>) -> Result<LicenseCheckStatus> { pub async fn check<R: Runtime>(app_handle: AppHandle<R>) -> Result<LicenseCheckStatus> {
debug!("Checking license"); debug!("Checking license");
check_license(&app_handle, CheckActivationRequestPayload{ check_license(
app_platform: get_os().to_string(), &app_handle,
app_version: app_handle.package_info().version.to_string(), CheckActivationRequestPayload {
}).await app_platform: get_os().to_string(),
app_version: app_handle.package_info().version.to_string(),
},
)
.await
} }
#[command] #[command]

View File

@@ -5,7 +5,7 @@ use tauri::{
}; };
mod commands; mod commands;
mod errors; pub mod error;
mod license; mod license;
use crate::commands::{activate, check, deactivate}; use crate::commands::{activate, check, deactivate};

View File

@@ -1,5 +1,5 @@
use crate::errors::Error::{ClientError, ServerError}; use crate::error::Error::{ClientError, ServerError};
use crate::errors::Result; use crate::error::Result;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@@ -27,6 +27,7 @@ use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tokio::time::timeout; use tokio::time::timeout;
use yaak_models::queries::{generate_id, list_plugins}; use yaak_models::queries::{generate_id, list_plugins};
use yaak_templates::error::Error::RenderError;
#[derive(Clone)] #[derive(Clone)]
pub struct PluginManager { pub struct PluginManager {
@@ -596,7 +597,7 @@ impl PluginManager {
fn_name: &str, fn_name: &str,
args: HashMap<String, String>, args: HashMap<String, String>,
purpose: RenderPurpose, purpose: RenderPurpose,
) -> Result<Option<String>> { ) -> yaak_templates::error::Result<String> {
let req = CallTemplateFunctionRequest { let req = CallTemplateFunctionRequest {
name: fn_name.to_string(), name: fn_name.to_string(),
args: CallTemplateFunctionArgs { args: CallTemplateFunctionArgs {
@@ -607,7 +608,8 @@ impl PluginManager {
let events = self let events = self
.send_and_wait(window_context, &InternalEventPayload::CallTemplateFunctionRequest(req)) .send_and_wait(window_context, &InternalEventPayload::CallTemplateFunctionRequest(req))
.await?; .await
.map_err(|e| RenderError(format!("Failed to call template function {e:}")))?;
let value = events.into_iter().find_map(|e| match e.payload { let value = events.into_iter().find_map(|e| match e.payload {
InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse { InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse {
@@ -616,7 +618,10 @@ impl PluginManager {
_ => None, _ => None,
}); });
Ok(value) match value {
None => Err(RenderError(format!("Template function not found {fn_name}"))),
Some(v) => Ok(v),
}
} }
pub async fn import_data<R: Runtime>( pub async fn import_data<R: Runtime>(

View File

@@ -1,7 +1,8 @@
use crate::events::{FormInput, RenderPurpose, WindowContext}; use crate::events::{RenderPurpose, WindowContext};
use crate::manager::PluginManager; use crate::manager::PluginManager;
use std::collections::HashMap; use std::collections::HashMap;
use tauri::{AppHandle, Manager, Runtime}; use tauri::{AppHandle, Manager, Runtime};
use yaak_templates::error::Result;
use yaak_templates::TemplateCallback; use yaak_templates::TemplateCallback;
#[derive(Clone)] #[derive(Clone)]
@@ -27,51 +28,20 @@ impl PluginTemplateCallback {
} }
impl TemplateCallback for PluginTemplateCallback { impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> { async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
// The beta named the function `Response` but was changed in stable. // The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate // Keep this here for a while because there's no easy way to migrate
let fn_name = if fn_name == "Response" { "response" } else { fn_name }; let fn_name = if fn_name == "Response" { "response" } else { fn_name };
let function = self
.plugin_manager
.get_template_functions_with_context(&self.window_context)
.await
.map_err(|e| e.to_string())?
.iter()
.flat_map(|f| f.functions.clone())
.find(|f| f.name == fn_name)
.ok_or("")?;
let mut args_with_defaults = args.clone();
// Fill in default values for all args
for arg in function.args {
let base = match arg {
FormInput::Text(a) => a.base,
FormInput::Editor(a) => a.base,
FormInput::Select(a) => a.base,
FormInput::Checkbox(a) => a.base,
FormInput::File(a) => a.base,
FormInput::HttpRequest(a) => a.base,
FormInput::Accordion(_) => continue,
FormInput::Banner(_) => continue,
FormInput::Markdown(_) => continue,
};
if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
}
}
let resp = self let resp = self
.plugin_manager .plugin_manager
.call_template_function( .call_template_function(
&self.window_context, &self.window_context,
fn_name, fn_name,
args_with_defaults, args,
self.render_purpose.to_owned(), self.render_purpose.to_owned(),
) )
.await .await?;
.map_err(|e| e.to_string())?; Ok(resp)
Ok(resp.unwrap_or_default())
} }
} }

View File

@@ -7,6 +7,7 @@ publish = false
[dependencies] [dependencies]
log = "0.4.22" log = "0.4.22"
serde = { version = "1.0.208", features = ["derive"] } serde = { version = "1.0.208", features = ["derive"] }
ts-rs = { version = "10.0.0" }
tokio = { version = "1.39.3", features = ["macros", "rt"] }
serde_json = "1.0.132" serde_json = "1.0.132"
thiserror = { workspace = true }
tokio = { version = "1.39.3", features = ["macros", "rt"] }
ts-rs = { version = "10.0.0" }

View File

@@ -0,0 +1,22 @@
use serde::{Serialize, Serializer};
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum Error {
#[error("Render Error: {0}")]
RenderError(String),
#[error("Render Error: Max recursion depth exceeded")]
RenderStackExceededError,
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -1,6 +1,7 @@
pub mod format; pub mod format;
pub mod parser; pub mod parser;
pub mod renderer; pub mod renderer;
pub mod error;
pub use parser::*; pub use parser::*;
pub use renderer::*; pub use renderer::*;

View File

@@ -1,82 +1,103 @@
use crate::error::Error::RenderStackExceededError;
use crate::error::Result;
use crate::{FnArg, Parser, Token, Tokens, Val}; use crate::{FnArg, Parser, Token, Tokens, Val};
use log::warn; use log::warn;
use serde_json::json; use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
use std::future::Future; use std::future::Future;
const MAX_DEPTH: usize = 50;
pub trait TemplateCallback { pub trait TemplateCallback {
fn run( fn run(
&self, &self,
fn_name: &str, fn_name: &str,
args: HashMap<String, String>, args: HashMap<String, String>,
) -> impl Future<Output = Result<String, String>> + Send; ) -> impl Future<Output = Result<String>> + Send;
} }
pub async fn render_json_value_raw<T: TemplateCallback>( pub async fn render_json_value_raw<T: TemplateCallback>(
v: serde_json::Value, v: serde_json::Value,
vars: &HashMap<String, String>, vars: &HashMap<String, String>,
cb: &T, cb: &T,
) -> serde_json::Value { ) -> Result<serde_json::Value> {
match v { let v = match v {
serde_json::Value::String(s) => json!(parse_and_render(&s, vars, cb).await), serde_json::Value::String(s) => json!(parse_and_render(&s, vars, cb).await?),
serde_json::Value::Array(a) => { serde_json::Value::Array(a) => {
let mut new_a = Vec::new(); let mut new_a = Vec::new();
for v in a { for v in a {
new_a.push(Box::pin(render_json_value_raw(v, vars, cb)).await) new_a.push(Box::pin(render_json_value_raw(v, vars, cb)).await?)
} }
json!(new_a) json!(new_a)
} }
serde_json::Value::Object(o) => { serde_json::Value::Object(o) => {
let mut new_o = serde_json::Map::new(); let mut new_o = serde_json::Map::new();
for (k, v) in o { for (k, v) in o {
let key = Box::pin(parse_and_render(&k, vars, cb)).await; let key = Box::pin(parse_and_render(&k, vars, cb)).await?;
let value = Box::pin(render_json_value_raw(v, vars, cb)).await; let value = Box::pin(render_json_value_raw(v, vars, cb)).await?;
new_o.insert(key, value); new_o.insert(key, value);
} }
json!(new_o) json!(new_o)
} }
v => v, v => v,
} };
Ok(v)
}
async fn parse_and_render_with_depth<T: TemplateCallback>(
template: &str,
vars: &HashMap<String, String>,
cb: &T,
depth: usize,
) -> Result<String> {
let mut p = Parser::new(template);
let tokens = p.parse();
render(tokens, vars, cb, depth + 1).await
} }
pub async fn parse_and_render<T: TemplateCallback>( pub async fn parse_and_render<T: TemplateCallback>(
template: &str, template: &str,
vars: &HashMap<String, String>, vars: &HashMap<String, String>,
cb: &T, cb: &T,
) -> String { ) -> Result<String> {
let mut p = Parser::new(template); parse_and_render_with_depth(template, vars, cb, 1).await
let tokens = p.parse();
render(tokens, vars, cb).await
} }
pub async fn render<T: TemplateCallback>( pub async fn render<T: TemplateCallback>(
tokens: Tokens, tokens: Tokens,
vars: &HashMap<String, String>, vars: &HashMap<String, String>,
cb: &T, cb: &T,
) -> String { mut depth: usize,
) -> Result<String> {
depth += 1;
if depth > MAX_DEPTH {
return Err(RenderStackExceededError);
}
let mut doc_str: Vec<String> = Vec::new(); let mut doc_str: Vec<String> = Vec::new();
for t in tokens.tokens { for t in tokens.tokens {
match t { match t {
Token::Raw { text } => doc_str.push(text), Token::Raw { text } => doc_str.push(text),
Token::Tag { val } => doc_str.push(render_tag(val, &vars, cb).await), Token::Tag { val } => doc_str.push(render_tag(val, &vars, cb, depth).await?),
Token::Eof => {} Token::Eof => {}
} }
} }
doc_str.join("") Ok(doc_str.join(""))
} }
async fn render_tag<T: TemplateCallback>( async fn render_tag<T: TemplateCallback>(
val: Val, val: Val,
vars: &HashMap<String, String>, vars: &HashMap<String, String>,
cb: &T, cb: &T,
) -> String { depth: usize,
match val { ) -> Result<String> {
let v = match val {
Val::Str { text } => text.into(), Val::Str { text } => text.into(),
Val::Var { name } => match vars.get(name.as_str()) { Val::Var { name } => match vars.get(name.as_str()) {
Some(v) => { Some(v) => {
let r = Box::pin(parse_and_render(v, vars, cb)).await; let r = Box::pin(parse_and_render_with_depth(v, vars, cb, depth)).await?;
r.to_string() r.to_string()
} }
None => "".into(), None => "".into(),
@@ -99,7 +120,7 @@ async fn render_tag<T: TemplateCallback>(
vars.get(var_name.as_str()).unwrap_or(&empty).to_string(), vars.get(var_name.as_str()).unwrap_or(&empty).to_string(),
), ),
FnArg { name, value: val } => { FnArg { name, value: val } => {
let r = Box::pin(render_tag(val.clone(), vars, cb)).await; let r = Box::pin(render_tag(val.clone(), vars, cb, depth)).await?;
(name.to_string(), r) (name.to_string(), r)
} }
}; };
@@ -114,11 +135,15 @@ async fn render_tag<T: TemplateCallback>(
} }
} }
Val::Null => "".into(), Val::Null => "".into(),
} };
Ok(v)
} }
#[cfg(test)] #[cfg(test)]
mod parse_and_render_tests { mod parse_and_render_tests {
use crate::error::Error::{RenderError, RenderStackExceededError};
use crate::error::Result;
use crate::renderer::TemplateCallback; use crate::renderer::TemplateCallback;
use crate::*; use crate::*;
use std::collections::HashMap; use std::collections::HashMap;
@@ -126,44 +151,43 @@ mod parse_and_render_tests {
struct EmptyCB {} struct EmptyCB {}
impl TemplateCallback for EmptyCB { impl TemplateCallback for EmptyCB {
async fn run( async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
todo!() todo!()
} }
} }
#[tokio::test] #[tokio::test]
async fn render_empty() { async fn render_empty() -> Result<()> {
let empty_cb = EmptyCB {}; let empty_cb = EmptyCB {};
let template = ""; let template = "";
let vars = HashMap::new(); let vars = HashMap::new();
let result = ""; let result = "";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string()); assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_text_only() { async fn render_text_only() -> Result<()> {
let empty_cb = EmptyCB {}; let empty_cb = EmptyCB {};
let template = "Hello World!"; let template = "Hello World!";
let vars = HashMap::new(); let vars = HashMap::new();
let result = "Hello World!"; let result = "Hello World!";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string()); assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_simple() { async fn render_simple() -> Result<()> {
let empty_cb = EmptyCB {}; let empty_cb = EmptyCB {};
let template = "${[ foo ]}"; let template = "${[ foo ]}";
let vars = HashMap::from([("foo".to_string(), "bar".to_string())]); let vars = HashMap::from([("foo".to_string(), "bar".to_string())]);
let result = "bar"; let result = "bar";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string()); assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_recursive_var() { async fn render_recursive_var() -> Result<()> {
let empty_cb = EmptyCB {}; let empty_cb = EmptyCB {};
let template = "${[ foo ]}"; let template = "${[ foo ]}";
let mut vars = HashMap::new(); let mut vars = HashMap::new();
@@ -172,49 +196,58 @@ mod parse_and_render_tests {
vars.insert("baz".to_string(), "baz".to_string()); vars.insert("baz".to_string(), "baz".to_string());
let result = "foo: bar: baz"; let result = "foo: bar: baz";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string()); assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_surrounded() { async fn render_self_referencing_var() -> Result<()> {
let empty_cb = EmptyCB {};
let template = "${[ foo ]}";
let mut vars = HashMap::new();
vars.insert("foo".to_string(), "${[ foo ]}".to_string());
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
Err(RenderStackExceededError)
);
Ok(())
}
#[tokio::test]
async fn render_surrounded() -> Result<()> {
let empty_cb = EmptyCB {}; let empty_cb = EmptyCB {};
let template = "hello ${[ word ]} world!"; let template = "hello ${[ word ]} world!";
let vars = HashMap::from([("word".to_string(), "cruel".to_string())]); let vars = HashMap::from([("word".to_string(), "cruel".to_string())]);
let result = "hello cruel world!"; let result = "hello cruel world!";
assert_eq!(parse_and_render(template, &vars, &empty_cb).await, result.to_string()); assert_eq!(parse_and_render(template, &vars, &empty_cb).await?, result.to_string());
Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_valid_fn() { async fn render_valid_fn() -> Result<()> {
let vars = HashMap::new(); let vars = HashMap::new();
let template = r#"${[ say_hello(a='John', b='Kate') ]}"#; let template = r#"${[ say_hello(a='John', b='Kate') ]}"#;
let result = r#"say_hello: 2, Some("John") Some("Kate")"#; let result = r#"say_hello: 2, Some("John") Some("Kate")"#;
struct CB {} struct CB {}
impl TemplateCallback for CB { impl TemplateCallback for CB {
async fn run( async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> Result<String, String> {
Ok(format!("{fn_name}: {}, {:?} {:?}", args.len(), args.get("a"), args.get("b"))) Ok(format!("{fn_name}: {}, {:?} {:?}", args.len(), args.get("a"), args.get("b")))
} }
} }
assert_eq!(parse_and_render(template, &vars, &CB {}).await, result); assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result);
Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_nested_fn() { async fn render_nested_fn() -> Result<()> {
let vars = HashMap::new(); let vars = HashMap::new();
let template = r#"${[ upper(foo=secret()) ]}"#; let template = r#"${[ upper(foo=secret()) ]}"#;
let result = r#"ABC"#; let result = r#"ABC"#;
struct CB {} struct CB {}
impl TemplateCallback for CB { impl TemplateCallback for CB {
async fn run( async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> Result<String, String> {
Ok(match fn_name { Ok(match fn_name {
"secret" => "abc".to_string(), "secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(), "upper" => args["foo"].to_string().to_uppercase(),
@@ -223,80 +256,79 @@ mod parse_and_render_tests {
} }
} }
assert_eq!(parse_and_render(template, &vars, &CB {}).await, result.to_string()); assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_fn_err() { async fn render_fn_err() -> Result<()> {
let vars = HashMap::new(); let vars = HashMap::new();
let template = r#"${[ error() ]}"#; let template = r#"${[ error() ]}"#;
let result = r#""#; let result = r#""#;
struct CB {} struct CB {}
impl TemplateCallback for CB { impl TemplateCallback for CB {
async fn run( async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
&self, Err(RenderError("Failed to do it!".to_string()))
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
Err("Failed to do it!".to_string())
} }
} }
assert_eq!(parse_and_render(template, &vars, &CB {}).await, result.to_string()); assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
Ok(())
} }
} }
#[cfg(test)] #[cfg(test)]
mod render_json_value_raw_tests { mod render_json_value_raw_tests {
use crate::error::Result;
use crate::{render_json_value_raw, TemplateCallback};
use serde_json::json; use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
use crate::{render_json_value_raw, TemplateCallback};
struct EmptyCB {} struct EmptyCB {}
impl TemplateCallback for EmptyCB { impl TemplateCallback for EmptyCB {
async fn run( async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
todo!() todo!()
} }
} }
#[tokio::test] #[tokio::test]
async fn render_json_value_string() { async fn render_json_value_string() -> Result<()> {
let v = json!("${[a]}"); let v = json!("${[a]}");
let mut vars = HashMap::new(); let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string()); vars.insert("a".to_string(), "aaa".to_string());
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await; assert_eq!(render_json_value_raw(v, &vars, &EmptyCB {}).await?, json!("aaa"));
assert_eq!(result, json!("aaa")) Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_json_value_array() { async fn render_json_value_array() -> Result<()> {
let v = json!(["${[a]}", "${[a]}"]); let v = json!(["${[a]}", "${[a]}"]);
let mut vars = HashMap::new(); let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string()); vars.insert("a".to_string(), "aaa".to_string());
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await; let result = render_json_value_raw(v, &vars, &EmptyCB {}).await?;
assert_eq!(result, json!(["aaa", "aaa"])) assert_eq!(result, json!(["aaa", "aaa"]));
Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_json_value_object() { async fn render_json_value_object() -> Result<()> {
let v = json!({"${[a]}": "${[a]}"}); let v = json!({"${[a]}": "${[a]}"});
let mut vars = HashMap::new(); let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string()); vars.insert("a".to_string(), "aaa".to_string());
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await; let result = render_json_value_raw(v, &vars, &EmptyCB {}).await?;
assert_eq!(result, json!({"aaa": "aaa"})) assert_eq!(result, json!({"aaa": "aaa"}));
Ok(())
} }
#[tokio::test] #[tokio::test]
async fn render_json_value_nested() { async fn render_json_value_nested() -> Result<()> {
let v = json!([ let v = json!([
123, 123,
{"${[a]}": "${[a]}"}, {"${[a]}": "${[a]}"},
@@ -308,7 +340,7 @@ mod render_json_value_raw_tests {
let mut vars = HashMap::new(); let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string()); vars.insert("a".to_string(), "aaa".to_string());
let result = render_json_value_raw(v, &vars, &EmptyCB {}).await; let result = render_json_value_raw(v, &vars, &EmptyCB {}).await?;
assert_eq!( assert_eq!(
result, result,
json!([ json!([
@@ -319,6 +351,8 @@ mod render_json_value_raw_tests {
false, false,
{"x": ["aaa"]} {"x": ["aaa"]}
]) ])
) );
Ok(())
} }
} }

View File

@@ -116,7 +116,7 @@ pub(crate) async fn send<R: Runtime>(
RenderPurpose::Send, RenderPurpose::Send,
), ),
) )
.await; .await?;
let mut ws_manager = ws_manager.lock().await; let mut ws_manager = ws_manager.lock().await;
ws_manager.send(&connection.id, Message::Text(request.message.clone().into())).await?; ws_manager.send(&connection.id, Message::Text(request.message.clone().into())).await?;
@@ -214,7 +214,7 @@ pub(crate) async fn connect<R: Runtime>(
RenderPurpose::Send, RenderPurpose::Send,
), ),
) )
.await; .await?;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
if let Some(auth_name) = request.authentication_type.clone() { if let Some(auth_name) = request.authentication_type.clone() {

View File

@@ -13,6 +13,9 @@ pub enum Error {
#[error("Plugin error: {0}")] #[error("Plugin error: {0}")]
PluginError(#[from] yaak_plugins::error::Error), PluginError(#[from] yaak_plugins::error::Error),
#[error("Render error: {0}")]
TemplateError(#[from] yaak_templates::error::Error),
#[error("WebSocket error: {0}")] #[error("WebSocket error: {0}")]
GenericError(String), GenericError(String),
} }

View File

@@ -1,6 +1,6 @@
mod commands; mod commands;
mod connect; mod connect;
mod error; pub mod error;
mod manager; mod manager;
mod render; mod render;

View File

@@ -1,3 +1,4 @@
use crate::error::Result;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use yaak_models::models::{Environment, HttpRequestHeader, WebsocketRequest}; use yaak_models::models::{Environment, HttpRequestHeader, WebsocketRequest};
use yaak_models::render::make_vars_hashmap; use yaak_models::render::make_vars_hashmap;
@@ -8,33 +9,33 @@ pub async fn render_request<T: TemplateCallback>(
base_environment: &Environment, base_environment: &Environment,
environment: Option<&Environment>, environment: Option<&Environment>,
cb: &T, cb: &T,
) -> WebsocketRequest { ) -> Result<WebsocketRequest> {
let vars = &make_vars_hashmap(base_environment, environment); let vars = &make_vars_hashmap(base_environment, environment);
let mut headers = Vec::new(); let mut headers = Vec::new();
for p in r.headers.clone() { for p in r.headers.clone() {
headers.push(HttpRequestHeader { headers.push(HttpRequestHeader {
enabled: p.enabled, enabled: p.enabled,
name: parse_and_render(&p.name, vars, cb).await, name: parse_and_render(&p.name, vars, cb).await?,
value: parse_and_render(&p.value, vars, cb).await, value: parse_and_render(&p.value, vars, cb).await?,
id: p.id, id: p.id,
}) })
} }
let mut authentication = BTreeMap::new(); let mut authentication = BTreeMap::new();
for (k, v) in r.authentication.clone() { for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value_raw(v, vars, cb).await); authentication.insert(k, render_json_value_raw(v, vars, cb).await?);
} }
let url = parse_and_render(r.url.as_str(), vars, cb).await; let url = parse_and_render(r.url.as_str(), vars, cb).await?;
let message = parse_and_render(&r.message.clone(), vars, cb).await; let message = parse_and_render(&r.message.clone(), vars, cb).await?;
WebsocketRequest { Ok(WebsocketRequest {
url, url,
headers, headers,
authentication, authentication,
message, message,
..r.to_owned() ..r.to_owned()
} })
} }