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

View File

@@ -42,7 +42,6 @@ openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu inst
[dependencies]
chrono = { version = "0.4.31", features = ["serde"] }
datetime = "0.5.2"
encoding_rs = "0.8.35"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
hex_color = "3.0.0"
@@ -71,7 +70,7 @@ tauri-plugin-updater = "2.5.0"
tauri-plugin-window-state = "2.2.1"
tokio = { version = "1.43.0", features = ["sync"] }
tokio-stream = "0.1.17"
ts-rs = { workspace = true }
thiserror = { workspace = true }
uuid = "1.12.1"
yaak-git = { path = "yaak-git" }
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::response_err;
use http::header::{ACCEPT, USER_AGENT};
@@ -43,14 +45,10 @@ pub async fn send_http_request<R: Runtime>(
environment: Option<Environment>,
cookie_jar: Option<CookieJar>,
cancelled_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> {
) -> Result<HttpResponse> {
let plugin_manager = window.state::<PluginManager>();
let workspace = get_workspace(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 workspace = get_workspace(window, &unrendered_request.workspace_id).await?;
let base_environment = get_base_environment(window, &unrendered_request.workspace_id).await?;
let settings = get_or_create_settings(window).await;
let cb = PluginTemplateCallback::new(
window.app_handle(),
@@ -61,9 +59,17 @@ pub async fn send_http_request<R: Runtime>(
let response_id = og_response.id.clone();
let response = Arc::new(Mutex::new(og_response.clone()));
let request =
render_http_request(&unrendered_request, &base_environment, environment.as_ref(), &cb)
.await;
let request = match render_http_request(
&unrendered_request,
&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;
@@ -139,10 +145,9 @@ pub async fn send_http_request<R: Runtime>(
serde_json::from_value(json_cookie).expect("Failed to deserialize cookie")
})
.map(|c| Ok(c))
.collect::<Vec<Result<_, ()>>>();
.collect::<Vec<Result<_>>>();
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)
.expect("Failed to create cookie store");
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)?;
let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(store);
let cookie_store = Arc::new(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
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())
.expect("Failed to create method");
let m = Method::from_str(&request.method.to_uppercase())
.map_err(|e| GenericError(e.to_string()))?;
let mut request_builder = client.request(m, url).query(&query_params);
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") {
let file_path = request_body
.get("filePath")
.ok_or("filePath not set")?
.ok_or(GenericError("filePath not set".to_string()))?
.as_str()
.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 start = std::time::Instant::now();

View File

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

View File

@@ -12,7 +12,7 @@ pub async fn render_template<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> String {
) -> yaak_templates::error::Result<String> {
let vars = &make_vars_hashmap(base_environment, environment);
render(template, vars, cb).await
}
@@ -22,7 +22,7 @@ pub async fn render_json_value<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> Value {
) -> yaak_templates::error::Result<Value> {
let vars = &make_vars_hashmap(base_environment, environment);
render_json_value_raw(value, vars, cb).await
}
@@ -32,32 +32,32 @@ pub async fn render_grpc_request<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> GrpcRequest {
) -> yaak_templates::error::Result<GrpcRequest> {
let vars = &make_vars_hashmap(base_environment, environment);
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,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,
id: p.id,
})
}
let mut authentication = BTreeMap::new();
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,
metadata,
authentication,
..r.to_owned()
}
})
}
pub async fn render_http_request<T: TemplateCallback>(
@@ -65,15 +65,15 @@ pub async fn render_http_request<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> HttpRequest {
) -> yaak_templates::error::Result<HttpRequest> {
let vars = &make_vars_hashmap(base_environment, environment);
let mut url_parameters = Vec::new();
for p in r.url_parameters.clone() {
url_parameters.push(HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,
id: p.id,
})
}
@@ -82,41 +82,41 @@ pub async fn render_http_request<T: TemplateCallback>(
for p in r.headers.clone() {
headers.push(HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,
id: p.id,
})
}
let mut body = BTreeMap::new();
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();
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
let (url, url_parameters) = apply_path_placeholders(&url, url_parameters);
HttpRequest {
Ok(HttpRequest {
url,
url_parameters,
headers,
body,
authentication,
..r.to_owned()
}
})
}
pub async fn render<T: TemplateCallback>(
template: &str,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
) -> yaak_templates::error::Result<String> {
parse_and_render(template, vars, cb).await
}

View File

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

View File

@@ -6,15 +6,15 @@ edition = "2021"
publish = false
[dependencies]
chrono = "0.4.38"
log = "0.4.26"
reqwest = { workspace = true, features = ["json"] }
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"
tauri = { workspace = true }
thiserror = { workspace = true }
ts-rs = { workspace = true }
yaak-models = { workspace = true }
[build-dependencies]
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.
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 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, };

View File

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

View File

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

View File

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

View File

@@ -27,6 +27,7 @@ use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
use tokio::time::timeout;
use yaak_models::queries::{generate_id, list_plugins};
use yaak_templates::error::Error::RenderError;
#[derive(Clone)]
pub struct PluginManager {
@@ -596,7 +597,7 @@ impl PluginManager {
fn_name: &str,
args: HashMap<String, String>,
purpose: RenderPurpose,
) -> Result<Option<String>> {
) -> yaak_templates::error::Result<String> {
let req = CallTemplateFunctionRequest {
name: fn_name.to_string(),
args: CallTemplateFunctionArgs {
@@ -607,7 +608,8 @@ impl PluginManager {
let events = self
.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 {
InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse {
@@ -616,7 +618,10 @@ impl PluginManager {
_ => 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>(

View File

@@ -1,7 +1,8 @@
use crate::events::{FormInput, RenderPurpose, WindowContext};
use crate::events::{RenderPurpose, WindowContext};
use crate::manager::PluginManager;
use std::collections::HashMap;
use tauri::{AppHandle, Manager, Runtime};
use yaak_templates::error::Result;
use yaak_templates::TemplateCallback;
#[derive(Clone)]
@@ -27,51 +28,20 @@ impl 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.
// 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 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
.plugin_manager
.call_template_function(
&self.window_context,
fn_name,
args_with_defaults,
args,
self.render_purpose.to_owned(),
)
.await
.map_err(|e| e.to_string())?;
Ok(resp.unwrap_or_default())
.await?;
Ok(resp)
}
}

View File

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

View File

@@ -116,7 +116,7 @@ pub(crate) async fn send<R: Runtime>(
RenderPurpose::Send,
),
)
.await;
.await?;
let mut ws_manager = ws_manager.lock().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,
),
)
.await;
.await?;
let mut headers = HeaderMap::new();
if let Some(auth_name) = request.authentication_type.clone() {

View File

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

View File

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

View File

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