Skip to content

Commit

Permalink
Initial version of a JS <-> Rust conversion trait.
Browse files Browse the repository at this point in the history
  • Loading branch information
Razican committed Sep 24, 2022
1 parent a3062d1 commit 4c09237
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 5 deletions.
42 changes: 42 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"boa_unicode",
"boa_wasm",
"boa_examples",
"boa_derive",
]

[workspace.metadata.workspaces]
Expand Down
23 changes: 23 additions & 0 deletions boa_derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "boa_derive"
version = "0.16.0"
edition = "2021"
rust-version = "1.60"
authors = ["boa-dev"]
description = "Ths crate adds derive macros for the main boa_engine crate."
repository = "https://github.com/boa-dev/boa"
license = "Unlicense/MIT"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
proc-macro = true

[dependencies]
syn = "1.0.99"
quote = "1.0.21"
proc-macro2 = "1.0.43"

[dev-dependencies]
trybuild = "1.0.64"
boa_engine = { path = "../boa_engine" }
71 changes: 71 additions & 0 deletions boa_derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, FieldsNamed};

#[proc_macro_derive(TryFromJs)]
pub fn derive_try_from_js(input: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree
let input = parse_macro_input!(input as DeriveInput);

let data = if let Data::Struct(data) = input.data {
data
} else {
panic!("you can only derive TryFromJs for structs");
};

let fields = if let Fields::Named(fields) = data.fields {
fields
} else {
panic!("you can only derive TryFromJs for named-field structs")
};

let conv = generate_conversion(fields);

let type_name = input.ident;

// Build the output, possibly using quasi-quotation
let expanded = quote! {
impl boa_engine::value::conversions::try_from_js::TryFromJs for #type_name {
fn try_from_js(value: &boa_engine::JsValue, context: &mut boa_engine::Context) -> boa_engine::JsResult<Self> {
match value {
boa_engine::JsValue::Object(o) => {#conv},
_ => context.throw_type_error("cannot convert value to a #type_name"),
}
}
}
};

// Hand the output tokens back to the compiler
expanded.into()
}

fn generate_conversion(fields: FieldsNamed) -> proc_macro2::TokenStream {
let mut field_list = Vec::with_capacity(fields.named.len());

let fields = fields.named.into_iter().map(|field| {
let name = field
.ident
.expect("you can only derive TryFromJs for named-field structs");
let name_str = format!("{name}");
field_list.push(name.clone());

let error_str = format!("cannot get property {name_str} of value");

quote! {
let #name = props.get(&#name_str.into()).ok_or_else(|| {
context.construct_type_error(#error_str)
})?.value().ok_or_else(|| {
context.construct_type_error(#error_str)
})?.clone().try_js_into(context)?;
}
});

quote! {
let o = o.borrow();
let props = o.properties();
#(#fields)*
Ok(Self {
#(#field_list),*
})
}
}
8 changes: 8 additions & 0 deletions boa_derive/tests/derive/simple_struct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use boa_derive::TryFromJs;

#[derive(TryFromJs)]
struct TestStruct {
inner: bool,
}

fn main() {}
5 changes: 5 additions & 0 deletions boa_derive/tests/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[test]
fn test() {
let t = trybuild::TestCases::new();
t.pass("tests/derive/simple_struct.rs");
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use super::{Display, JsBigInt, JsObject, JsString, JsSymbol, JsValue, Profiler};

mod serde_json;
pub mod try_from_js;

impl From<&Self> for JsValue {
#[inline]
fn from(value: &Self) -> Self {
Expand Down
File renamed without changes.
186 changes: 186 additions & 0 deletions boa_engine/src/value/conversions/try_from_js.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use crate::{Context, JsBigInt, JsResult, JsValue};
use num_bigint::BigInt;

/// This trait adds a fallible and efficient conversions from a [`JsValue`] to Rust types.
pub trait TryFromJs: Sized {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self>;
}

impl JsValue {
/// This function is the inverse of [`TryFromJs`]. It tries to convert a [`JsValue`] to a given
/// Rust type.
pub fn try_js_into<T>(&self, context: &mut Context) -> JsResult<T>
where
T: TryFromJs,
{
T::try_from_js(self, context)
}
}

impl TryFromJs for bool {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Boolean(b) => Ok(*b),
_ => context.throw_type_error("cannot convert value to a boolean"),
}
}
}

impl TryFromJs for String {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::String(s) => Ok(s.as_str().to_owned()),
_ => context.throw_type_error("cannot convert value to a String"),
}
}
}

impl<T> TryFromJs for Option<T>
where
T: TryFromJs,
{
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Null | JsValue::Undefined => Ok(None),
value => Ok(Some(T::try_from_js(value, context)?)),
}
}
}

impl TryFromJs for JsBigInt {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::BigInt(b) => Ok(b.clone()),
_ => context.throw_type_error("cannot convert value to a BigInt"),
}
}
}

impl TryFromJs for BigInt {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::BigInt(b) => Ok(b.as_inner().clone()),
_ => context.throw_type_error("cannot convert value to a BigInt"),
}
}
}

impl TryFromJs for JsValue {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
Ok(value.clone())
}
}

impl TryFromJs for f64 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => Ok((*i).into()),
JsValue::Rational(r) => Ok(*r),
_ => context.throw_type_error("cannot convert value to a f64"),
}
}
}

impl TryFromJs for i8 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => (*i).try_into().map_err(|e| {
context.construct_type_error(format!("cannot convert value to a i8: {e}"))
}),
_ => context.throw_type_error("cannot convert value to a i8"),
}
}
}

impl TryFromJs for u8 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => (*i).try_into().map_err(|e| {
context.construct_type_error(format!("cannot convert value to a u8: {e}"))
}),
_ => context.throw_type_error("cannot convert value to a u8"),
}
}
}

impl TryFromJs for i16 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => (*i).try_into().map_err(|e| {
context.construct_type_error(format!("cannot convert value to a i16: {e}"))
}),
_ => context.throw_type_error("cannot convert value to a i16"),
}
}
}

impl TryFromJs for u16 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => (*i).try_into().map_err(|e| {
context.construct_type_error(format!("cannot convert value to a iu16: {e}"))
}),
_ => context.throw_type_error("cannot convert value to a u16"),
}
}
}

impl TryFromJs for i32 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => Ok(*i),
_ => context.throw_type_error("cannot convert value to a i32"),
}
}
}

impl TryFromJs for u32 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => (*i).try_into().map_err(|e| {
context.construct_type_error(format!("cannot convert value to a u32: {e}"))
}),
_ => context.throw_type_error("cannot convert value to a u32"),
}
}
}

impl TryFromJs for i64 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => Ok((*i).into()),
_ => context.throw_type_error("cannot convert value to a i64"),
}
}
}

impl TryFromJs for u64 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => (*i).try_into().map_err(|e| {
context.construct_type_error(format!("cannot convert value to a u64: {e}"))
}),
_ => context.throw_type_error("cannot convert value to a u64"),
}
}
}

impl TryFromJs for i128 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => Ok((*i).into()),
_ => context.throw_type_error("cannot convert value to a i128"),
}
}
}

impl TryFromJs for u128 {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Integer(i) => (*i).try_into().map_err(|e| {
context.construct_type_error(format!("cannot convert value to a u128: {e}"))
}),
_ => context.throw_type_error("cannot convert value to a u128"),
}
}
}
Loading

0 comments on commit 4c09237

Please sign in to comment.