Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create SsrError enum #45

Merged
merged 4 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading