refactor(tauri) execute_promise String/Serialize management (#724)

This commit is contained in:
Lucas Fernandes Nogueira 2020-06-29 15:39:39 -03:00 committed by GitHub
parent 6d23b0673e
commit 6b097345ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 157 additions and 176 deletions

View File

@ -0,0 +1,8 @@
---
"tauri-api": minor
"tauri": minor
---
The `execute_promise` and `execute_promise_sync` helpers now accepts any `tauri::Result<T>` where `T: impl Serialize`.
This means that you do not need to serialize your response manually or deal with String quotes anymore.
As part of this refactor, the `event::emit` function also supports `impl Serialize` instead of `String`.

View File

@ -18,7 +18,7 @@ export interface HttpOptions {
method: HttpVerb
url: string
headers?: Record<string, any>
propertys?: Record<string, any>
params?: Record<string, any>
body?: Body
followRedirects: boolean
maxRedirections: boolean

View File

@ -221,7 +221,7 @@ impl HttpRequestBuilder {
///
/// The response will be transformed to String,
/// If reading the response as binary, the byte array will be serialized using serde_json
pub fn make_request(options: HttpRequestOptions) -> crate::Result<String> {
pub fn make_request(options: HttpRequestOptions) -> crate::Result<Value> {
let method = Method::from_bytes(options.method.to_uppercase().as_bytes())?;
let mut builder = RequestBuilder::new(method, options.url);
if let Some(params) = options.params {
@ -291,12 +291,9 @@ pub fn make_request(options: HttpRequestOptions) -> crate::Result<String> {
let response = response?;
if response.is_success() {
let response_data = match options.response_type.unwrap_or(ResponseType::Json) {
ResponseType::Json => {
let result = response.json::<Value>()?;
serde_json::to_string(&result)?
}
ResponseType::Text => response.text()?,
ResponseType::Binary => serde_json::to_string(&response.bytes()?)?,
ResponseType::Json => response.json::<Value>()?,
ResponseType::Text => Value::String(response.text()?),
ResponseType::Binary => Value::String(serde_json::to_string(&response.bytes()?)?),
};
Ok(response_data)
} else {

View File

@ -16,6 +16,7 @@ use std::path::MAIN_SEPARATOR;
/// .show();
/// ```
#[allow(dead_code)]
#[derive(Default)]
pub struct Notification {
/// The notification body.
body: Option<String>,
@ -28,11 +29,7 @@ pub struct Notification {
impl Notification {
/// Initializes a instance of a Notification.
pub fn new() -> Self {
Self {
body: None,
title: None,
icon: None,
}
Default::default()
}
/// Sets the notification body.

View File

@ -1,12 +1,15 @@
/// Formats a function to be evaluated as callback.
/// If the arg is a string literal, it needs the proper quotes.
use serde::Serialize;
use serde_json::Value as JsonValue;
use std::fmt::Display;
/// Formats a function name and argument to be evaluated as callback.
///
/// # Examples
/// ```
/// use tauri_api::rpc::format_callback;
/// // callback with a string argument
/// // returns `window["callback-function-name"]("the string response")`
/// format_callback("callback-function-name".to_string(), r#""the string response""#.to_string());
/// let cb = format_callback("callback-function-name", "the string response");
/// assert_eq!(cb, r#"window["callback-function-name"]("the string response")"#);
/// ```
///
/// ```
@ -17,39 +20,50 @@
/// struct MyResponse {
/// value: String
/// }
/// // this returns `window["callback-function-name"]({value: "some value"})`
/// format_callback("callback-function-name".to_string(), serde_json::to_string(&MyResponse {
/// let cb = format_callback("callback-function-name", serde_json::to_value(&MyResponse {
/// value: "some value".to_string()
/// }).expect("failed to serialize type"));
/// }).expect("failed to serialize"));
/// assert_eq!(cb, r#"window["callback-function-name"]({"value":"some value"})"#);
/// ```
pub fn format_callback(function_name: String, arg: String) -> String {
let formatted_string = &format!("window[\"{}\"]({})", function_name, arg);
formatted_string.to_string()
pub fn format_callback<T: Into<JsonValue>, S: AsRef<str> + Display>(
function_name: S,
arg: T,
) -> String {
format!(r#"window["{}"]({})"#, function_name, arg.into().to_string())
}
/// Formats a Result type to its callback version.
/// Formats a Result type to its Promise response.
/// Useful for Promises handling.
/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
///
/// If the Result is Ok, `format_callback` will be called directly.
/// If the result is an Err, we assume the error message is a string, and quote it.
/// * `result` the Result to check
/// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise.
/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
///
/// Note that the callback strings are automatically generated by the `promisified` helper.
///
/// # Examples
/// ```
/// use tauri_api::rpc::format_callback_result;
/// // returns `window["success_cb"](5)`
/// format_callback_result(Ok("5".to_string()), "success_cb".to_string(), "error_cb".to_string());
/// // returns `window["error_cb"]("error message here")`
/// format_callback_result(Err("error message here".to_string()), "success_cb".to_string(), "error_cb".to_string());
/// let res: Result<u8, &str> = Ok(5);
/// let cb = format_callback_result(res, "success_cb".to_string(), "error_cb".to_string()).expect("failed to format");
/// assert_eq!(cb, r#"window["success_cb"](5)"#);
///
/// let res: Result<&str, &str> = Err("error message here");
/// let cb = format_callback_result(res, "success_cb".to_string(), "error_cb".to_string()).expect("failed to format");
/// assert_eq!(cb, r#"window["error_cb"]("error message here")"#);
/// ```
pub fn format_callback_result(
result: Result<String, String>,
callback: String,
pub fn format_callback_result<T: Serialize, E: Serialize>(
result: Result<T, E>,
success_callback: String,
error_callback: String,
) -> String {
match result {
Ok(res) => format_callback(callback, res),
Err(err) => format_callback(error_callback, format!("\"{}\"", err)),
}
) -> crate::Result<String> {
let rpc = match result {
Ok(res) => format_callback(success_callback, serde_json::to_value(res)?),
Err(err) => format_callback(error_callback, serde_json::to_value(err)?),
};
Ok(rpc)
}
#[cfg(test)]
@ -62,16 +76,9 @@ mod test {
fn qc_formating(f: String, a: String) -> bool {
// can not accept empty strings
if f != "" && a != "" {
// get length of function and argument
let alen = &a.len();
let flen = &f.len();
// call format callback
let fc = format_callback(f, a);
// get length of the resulting string
let fclen = fc.len();
// if formatted string equals the length of the argument and the function plus 12 then its correct.
fclen == alen + flen + 12
let fc = format_callback(f.clone(), a.clone());
fc == format!(r#"window["{}"]({})"#, f, serde_json::Value::String(a))
} else {
true
}
@ -80,33 +87,18 @@ mod test {
// check arbitrary strings in format_callback_result
#[quickcheck]
fn qc_format_res(result: Result<String, String>, c: String, ec: String) -> bool {
// match on result to decide how to call the function.
match result {
// if ok, get length of result and callback strings.
Ok(r) => {
let rlen = r.len();
let clen = c.len();
let resp = format_callback_result(result.clone(), c.clone(), ec.clone())
.expect("failed to format callback result");
let (function, value) = match result {
Ok(v) => (c, v),
Err(e) => (ec, e),
};
// take the ok string from result and pass it into format_callback_result as an ok.
let resp = format_callback_result(Ok(r), c, ec);
// get response string length
let reslen = resp.len();
// if response string length equals result and callback length plus 12 characters then it is correct.
reslen == rlen + clen + 12
}
// If Err, get length of Err and Error callback
Err(err) => {
let eclen = ec.len();
let errlen = err.len();
// pass err as Err into format_callback_result with callback and error callback
let resp = format_callback_result(Err(err), c, ec);
// get response string length
let reslen = resp.len();
// if length of response string equals the error length and the error callback length plus 14 characters then its is correct.
reslen == eclen + errlen + 14
}
}
resp
== format!(
r#"window["{}"]({})"#,
function,
serde_json::Value::String(value),
)
}
}

View File

@ -179,7 +179,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
callback,
error,
} => {
salt::validate(webview, salt, callback, error);
salt::validate(webview, salt, callback, error)?;
}
Listen {
event,
@ -206,7 +206,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
error,
} => {
#[cfg(open_dialog)]
dialog::open(webview, options, callback, error);
dialog::open(webview, options, callback, error)?;
#[cfg(not(open_dialog))]
whitelist_error(webview, error, "title");
}
@ -216,7 +216,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
error,
} => {
#[cfg(save_dialog)]
dialog::save(webview, options, callback, error);
dialog::save(webview, options, callback, error)?;
#[cfg(not(save_dialog))]
throw_whitelist_error(webview, "saveDialog");
}
@ -244,7 +244,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
crate::execute_promise(
webview,
move || match crate::cli::get_matches() {
Some(matches) => Ok(serde_json::to_string(matches)?),
Some(matches) => Ok(matches),
None => Err(anyhow::anyhow!(r#""failed to get matches""#)),
},
callback,
@ -271,7 +271,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
}
RequestNotificationPermission { callback, error } => {
#[cfg(notification)]
notification::request_permission(webview, callback, error);
notification::request_permission(webview, callback, error)?;
#[cfg(not(notification))]
whitelist_error(webview, error, "notification");
}

View File

@ -70,7 +70,7 @@ pub fn load<T: 'static>(
}
})
.map_err(|err| err.into())
.map(|_| r#""Asset loaded successfully""#.to_string())
.map(|_| "Asset loaded successfully".to_string())
}
},
callback,

View File

@ -1,13 +1,14 @@
use super::cmd::{OpenDialogOptions, SaveDialogOptions};
use crate::api::dialog::{pick_folder, save_file, select, select_multiple, Response};
use serde_json::Value as JsonValue;
use web_view::WebView;
/// maps a dialog response to a JS value to eval
fn map_response(response: Response) -> String {
fn map_response(response: Response) -> JsonValue {
match response {
Response::Okay(path) => format!(r#""{}""#, path).replace("\\", "\\\\"),
Response::OkayMultiple(paths) => format!("{:?}", paths),
Response::Cancel => panic!("unexpected response type"),
Response::Okay(path) => path.into(),
Response::OkayMultiple(paths) => paths.into(),
Response::Cancel => JsonValue::Null,
}
}
@ -18,7 +19,7 @@ pub fn open<T: 'static>(
options: OpenDialogOptions,
callback: String,
error: String,
) {
) -> crate::Result<()> {
crate::execute_promise_sync(
webview,
move || {
@ -33,7 +34,8 @@ pub fn open<T: 'static>(
},
callback,
error,
);
)?;
Ok(())
}
/// Shows a save dialog.
@ -43,11 +45,12 @@ pub fn save<T: 'static>(
options: SaveDialogOptions,
callback: String,
error: String,
) {
) -> crate::Result<()> {
crate::execute_promise_sync(
webview,
move || save_file(options.filter, options.default_path).map(map_response),
callback,
error,
);
)?;
Ok(())
}

View File

@ -28,7 +28,6 @@ pub fn read_dir<T: 'static>(
(false, None)
};
dir::read_dir(resolve_path(path, dir)?, recursive)
.and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
},
callback,
error,
@ -55,9 +54,7 @@ pub fn copy_file<T: 'static>(
),
None => (source, destination),
};
fs::copy(src, dest)
.map_err(|e| e.into())
.map(|_| "".to_string())
fs::copy(src, dest).map_err(|e| e.into())
},
callback,
error,
@ -88,7 +85,7 @@ pub fn create_dir<T: 'static>(
fs::create_dir(resolved_path)
};
response.map_err(|e| e.into()).map(|_| "".to_string())
response.map_err(|e| e.into())
},
callback,
error,
@ -119,7 +116,7 @@ pub fn remove_dir<T: 'static>(
fs::remove_dir(resolved_path)
};
response.map_err(|e| e.into()).map(|_| "".to_string())
response.map_err(|e| e.into())
},
callback,
error,
@ -139,9 +136,7 @@ pub fn remove_file<T: 'static>(
webview,
move || {
let resolved_path = resolve_path(path, options.and_then(|o| o.dir))?;
fs::remove_file(resolved_path)
.map_err(|e| e.into())
.map(|_| "".to_string())
fs::remove_file(resolved_path).map_err(|e| e.into())
},
callback,
error,
@ -168,9 +163,7 @@ pub fn rename_file<T: 'static>(
),
None => (old_path, new_path),
};
fs::rename(old, new)
.map_err(|e| e.into())
.map(|_| "".to_string())
fs::rename(old, new).map_err(|e| e.into())
},
callback,
error,
@ -192,11 +185,7 @@ pub fn write_file<T: 'static>(
move || {
File::create(resolve_path(file, options.and_then(|o| o.dir))?)
.map_err(|e| e.into())
.and_then(|mut f| {
f.write_all(contents.as_bytes())
.map_err(|err| err.into())
.map(|_| "".to_string())
})
.and_then(|mut f| f.write_all(contents.as_bytes()).map_err(|err| err.into()))
},
callback,
error,
@ -221,11 +210,7 @@ pub fn write_binary_file<T: 'static>(
.and_then(|c| {
File::create(resolve_path(file, options.and_then(|o| o.dir))?)
.map_err(|e| e.into())
.and_then(|mut f| {
f.write_all(&c)
.map_err(|err| err.into())
.map(|_| "".to_string())
})
.and_then(|mut f| f.write_all(&c).map_err(|err| err.into()))
})
},
callback,
@ -244,10 +229,7 @@ pub fn read_text_file<T: 'static>(
) {
crate::execute_promise(
webview,
move || {
file::read_string(resolve_path(path, options.and_then(|o| o.dir))?)
.and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
},
move || file::read_string(resolve_path(path, options.and_then(|o| o.dir))?),
callback,
error,
);
@ -264,10 +246,7 @@ pub fn read_binary_file<T: 'static>(
) {
crate::execute_promise(
webview,
move || {
file::read_binary(resolve_path(path, options.and_then(|o| o.dir))?)
.and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
},
move || file::read_binary(resolve_path(path, options.and_then(|o| o.dir))?),
callback,
error,
);

View File

@ -1,4 +1,4 @@
use tauri_api::http::{make_request as request, HttpRequestOptions, ResponseType};
use tauri_api::http::{make_request as request, HttpRequestOptions};
use web_view::WebView;
/// Makes an HTTP request and resolves the response to the webview
@ -8,21 +8,5 @@ pub fn make_request<T: 'static>(
callback: String,
error: String,
) {
crate::execute_promise(
webview,
move || {
let response_type = options.response_type.clone();
request(options).map(
|response| match response_type.unwrap_or(ResponseType::Json) {
ResponseType::Text => format!(
r#""{}""#,
response.replace(r#"""#, r#"\""#).replace(r#"\\""#, r#"\""#)
),
_ => response,
},
)
},
callback,
error,
);
crate::execute_promise(webview, move || request(options), callback, error);
}

View File

@ -1,4 +1,5 @@
use super::cmd::NotificationOptions;
use serde_json::Value as JsonValue;
use web_view::WebView;
pub fn send<T: 'static>(
@ -18,9 +19,7 @@ pub fn send<T: 'static>(
if let Some(icon) = options.icon {
notification = notification.icon(icon);
}
notification
.show()
.map_err(|e| anyhow::anyhow!(r#""{}""#, e.to_string()))?;
notification.show()?;
Ok("".to_string())
},
callback,
@ -38,9 +37,9 @@ pub fn is_permission_granted<T: 'static>(
move || {
let settings = crate::settings::read_settings()?;
if let Some(allow_notification) = settings.allow_notification {
Ok(allow_notification.to_string())
Ok(JsonValue::String(allow_notification.to_string()))
} else {
Ok("null".to_string())
Ok(JsonValue::Null)
}
},
callback,
@ -52,7 +51,7 @@ pub fn request_permission<T: 'static>(
webview: &mut WebView<'_, T>,
callback: String,
error: String,
) {
) -> crate::Result<()> {
crate::execute_promise_sync(
webview,
move || {
@ -82,5 +81,6 @@ pub fn request_permission<T: 'static>(
},
callback,
error,
);
)?;
Ok(())
}

