Browse Source

Implementation of JsMap Wrapper (#2115)

<!---
Thank you for contributing to Boa! Please fill out the template below, and remove or add any
information as you feel neccessary.
--->

This Pull Request related to JsMap for #2098.

Any feedback on implementing JsMapIterator would be welcome. I wasn't entirely sure if it was the right approach, but as I worked on the example file, it felt like something at least similar would be needed to use Map's .entries(), .keys(), and .values() methods.

It changes the following:

- Implements JsMap Wrapper
- Implements JsMapIterator Wrapper
- Creates JsMap example in boa_examples
pull/2168/head
Kevin 2 years ago
parent
commit
d0d70345eb
  1. 426
      boa_engine/src/object/jsmap.rs
  2. 57
      boa_engine/src/object/jsmap_iterator.rs
  3. 13
      boa_engine/src/object/jsobject.rs
  4. 4
      boa_engine/src/object/mod.rs
  5. 58
      boa_examples/src/bin/jsmap.rs

426
boa_engine/src/object/jsmap.rs

@ -0,0 +1,426 @@
//! This module implements a wrapper for the Map Builtin Javascript Object
use crate::{
builtins::map::{add_entries_from_iterable, ordered_map::OrderedMap},
builtins::Map,
object::{JsFunction, JsMapIterator, JsObject, JsObjectType, ObjectData},
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use std::ops::Deref;
/// `JsMap` provides a wrapper for Boa's implementation of the Javascript `Map` object.
///
/// # Examples
///
/// Create a `JsMap` and set a new entry
/// ```
/// # use boa_engine::{
/// # object::JsMap,
/// # Context, JsValue,
/// # };
///
/// // Create default `Context`
/// let context = &mut Context::default();
///
/// // Create a new empty `JsMap`.
/// let map = JsMap::new(context);
///
/// // Set key-value pairs for the `JsMap`.
/// map.set("Key-1", "Value-1", context).unwrap();
/// map.set("Key-2", 10, context).unwrap();
///
/// assert_eq!(map.get_size(context).unwrap(), 2.into());
///
/// ```
///
/// Create a `JsMap` from a `JsArray`
/// ```
/// # use boa_engine::{
/// # object::{JsArray, JsMap},
/// # Context, JsValue,
/// # };
///
/// // Create a default `Context`
/// let context = &mut Context::default();
///
/// // Create an array of two `[key, value]` pairs
/// let js_array = JsArray::new(context);
///
/// // Create a `[key, value]` pair of JsValues
/// let vec_one: Vec<JsValue> = vec![JsValue::new("first-key"), JsValue::new("first-value")];
///
/// // We create an push our `[key, value]` pair onto our array as a `JsArray`
/// js_array.push(JsArray::from_iter(vec_one, context), context).unwrap();
///
/// // Create a `JsMap` from the `JsArray` using it's iterable property.
/// let js_iterable_map = JsMap::from_js_iterable(&js_array.into(), context).unwrap();
///
/// assert_eq!(js_iterable_map.get("first-key", context).unwrap(), "first-value".into());
///
/// ```
///
#[derive(Debug, Clone, Trace, Finalize)]
pub struct JsMap {
inner: JsObject,
}
impl JsMap {
/// Creates a new empty [`JsMap`] object.
///
/// # Example
///
/// ```
/// # use boa_engine::{
/// # object::JsMap,
/// # Context, JsValue,
/// # };
///
/// // Create a new context.
/// let context = &mut Context::default();
///
/// // Create a new empty `JsMap`.
/// let map = JsMap::new(context);
///
/// ```
#[inline]
pub fn new(context: &mut Context) -> Self {
let map = Self::create_map(context);
Self { inner: map }
}
/// Create a new [`JsMap`] object from a [`JsObject`] that has an `@@Iterator` field.
///
/// # Examples
/// ```
/// # use boa_engine::{
/// # object::{JsArray, JsMap},
/// # Context, JsResult, JsValue,
/// # };
///
/// // Create a default `Context`
/// let context = &mut Context::default();
///
/// // Create an array of two `[key, value]` pairs
/// let js_array = JsArray::new(context);
///
/// // Create a `[key, value]` pair of JsValues and add it to the `JsArray` as a `JsArray`
/// let vec_one: Vec<JsValue> = vec![JsValue::new("first-key"), JsValue::new("first-value")];
/// js_array.push(JsArray::from_iter(vec_one, context), context).unwrap();
///
/// // Create a `JsMap` from the `JsArray` using it's iterable property.
/// let js_iterable_map = JsMap::from_js_iterable(&js_array.into(), context).unwrap();
///
/// ```
///
#[inline]
pub fn from_js_iterable(iterable: &JsValue, context: &mut Context) -> JsResult<Self> {
// Create a new map object.
let map = Self::create_map(context);
// Let adder be Get(map, "set") per spec. This action should not fail with default map.
let adder = map
.get("set", context)
.expect("creating a map with the default prototype must not fail");
let _completion_record = add_entries_from_iterable(&map, iterable, &adder, context)?;
Ok(Self { inner: map })
}
/// Creates a [`JsMap`] from a valid [`JsObject`], or returns a `TypeError` if the provided object is not a [`JsMap`]
///
/// # Examples
///
/// Valid Example - returns a `JsMap` object
/// ```
/// # use boa_engine::{
/// # builtins::map::ordered_map::OrderedMap,
/// # object::{JsObject, ObjectData, JsMap},
/// # Context, JsValue,
/// # };
///
/// let context = &mut Context::default();
///
/// // `some_object` can be any JavaScript `Map` object.
/// let some_object = JsObject::from_proto_and_data(
/// context.intrinsics().constructors().map().prototype(),
/// ObjectData::map(OrderedMap::new())
/// );
///
/// // Create `JsMap` object with incoming object.
/// let js_map = JsMap::from_object(some_object, context).unwrap();
///
/// ```
///
/// Invalid Example - returns a `TypeError` with the message "object is not a Map"
/// ```
/// # use boa_engine::{
/// # object::{JsObject, JsArray, JsMap},
/// # Context, JsResult, JsValue,
/// # };
///
/// let context = &mut Context::default();
///
/// let some_object = JsArray::new(context);
///
/// // Some object is an Array object, not a map object
/// assert!(JsMap::from_object(some_object.into(), context).is_err());
///
/// ```
#[inline]
pub fn from_object(object: JsObject, context: &mut Context) -> JsResult<Self> {
if object.borrow().is_map() {
Ok(Self { inner: object })
} else {
context.throw_type_error("object is not a Map")
}
}
// Utility function to generate the default `Map` object.
fn create_map(context: &mut Context) -> JsObject {
// Get default Map prototype
let prototype = context.intrinsics().constructors().map().prototype();
// Create a default map object with [[MapData]] as a new empty list
JsObject::from_proto_and_data(prototype, ObjectData::map(OrderedMap::new()))
}
/// Returns a new [`JsMapIterator`] object that yields the `[key, value]` pairs within the [`JsMap`] in insertion order.
#[inline]
pub fn entries(&self, context: &mut Context) -> JsResult<JsMapIterator> {
let iterator_record = Map::entries(&self.inner.clone().into(), &[], context)?
.get_iterator(context, None, None)?;
let map_iterator_object = iterator_record.iterator();
JsMapIterator::from_object(map_iterator_object.clone(), context)
}
/// Returns a new [`JsMapIterator`] object that yields the `key` for each element within the [`JsMap`] in insertion order.
#[inline]
pub fn keys(&self, context: &mut Context) -> JsResult<JsMapIterator> {
let iterator_record = Map::keys(&self.inner.clone().into(), &[], context)?
.get_iterator(context, None, None)?;
let map_iterator_object = iterator_record.iterator();
JsMapIterator::from_object(map_iterator_object.clone(), context)
}
/// Inserts a new entry into the [`JsMap`] object
///
/// # Example
///
/// ```
/// # use boa_engine::{
/// # object::JsMap,
/// # Context, JsValue,
/// # };
///
/// let context = &mut Context::default();
///
/// let js_map = JsMap::new(context);
///
/// js_map.set("foo", "bar", context).unwrap();
/// js_map.set(2, 4, context).unwrap();
///
/// assert_eq!(js_map.get("foo", context).unwrap(), "bar".into());
/// assert_eq!(js_map.get(2, context).unwrap(), 4.into())
///
/// ```
#[inline]
pub fn set<K, V>(&self, key: K, value: V, context: &mut Context) -> JsResult<JsValue>
where
K: Into<JsValue>,
V: Into<JsValue>,
{
Map::set(
&self.inner.clone().into(),
&[key.into(), value.into()],
context,
)
}
/// Gets the size of the [`JsMap`] object.
///
/// # Example
///
/// ```
/// # use boa_engine::{
/// # object::JsMap,
/// # Context, JsValue,
/// # };
///
/// let context = &mut Context::default();
///
/// let js_map = JsMap::new(context);
///
/// js_map.set("foo", "bar", context).unwrap();
///
/// let map_size = js_map.get_size(context).unwrap();
///
/// assert_eq!(map_size, 1.into());
///
/// ```
#[inline]
pub fn get_size(&self, context: &mut Context) -> JsResult<JsValue> {
Map::get_size(&self.inner.clone().into(), &[], context)
}
/// Removes element from [`JsMap`] with a matching `key` value.
///
/// # Example
///
/// ```
/// # use boa_engine::{
/// # object::JsMap,
/// # Context, JsValue,
/// # };
///
/// let context = &mut Context::default();
///
/// let js_map = JsMap::new(context);
/// js_map.set("foo", "bar", context).unwrap();
/// js_map.set("hello", "world", context).unwrap();
///
/// js_map.delete("foo", context).unwrap();
///
/// assert_eq!(js_map.get_size(context).unwrap(), 1.into());
/// assert_eq!(js_map.get("foo", context).unwrap(), JsValue::undefined());
///
/// ```
#[inline]
pub fn delete<T>(&self, key: T, context: &mut Context) -> JsResult<JsValue>
where
T: Into<JsValue>,
{
Map::delete(&self.inner.clone().into(), &[key.into()], context)
}
/// Gets the value associated with the specified key within the [`JsMap`], or `undefined` if the key does not exist.
///
/// # Example
///
/// ```
/// # use boa_engine::{
/// # object::JsMap,
/// # Context, JsValue,
/// # };
///
/// let context = &mut Context::default();
/// let js_map = JsMap::new(context);
/// js_map.set("foo", "bar", context).unwrap();
///
/// let retrieved_value = js_map.get("foo", context).unwrap();
///
/// assert_eq!(retrieved_value, "bar".into());
///
/// ```
#[inline]
pub fn get<T>(&self, key: T, context: &mut Context) -> JsResult<JsValue>
where
T: Into<JsValue>,
{
Map::get(&self.inner.clone().into(), &[key.into()], context)
}
/// Removes all entries from the [`JsMap`].
///
/// # Example
///
/// ```
/// # use boa_engine::{
/// # object::JsMap,
/// # Context, JsValue,
/// # };
///
/// let context = &mut Context::default();
///
/// let js_map = JsMap::new(context);
/// js_map.set("foo", "bar", context).unwrap();
/// js_map.set("hello", "world", context).unwrap();
///
/// js_map.clear(context).unwrap();
///
/// assert_eq!(js_map.get_size(context).unwrap(), 0.into());
///
/// ```
#[inline]
pub fn clear(&self, context: &mut Context) -> JsResult<JsValue> {
Map::clear(&self.inner.clone().into(), &[], context)
}
/// Checks if [`JsMap`] has an entry with the provided `key` value.
///
/// # Example
///
/// ```
/// # use boa_engine::{
/// # object::JsMap,
/// # Context, JsValue,
/// # };
///
/// let context = &mut Context::default();
///
/// let js_map = JsMap::new(context);
/// js_map.set("foo", "bar", context).unwrap();
///
/// let has_key = js_map.has("foo", context).unwrap();
///
/// assert_eq!(has_key, true.into());
///
/// ```
#[inline]
pub fn has<T>(&self, key: T, context: &mut Context) -> JsResult<JsValue>
where
T: Into<JsValue>,
{
Map::has(&self.inner.clone().into(), &[key.into()], context)
}
/// Executes the provided callback function for each key-value pair within the [`JsMap`].
#[inline]
pub fn for_each(
&self,
callback: JsFunction,
this_arg: JsValue,
context: &mut Context,
) -> JsResult<JsValue> {
Map::for_each(
&self.inner.clone().into(),
&[callback.into(), this_arg],
context,
)
}
/// Returns a new [`JsMapIterator`] object that yields the `value` for each element within the [`JsMap`] in insertion order.
#[inline]
pub fn values(&self, context: &mut Context) -> JsResult<JsMapIterator> {
let iterator_record = Map::values(&self.inner.clone().into(), &[], context)?
.get_iterator(context, None, None)?;
let map_iterator_object = iterator_record.iterator();
JsMapIterator::from_object(map_iterator_object.clone(), context)
}
}
impl From<JsMap> for JsObject {
#[inline]
fn from(o: JsMap) -> Self {
o.inner.clone()
}
}
impl From<JsMap> for JsValue {
#[inline]
fn from(o: JsMap) -> Self {
o.inner.clone().into()
}
}
impl Deref for JsMap {
type Target = JsObject;
#[inline]
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl JsObjectType for JsMap {}

57
boa_engine/src/object/jsmap_iterator.rs

@ -0,0 +1,57 @@
//! This module implements a wrapper for the `MapIterator` object
use crate::{
builtins::map::map_iterator::MapIterator,
object::{JsObject, JsObjectType},
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use std::ops::Deref;
/// JavaScript `MapIterator` rust object
#[derive(Debug, Clone, Finalize, Trace)]
pub struct JsMapIterator {
inner: JsObject,
}
impl JsMapIterator {
/// Create a [`JsMapIterator`] from a [`JsObject`]. If object is not a `MapIterator`, throw `TypeError`
#[inline]
pub fn from_object(object: JsObject, context: &mut Context) -> JsResult<Self> {
if object.borrow().is_map_iterator() {
Ok(Self { inner: object })
} else {
context.throw_type_error("object is not a MapIterator")
}
}
/// Advances the `JsMapIterator` and gets the next result in the `JsMap`
pub fn next(&self, context: &mut Context) -> JsResult<JsValue> {
MapIterator::next(&self.inner.clone().into(), &[], context)
}
}
impl From<JsMapIterator> for JsObject {
#[inline]
fn from(o: JsMapIterator) -> Self {
o.inner.clone()
}
}
impl From<JsMapIterator> for JsValue {
#[inline]
fn from(o: JsMapIterator) -> Self {
o.inner.clone().into()
}
}
impl Deref for JsMapIterator {
type Target = JsObject;
#[inline]
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl JsObjectType for JsMapIterator {}

13
boa_engine/src/object/jsobject.rs

@ -313,7 +313,7 @@ impl JsObject {
self.borrow().is_array_buffer() self.borrow().is_array_buffer()
} }
/// Checks if it is a `Map` object.pub /// Checks if it is a `Map` object.
/// ///
/// # Panics /// # Panics
/// ///
@ -324,6 +324,17 @@ impl JsObject {
self.borrow().is_map() self.borrow().is_map()
} }
/// Checks if it's a `MapIterator` object
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[inline]
#[track_caller]
pub fn is_map_iterator(&self) -> bool {
self.borrow().is_map_iterator()
}
/// Checks if it's a `String` object. /// Checks if it's a `String` object.
/// ///
/// # Panics /// # Panics

4
boa_engine/src/object/mod.rs

@ -63,6 +63,8 @@ mod tests;
pub(crate) mod internal_methods; pub(crate) mod internal_methods;
mod jsarray; mod jsarray;
mod jsfunction; mod jsfunction;
mod jsmap;
mod jsmap_iterator;
mod jsobject; mod jsobject;
mod jsproxy; mod jsproxy;
mod jstypedarray; mod jstypedarray;
@ -71,6 +73,8 @@ mod property_map;
pub use jsarray::*; pub use jsarray::*;
pub use jsfunction::*; pub use jsfunction::*;
pub use jsmap::*;
pub use jsmap_iterator::*;
pub use jsproxy::*; pub use jsproxy::*;
pub use jstypedarray::*; pub use jstypedarray::*;

58
boa_examples/src/bin/jsmap.rs

@ -0,0 +1,58 @@
use boa_engine::{
object::{JsArray, JsMap},
Context, JsResult, JsValue,
};
fn main() -> JsResult<()> {
// Create a `Context` for the Javascript executor.
let context = &mut Context::default();
// Create a new empty map.
let map = JsMap::new(context);
// Set a key-value for the map.
map.set("Key-1", "Value-1", context)?;
let map_check = map.has("Key-1", context)?;
assert_eq!(map_check, true.into()); // true
// Set a second key-value to the same map.
map.set(2, 4, context)?;
assert_eq!(map.get_size(context)?, 2.into()); //true
assert_eq!(map.get("Key-1", context)?, "Value-1".into());
assert_eq!(map.get(2, context)?, 4.into());
// Delete an entry with a provided key.
map.delete("Key-1", context)?;
assert_eq!(map.get_size(context)?, 1.into());
let deleted_key_one = map.get("Key-1", context)?;
assert_eq!(deleted_key_one, JsValue::undefined());
// Retrieve a MapIterator for all entries in the Map.
let entries = map.entries(context)?;
let _first_value = entries.next(context)?;
// Create a multidimensional array with key value pairs -> [[first-key, first-value], [second-key, second-value]]
let js_array = JsArray::new(context);
let vec_one = vec![JsValue::new("first-key"), JsValue::new("first-value")];
let vec_two = vec![JsValue::new("second-key"), JsValue::new("second-value")];
js_array.push(JsArray::from_iter(vec_one, context), context)?;
js_array.push(JsArray::from_iter(vec_two, context), context)?;
// Create a map from the JsArray using it's iterable property.
let iter_map = JsMap::from_js_iterable(&js_array.into(), context)?;
assert_eq!(iter_map.get("first-key", context)?, "first-value".into());
iter_map.set("third-key", "third-value", context)?;
assert_eq!(iter_map.get_size(context)?, JsValue::new(3));
Ok(())
}
Loading…
Cancel
Save