diff --git a/boa_engine/src/object/jsmap.rs b/boa_engine/src/object/jsmap.rs new file mode 100644 index 0000000000..885929c708 --- /dev/null +++ b/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 = 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 = 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 { + // 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 { + 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 { + 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 { + 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(&self, key: K, value: V, context: &mut Context) -> JsResult + where + K: Into, + V: Into, + { + 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 { + 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(&self, key: T, context: &mut Context) -> JsResult + where + T: Into, + { + 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(&self, key: T, context: &mut Context) -> JsResult + where + T: Into, + { + 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 { + 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(&self, key: T, context: &mut Context) -> JsResult + where + T: Into, + { + 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 { + 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 { + 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 for JsObject { + #[inline] + fn from(o: JsMap) -> Self { + o.inner.clone() + } +} + +impl From 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 {} diff --git a/boa_engine/src/object/jsmap_iterator.rs b/boa_engine/src/object/jsmap_iterator.rs new file mode 100644 index 0000000000..ae19562cc2 --- /dev/null +++ b/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 { + 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 { + MapIterator::next(&self.inner.clone().into(), &[], context) + } +} + +impl From for JsObject { + #[inline] + fn from(o: JsMapIterator) -> Self { + o.inner.clone() + } +} + +impl From 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 {} diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index b4f9fc313e..de34cfb93a 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -313,7 +313,7 @@ impl JsObject { self.borrow().is_array_buffer() } - /// Checks if it is a `Map` object.pub + /// Checks if it is a `Map` object. /// /// # Panics /// @@ -324,6 +324,17 @@ impl JsObject { 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. /// /// # Panics diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 73b80427d9..204644b74b 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -63,6 +63,8 @@ mod tests; pub(crate) mod internal_methods; mod jsarray; mod jsfunction; +mod jsmap; +mod jsmap_iterator; mod jsobject; mod jsproxy; mod jstypedarray; @@ -71,6 +73,8 @@ mod property_map; pub use jsarray::*; pub use jsfunction::*; +pub use jsmap::*; +pub use jsmap_iterator::*; pub use jsproxy::*; pub use jstypedarray::*; diff --git a/boa_examples/src/bin/jsmap.rs b/boa_examples/src/bin/jsmap.rs new file mode 100644 index 0000000000..df18bebb7d --- /dev/null +++ b/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(()) +}