View File

@ -6,14 +6,13 @@ pub fn validate<T: 'static>(
salt: String,
callback: String,
error: String,
) {
) -> crate::Result<()> {
let response = if crate::salt::is_valid(salt) {
Ok("'VALID'".to_string())
} else {
Err("'INVALID SALT'".to_string())
};
let callback_string = crate::api::rpc::format_callback_result(response, callback, error);
webview
.eval(callback_string.as_str())
.expect("Failed to eval JS from validate()");
let callback_string = crate::api::rpc::format_callback_result(response, callback, error)?;
webview.eval(callback_string.as_str())?;
Ok(())
}

View File

@ -4,6 +4,8 @@ use std::sync::{Arc, Mutex};
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use serde::Serialize;
use serde_json::Value as JsonValue;
use web_view::Handle;
/// An event handler.
@ -55,13 +57,17 @@ pub fn listen<F: FnMut(Option<String>) + Send + 'static>(id: String, handler: F)
}
/// Emits an event to JS.
pub fn emit<T: 'static>(webview_handle: &Handle<T>, event: String, payload: Option<String>) {
pub fn emit<T: 'static, S: Serialize>(
webview_handle: &Handle<T>,
event: String,
payload: Option<S>,
) -> crate::Result<()> {
let salt = crate::salt::generate();
let js_payload = if let Some(payload_str) = payload {
payload_str
let js_payload = if let Some(payload_value) = payload {
serde_json::to_value(payload_value)?
} else {
"void 0".to_string()
JsonValue::Null
};
webview_handle
@ -75,6 +81,8 @@ pub fn emit<T: 'static>(webview_handle: &Handle<T>, event: String, payload: Opti
))
})
.expect("Failed to dispatch JS from emit");
Ok(())
}
/// Triggers the given event with its payload.

