diff --git a/Cargo.toml b/Cargo.toml index f15541e..efe16a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ssr_rs" -version = "0.5.8" +version = "0.6.0" authors = ["Valerio "] edition = "2021" description = "Server side rendering with the v8 engine for parse and evaluate the javascript code" diff --git a/src/lib.rs b/src/lib.rs index 9fb9a1a..b02c3f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,4 +145,4 @@ //!``` mod ssr; -pub use ssr::Ssr; +pub use ssr::{Ssr, SsrError}; diff --git a/src/ssr.rs b/src/ssr.rs index 641062f..11a42dd 100644 --- a/src/ssr.rs +++ b/src/ssr.rs @@ -1,6 +1,24 @@ // TODO: replace hashmap with more performant https://nnethercote.github.io/perf-book/hashing.html use std::collections::HashMap; +use std::fmt; + +/// This enum holds all the possible Ssr error states. +#[derive(Debug, PartialEq, Eq)] +pub enum SsrError { + InvalidJs(&'static str), + FailedToParseJs(&'static str), + FailedJsExecution(&'static str), +} + +impl fmt::Display for SsrError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} +/// This struct holds all the necessary v8 utilities to +/// execute Javascript code. +/// It cannot be shared across threads. #[derive(Debug)] pub struct Ssr<'s, 'i> { isolate: *mut v8::OwnedIsolate, @@ -26,22 +44,20 @@ where { /// Initialize a V8 js engine instance. It's mandatory to call it before /// any call to V8. The Ssr module needs this function call before any other - /// operation. + /// operation. It cannot be called more than once per process. pub fn create_platform() { let platform = v8::new_default_platform(0, false).make_shared(); v8::V8::initialize_platform(platform); v8::V8::initialize(); } - /// It creates a new SSR instance. + /// It creates a new SSR instance (multiple instances are allowed). /// /// This function is expensive and it should be called as less as possible. /// /// Even though V8 allows multiple threads the Ssr struct created with this call can be accessed by just /// the thread that created it. /// - /// Multiple instances are allowed. - /// /// Entry point is the JS element that the bundler exposes. It has to be an empty string in /// case the bundle is exported as IIFE. /// @@ -50,7 +66,7 @@ where /// /// See the examples folder for more about using multiple parallel instances for multi-threaded /// execution. - pub fn from(source: String, entry_point: &str) -> Result { + pub fn from(source: String, entry_point: &str) -> Result { let isolate = Box::into_raw(Box::new(v8::Isolate::new(v8::CreateParams::default()))); let handle_scope = unsafe { Box::into_raw(Box::new(v8::HandleScope::new(&mut *isolate))) }; @@ -64,25 +80,25 @@ where let code = match v8::String::new(scope, &format!("{source};{entry_point}")) { Some(val) => val, - None => return Err("Invalid JS: Strings are needed"), + None => return Err(SsrError::InvalidJs("Strings are needed")), }; let script = match v8::Script::compile(scope, code, None) { Some(val) => val, - None => return Err("Invalid JS: There aren't runnable scripts"), + None => return Err(SsrError::InvalidJs("There aren't runnable scripts")), }; let exports = match script.run(scope) { Some(val) => val, - None => return Err("Invalid JS: Execute your script with d8 to debug"), + None => return Err(SsrError::InvalidJs("Execute your script with d8 to debug")), }; let object = match exports.to_object(scope) { Some(val) => val, None => { - return Err( - "Invalid JS: The script does not return any object after being executed", - ) + return Err(SsrError::InvalidJs( + "The script does not return any object after being executed", + )) } }; @@ -93,22 +109,34 @@ where .iter() .enumerate() .map( - |(i, &p)| -> Result<(String, v8::Local), &'static str> { + |(i, &p)| -> Result<(String, v8::Local), SsrError> { let name = match p.get_index(scope, i as u32) { Some(val) => val, - None => return Err("Failed to get function name"), + None => { + return Err(SsrError::FailedToParseJs( + "Failed to get function name", + )) + } }; let mut scope = v8::EscapableHandleScope::new(scope); let func = match object.get(&mut scope, name) { Some(val) => val, - None => return Err("Failed to get function from obj"), + None => { + return Err(SsrError::FailedToParseJs( + "Failed to get function from obj", + )) + } }; let fn_name = match name.to_string(&mut scope) { Some(val) => val.to_rust_string_lossy(&mut scope), - None => return Err("Failed to find function name"), + None => { + return Err(SsrError::FailedToParseJs( + "Failed to find function name", + )) + } }; Ok((fn_name, scope.escape(func.cast()))) @@ -131,7 +159,7 @@ where } /// Execute the Javascript functions and return the result as string. - pub fn render_to_string(&mut self, params: Option<&str>) -> Result { + pub fn render_to_string(&mut self, params: Option<&str>) -> Result { let scope = unsafe { &mut *self.scope }; let params: v8::Local = match v8::String::new(scope, params.unwrap_or("")) { @@ -147,13 +175,17 @@ where for key in self.fn_map.keys() { let mut result = match self.fn_map[key].call(scope, undef, &[params]) { Some(val) => val, - None => return Err("Failed to call function"), + None => return Err(SsrError::FailedJsExecution("Failed to call function")), }; if result.is_promise() { let promise = match v8::Local::::try_from(result) { Ok(val) => val, - Err(_) => return Err("Failed to cast main function to promise"), + Err(_) => { + return Err(SsrError::FailedJsExecution( + "Failed to cast main function to promise", + )) + } }; while promise.state() == v8::PromiseState::Pending { @@ -165,7 +197,11 @@ where let result = match result.to_string(scope) { Some(val) => val, - None => return Err("Failed to parse the result to string"), + None => { + return Err(SsrError::FailedJsExecution( + "Failed to parse the result to string", + )) + } }; rendered = format!("{}{}", rendered, result.to_rust_string_lossy(scope)); @@ -197,7 +233,7 @@ mod tests { assert_eq!( res.unwrap_err(), - "Invalid JS: Execute your script with d8 to debug" + SsrError::InvalidJs("Execute your script with d8 to debug") ); } @@ -209,7 +245,7 @@ mod tests { let res = Ssr::from(source.to_owned(), "SSR"); assert_eq!( res.unwrap_err(), - "Invalid JS: Execute your script with d8 to debug" + SsrError::InvalidJs("Execute your script with d8 to debug") ); } @@ -233,14 +269,14 @@ mod tests { let mut js = Ssr::from(accept_params_source, "SSR").unwrap(); println!("Before render_to_string"); - let result = js.render_to_string(Some(&props)).unwrap(); + let result = js.render_to_string(Some(props)).unwrap(); assert_eq!(result, "These are our parameters: {\"Hello world\"}"); let no_params_source = r##"var SSR = {x: () => "I don't accept params"};"##.to_string(); let mut js2 = Ssr::from(no_params_source, "SSR").unwrap(); - let result2 = js2.render_to_string(Some(&props)).unwrap(); + let result2 = js2.render_to_string(Some(props)).unwrap(); assert_eq!(result2, "I don't accept params");