Skip to content

Commit

Permalink
Create SsrError enum (#45)
Browse files Browse the repository at this point in the history
* feat: create SsrError enum

* doc: improve documentation

* feat: update version to v0.6.0

* refactor: better exports
  • Loading branch information
Valerioageno authored Sep 7, 2024
1 parent 7f023cb commit eb6014c
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 25 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ssr_rs"
version = "0.5.8"
version = "0.6.0"
authors = ["Valerio <[email protected]>"]
edition = "2021"
description = "Server side rendering with the v8 engine for parse and evaluate the javascript code"
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,4 @@
//!```
mod ssr;

pub use ssr::Ssr;
pub use ssr::{Ssr, SsrError};
82 changes: 59 additions & 23 deletions src/ssr.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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.
///
Expand All @@ -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<Self, &'static str> {
pub fn from(source: String, entry_point: &str) -> Result<Self, SsrError> {
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))) };
Expand All @@ -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",
))
}
};

Expand All @@ -93,22 +109,34 @@ where
.iter()
.enumerate()
.map(
|(i, &p)| -> Result<(String, v8::Local<v8::Function>), &'static str> {
|(i, &p)| -> Result<(String, v8::Local<v8::Function>), 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())))
Expand All @@ -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<String, &'static str> {
pub fn render_to_string(&mut self, params: Option<&str>) -> Result<String, SsrError> {
let scope = unsafe { &mut *self.scope };

let params: v8::Local<v8::Value> = match v8::String::new(scope, params.unwrap_or("")) {
Expand All @@ -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::<v8::Promise>::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 {
Expand All @@ -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));
Expand Down Expand Up @@ -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")
);
}

Expand All @@ -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")
);
}

Expand All @@ -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");

Expand Down

0 comments on commit eb6014c

Please sign in to comment.