View File

@ -32,17 +32,19 @@ mod endpoints;
/// The salt helpers.
mod salt;
use std::process::Stdio;
/// Alias for a Result with error type anyhow::Error.
pub use anyhow::Result;
use threadpool::ThreadPool;
pub use web_view::Handle;
use web_view::WebView;
pub use app::*;
pub use tauri_api as api;
pub use web_view::Handle;
use std::process::Stdio;
use api::rpc::{format_callback, format_callback_result};
use serde::Serialize;
use threadpool::ThreadPool;
use web_view::WebView;
thread_local!(static POOL: ThreadPool = ThreadPool::new(4));
/// Executes the operation in the thread pool.
@ -56,33 +58,49 @@ pub fn spawn<F: FnOnce() -> () + Send + 'static>(task: F) {
/// Synchronously executes the given task
/// and evaluates its Result to the JS promise described by the `callback` and `error` function names.
pub fn execute_promise_sync<T: 'static, F: FnOnce() -> crate::Result<String> + Send + 'static>(
pub fn execute_promise_sync<
T: 'static,
R: Serialize,
F: FnOnce() -> crate::Result<R> + Send + 'static,
>(
webview: &mut WebView<'_, T>,
task: F,
callback: String,
error: String,
) {
) -> crate::Result<()> {
let handle = webview.handle();
let callback_string =
api::rpc::format_callback_result(task().map_err(|err| err.to_string()), callback, error);
handle
.dispatch(move |_webview| _webview.eval(callback_string.as_str()))
.expect("Failed to dispatch promise callback");
format_callback_result(task().map_err(|err| err.to_string()), callback, error)?;
handle.dispatch(move |_webview| _webview.eval(callback_string.as_str()))?;
Ok(())
}
/// Asynchronously executes the given task
/// and evaluates its Result to the JS promise described by the `callback` and `error` function names.
pub fn execute_promise<T: 'static, F: FnOnce() -> crate::Result<String> + Send + 'static>(
/// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names.
///
/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
pub fn execute_promise<
T: 'static,
R: Serialize,
F: FnOnce() -> crate::Result<R> + Send + 'static,
>(
webview: &mut WebView<'_, T>,
task: F,
callback: String,
error: String,
success_callback: String,
error_callback: String,
) {
let handle = webview.handle();
POOL.with(|thread| {
thread.execute(move || {
let callback_string =
api::rpc::format_callback_result(task().map_err(|err| err.to_string()), callback, error);
let callback_string = match format_callback_result(
task().map_err(|err| err.to_string()),
success_callback,
error_callback.clone(),
) {
Ok(callback_string) => callback_string,
Err(e) => format_callback(error_callback, e.to_string()),
};
handle
.dispatch(move |_webview| _webview.eval(callback_string.as_str()))
.expect("Failed to dispatch promise callback")
@ -100,11 +118,7 @@ pub fn call<T: 'static>(
) {
execute_promise(
webview,
|| {
api::command::get_output(command, args, Stdio::piped())
.map_err(|err| err)
.map(|output| format!(r#""{}""#, output))
},
|| api::command::get_output(command, args, Stdio::piped()),
callback,
error,
);