Browse Source

Feature `JsArray` (#1746)

This PR introduces a new API for JavaScript builtin objects in Rust (such as `Array`, `Map`, `Proxy`, etc). Rather than just expose the raw builtin functions as discussed here #1692 (though having raw API exposed may be nice as well), In this PR we introduce a very light wrapper around the raw API, for a more pleasant user experience. The wrapper implements functions that are specific to the wrapper type (for `Array` this would be methods like `pop`, `push`, etc) as well as implementing `Deref<Target = JsObject>` so we can call `JsObject` functions without converting to `JsObject` and `Into<JsValue>` for easy to `JsValue` conversions.

Please check `jsarray.rs` in the `examples`
pull/1855/head
Halid Odat 3 years ago
parent
commit
9f6aa1972c
  1. 107
      boa/examples/jsarray.rs
  2. 364
      boa/src/object/jsarray.rs
  3. 8
      boa/src/object/mod.rs
  4. 2
      boa/src/value/conversions.rs

107
boa/examples/jsarray.rs

@ -0,0 +1,107 @@
use boa::{
object::{FunctionBuilder, JsArray},
Context, JsValue,
};
fn main() -> Result<(), JsValue> {
// We create a new `Context` to create a new Javascript executor.
let context = &mut Context::default();
// Create an empty array.
let array = JsArray::new(context);
assert!(array.is_empty(context)?);
array.push("Hello, world", context)?; // [ "Hello, world" ]
array.push(true, context)?; // [ "Hello, world", true ]
assert!(!array.is_empty(context)?);
assert_eq!(array.pop(context)?, JsValue::new(true)); // [ "Hello, world" ]
assert_eq!(array.pop(context)?, JsValue::new("Hello, world")); // [ ]
assert_eq!(array.pop(context)?, JsValue::undefined()); // [ ]
array.push(1, context)?; // [ 1 ]
assert_eq!(array.pop(context)?, JsValue::new(1)); // [ ]
assert_eq!(array.pop(context)?, JsValue::undefined()); // [ ]
array.push_items(
&[
JsValue::new(10),
JsValue::new(11),
JsValue::new(12),
JsValue::new(13),
JsValue::new(14),
],
context,
)?; // [ 10, 11, 12, 13, 14 ]
array.reverse(context)?; // [ 14, 13, 12, 11, 10 ]
assert_eq!(array.index_of(12, None, context)?, Some(2));
// We can also use JsObject method `.get()` through the Deref trait.
let element = array.get(2, context)?; // array[ 0 ]
assert_eq!(element, JsValue::new(12));
// Or we can use the `.at(index)` method.
assert_eq!(array.at(0, context)?, JsValue::new(14)); // first element
assert_eq!(array.at(-1, context)?, JsValue::new(10)); // last element
// Join the array with an optional separator (default ",").
let joined_array = array.join(None, context)?;
assert_eq!(joined_array, "14,13,12,11,10");
array.fill(false, Some(1), Some(4), context)?;
let joined_array = array.join(Some("::".into()), context)?;
assert_eq!(joined_array, "14::false::false::false::10");
let filter_callback = FunctionBuilder::native(context, |_this, args, _context| {
Ok(args.get(0).cloned().unwrap_or_default().is_number().into())
})
.build();
let map_callback = FunctionBuilder::native(context, |_this, args, context| {
args.get(0)
.cloned()
.unwrap_or_default()
.pow(&JsValue::new(2), context)
})
.build();
let mut data = Vec::new();
for i in 1..=5 {
data.push(JsValue::new(i));
}
let another_array = JsArray::from_iter(data, context); // [ 1, 2, 3, 4, 5]
let chained_array = array // [ 14, false, false, false, 10 ]
.filter(filter_callback, None, context)? // [ 14, 10 ]
.map(map_callback, None, context)? // [ 196, 100 ]
.sort(None, context)? // [ 100, 196 ]
.concat(&[another_array.into()], context)? // [ 100, 196, 1, 2, 3, 4, 5 ]
.slice(Some(1), Some(5), context)?; // [ 196, 1, 2, 3 ]
assert_eq!(chained_array.join(None, context)?, "196,1,2,3");
let reduce_callback = FunctionBuilder::native(context, |_this, args, context| {
let accumulator = args.get(0).cloned().unwrap_or_default();
let value = args.get(1).cloned().unwrap_or_default();
accumulator.add(&value, context)
})
.build();
assert_eq!(
chained_array.reduce(reduce_callback, Some(JsValue::new(0)), context)?,
JsValue::new(202)
);
context
.global_object()
.clone()
.set("myArray", array, true, context)?;
Ok(())
}

364
boa/src/object/jsarray.rs

@ -0,0 +1,364 @@
use std::ops::Deref;
use crate::{
builtins::Array,
gc::{Finalize, Trace},
object::{JsObject, JsObjectType},
Context, JsResult, JsString, JsValue,
};
/// JavaScript `Array` rust object.
#[derive(Debug, Clone, Trace, Finalize)]
pub struct JsArray {
inner: JsObject,
}
impl JsArray {
/// Create a new empty array.
#[inline]
pub fn new(context: &mut Context) -> Self {
let inner = Array::array_create(0, None, context)
.expect("creating an empty array with the default prototype must not fail");
Self { inner }
}
/// Create an array from a `IntoIterator<Item = JsValue>` convertable object.
#[inline]
pub fn from_iter<I>(elements: I, context: &mut Context) -> Self
where
I: IntoIterator<Item = JsValue>,
{
Self {
inner: Array::create_array_from_list(elements, context),
}
}
/// Create an array from a `JsObject`, if the object is not an array throw a `TypeError`.
///
/// This does not copy the fields of the array, it only does a shallow copy.
#[inline]
pub fn from_object(object: JsObject, context: &mut Context) -> JsResult<Self> {
if object.borrow().is_array() {
Ok(Self { inner: object })
} else {
context.throw_type_error("object is not an Array")
}
}
/// Get the length of the array.
///
/// Same a `array.length` in JavaScript.
#[inline]
pub fn length(&self, context: &mut Context) -> JsResult<usize> {
self.inner.length_of_array_like(context)
}
/// Check if the array is empty, i.e. the `length` is zero.
#[inline]
pub fn is_empty(&self, context: &mut Context) -> JsResult<bool> {
self.inner.length_of_array_like(context).map(|len| len == 0)
}
/// Push an element to the array.
#[inline]
pub fn push<T>(&self, value: T, context: &mut Context) -> JsResult<JsValue>
where
T: Into<JsValue>,
{
self.push_items(&[value.into()], context)
}
/// Pushes a slice of elements to the array.
#[inline]
pub fn push_items(&self, items: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Array::push(&self.inner.clone().into(), items, context)
}
/// Pops an element from the array.
#[inline]
pub fn pop(&self, context: &mut Context) -> JsResult<JsValue> {
Array::pop(&self.inner.clone().into(), &[], context)
}
#[inline]
pub fn at<T>(&self, index: T, context: &mut Context) -> JsResult<JsValue>
where
T: Into<i64>,
{
Array::at(&self.inner.clone().into(), &[index.into().into()], context)
}
#[inline]
pub fn shift(&self, context: &mut Context) -> JsResult<JsValue> {
Array::shift(&self.inner.clone().into(), &[], context)
}
#[inline]
pub fn unshift(&self, items: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Array::shift(&self.inner.clone().into(), items, context)
}
#[inline]
pub fn reverse(&self, context: &mut Context) -> JsResult<Self> {
Array::reverse(&self.inner.clone().into(), &[], context)?;
Ok(self.clone())
}
#[inline]
pub fn concat(&self, items: &[JsValue], context: &mut Context) -> JsResult<Self> {
let object = Array::concat(&self.inner.clone().into(), items, context)?
.as_object()
.cloned()
.expect("Array.prototype.filter should always return object");
Self::from_object(object, context)
}
#[inline]
pub fn join(&self, separator: Option<JsString>, context: &mut Context) -> JsResult<JsString> {
Array::join(&self.inner.clone().into(), &[separator.into()], context).map(|x| {
x.as_string()
.cloned()
.expect("Array.prototype.join always returns string")
})
}
#[inline]
pub fn fill<T>(
&self,
value: T,
start: Option<u32>,
end: Option<u32>,
context: &mut Context,
) -> JsResult<Self>
where
T: Into<JsValue>,
{
Array::fill(
&self.inner.clone().into(),
&[value.into(), start.into(), end.into()],
context,
)?;
Ok(self.clone())
}
#[inline]
pub fn index_of<T>(
&self,
search_element: T,
from_index: Option<u32>,
context: &mut Context,
) -> JsResult<Option<u32>>
where
T: Into<JsValue>,
{
let index = Array::index_of(
&self.inner.clone().into(),
&[search_element.into(), from_index.into()],
context,
)?
.as_number()
.expect("Array.prototype.indexOf should always return number");
#[allow(clippy::float_cmp)]
if index == -1.0 {
Ok(None)
} else {
Ok(Some(index as u32))
}
}
#[inline]
pub fn last_index_of<T>(
&self,
search_element: T,
from_index: Option<u32>,
context: &mut Context,
) -> JsResult<Option<u32>>
where
T: Into<JsValue>,
{
let index = Array::last_index_of(
&self.inner.clone().into(),
&[search_element.into(), from_index.into()],
context,
)?
.as_number()
.expect("Array.prototype.lastIndexOf should always return number");
#[allow(clippy::float_cmp)]
if index == -1.0 {
Ok(None)
} else {
Ok(Some(index as u32))
}
}
#[inline]
pub fn find(
&self,
predicate: JsObject,
this_arg: Option<JsValue>,
context: &mut Context,
) -> JsResult<JsValue> {
Array::find(
&self.inner.clone().into(),
&[predicate.into(), this_arg.into()],
context,
)
}
#[inline]
pub fn filter(
&self,
callback: JsObject,
this_arg: Option<JsValue>,
context: &mut Context,
) -> JsResult<Self> {
let object = Array::filter(
&self.inner.clone().into(),
&[callback.into(), this_arg.into()],
context,
)?
.as_object()
.cloned()
.expect("Array.prototype.filter should always return object");
Self::from_object(object, context)
}
#[inline]
pub fn map(
&self,
callback: JsObject,
this_arg: Option<JsValue>,
context: &mut Context,
) -> JsResult<Self> {
let object = Array::map(
&self.inner.clone().into(),
&[callback.into(), this_arg.into()],
context,
)?
.as_object()
.cloned()
.expect("Array.prototype.map should always return object");
Self::from_object(object, context)
}
#[inline]
pub fn every(
&self,
callback: JsObject,
this_arg: Option<JsValue>,
context: &mut Context,
) -> JsResult<bool> {
let result = Array::every(
&self.inner.clone().into(),
&[callback.into(), this_arg.into()],
context,
)?
.as_boolean()
.expect("Array.prototype.every should always return boolean");
Ok(result)
}
#[inline]
pub fn some(
&self,
callback: JsObject,
this_arg: Option<JsValue>,
context: &mut Context,
) -> JsResult<bool> {
let result = Array::some(
&self.inner.clone().into(),
&[callback.into(), this_arg.into()],
context,
)?
.as_boolean()
.expect("Array.prototype.some should always return boolean");
Ok(result)
}
#[inline]
pub fn sort(&self, compare_fn: Option<JsObject>, context: &mut Context) -> JsResult<Self> {
Array::sort(&self.inner.clone().into(), &[compare_fn.into()], context)?;
Ok(self.clone())
}
#[inline]
pub fn slice(
&self,
start: Option<u32>,
end: Option<u32>,
context: &mut Context,
) -> JsResult<Self> {
let object = Array::slice(
&self.inner.clone().into(),
&[start.into(), end.into()],
context,
)?
.as_object()
.cloned()
.expect("Array.prototype.slice should always return object");
Self::from_object(object, context)
}
#[inline]
pub fn reduce(
&self,
callback: JsObject,
initial_value: Option<JsValue>,
context: &mut Context,
) -> JsResult<JsValue> {
Array::reduce(
&self.inner.clone().into(),
&[callback.into(), initial_value.into()],
context,
)
}
#[inline]
pub fn reduce_right(
&self,
callback: JsObject,
initial_value: Option<JsValue>,
context: &mut Context,
) -> JsResult<JsValue> {
Array::reduce_right(
&self.inner.clone().into(),
&[callback.into(), initial_value.into()],
context,
)
}
}
impl From<JsArray> for JsObject {
#[inline]
fn from(o: JsArray) -> Self {
o.inner.clone()
}
}
impl From<JsArray> for JsValue {
#[inline]
fn from(o: JsArray) -> Self {
o.inner.clone().into()
}
}
impl Deref for JsArray {
type Target = JsObject;
#[inline]
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl JsObjectType for JsArray {}

8
boa/src/object/mod.rs

@ -53,10 +53,18 @@ use self::internal_methods::{
mod tests;
pub(crate) mod internal_methods;
mod jsarray;
mod jsobject;
mod operations;
mod property_map;
pub use jsarray::*;
pub(crate) trait JsObjectType:
Into<JsValue> + Into<JsObject> + Deref<Target = JsObject>
{
}
/// Static `prototype`, usually set on constructors as a key to point to their respective prototype object.
pub static PROTOTYPE: &str = "prototype";

2
boa/src/value/conversions.rs

@ -153,7 +153,7 @@ where
fn from(value: Option<T>) -> Self {
match value {
Some(value) => value.into(),
None => Self::null(),
None => Self::undefined(),
}
}
}

Loading…
Cancel
Save