boa_engine/object/builtins/
jsmap.rs

1//! A Rust API wrapper for Boa's `Map` Builtin ECMAScript Object
2use crate::{
3    Context, JsResult, JsValue,
4    builtins::{
5        Map,
6        iterable::IteratorHint,
7        map::{add_entries_from_iterable, ordered_map::OrderedMap},
8    },
9    error::JsNativeError,
10    js_string,
11    object::{JsFunction, JsMapIterator, JsObject},
12    value::TryFromJs,
13};
14
15use boa_gc::{Finalize, Trace};
16use std::ops::Deref;
17
18/// `JsMap` provides a wrapper for Boa's implementation of the ECMAScript `Map` object.
19///
20/// # Examples
21///
22/// Create a `JsMap` and set a new entry
23/// ```
24/// # use boa_engine::{
25/// #  object::builtins::JsMap,
26/// #  Context, JsValue, JsResult, js_string
27/// # };
28/// # fn main() -> JsResult<()> {
29/// // Create default `Context`
30/// let context = &mut Context::default();
31///
32/// // Create a new empty `JsMap`.
33/// let map = JsMap::new(context);
34///
35/// // Set key-value pairs for the `JsMap`.
36/// map.set(js_string!("Key-1"), js_string!("Value-1"), context)?;
37/// map.set(js_string!("Key-2"), 10, context)?;
38///
39/// assert_eq!(map.get_size(context)?, 2.into());
40/// # Ok(())
41/// # }
42/// ```
43///
44/// Create a `JsMap` from a `JsArray`
45/// ```
46/// # use boa_engine::{
47/// #    object::builtins::{JsArray, JsMap},
48/// #    Context, JsValue, JsResult, js_string
49/// # };
50/// # fn main() -> JsResult<()> {
51/// // Create a default `Context`
52/// let context = &mut Context::default();
53///
54/// // Create an array of two `[key, value]` pairs
55/// let js_array = JsArray::new(context);
56///
57/// // Create a `[key, value]` pair of JsValues
58/// let vec_one: Vec<JsValue> = vec![
59///     js_string!("first-key").into(),
60///     js_string!("first-value").into()
61/// ];
62///
63/// // We create an push our `[key, value]` pair onto our array as a `JsArray`
64/// js_array.push(JsArray::from_iter(vec_one, context), context)?;
65///
66/// // Create a `JsMap` from the `JsArray` using it's iterable property.
67/// let js_iterable_map = JsMap::from_js_iterable(&js_array.into(), context)?;
68///
69/// assert_eq!(
70///     js_iterable_map.get(js_string!("first-key"), context)?,
71///     js_string!("first-value").into()
72/// );
73///
74/// # Ok(())
75/// }
76/// ```
77#[derive(Debug, Clone, Trace, Finalize)]
78pub struct JsMap {
79    inner: JsObject,
80}
81
82impl JsMap {
83    /// Creates a new empty [`JsMap`] object.
84    ///
85    /// # Example
86    /// ```
87    /// # use boa_engine::{
88    /// #    object::builtins::JsMap,
89    /// #    Context, JsValue,
90    /// # };
91    /// # // Create a new context.
92    /// # let context = &mut Context::default();
93    /// // Create a new empty `JsMap`.
94    /// let map = JsMap::new(context);
95    /// ```
96    #[inline]
97    pub fn new(context: &mut Context) -> Self {
98        let map = Self::create_map(context);
99        Self { inner: map }
100    }
101
102    /// Create a new [`JsMap`] object from a [`JsObject`] that has an `@@Iterator` field.
103    ///
104    /// # Examples
105    /// ```
106    /// # use boa_engine::{
107    /// #    object::builtins::{JsArray, JsMap},
108    /// #    Context, JsResult, JsValue, js_string
109    /// # };
110    /// # fn main() -> JsResult<()> {
111    /// # // Create a default `Context`
112    /// # let context = &mut Context::default();
113    /// // Create an array of two `[key, value]` pairs
114    /// let js_array = JsArray::new(context);
115    ///
116    /// // Create a `[key, value]` pair of JsValues and add it to the `JsArray` as a `JsArray`
117    /// let vec_one: Vec<JsValue> = vec![js_string!("first-key").into(), js_string!("first-value").into()];
118    /// js_array.push(JsArray::from_iter(vec_one, context), context)?;
119    ///
120    /// // Create a `JsMap` from the `JsArray` using it's iterable property.
121    /// let js_iterable_map = JsMap::from_js_iterable(&js_array.into(), context)?;
122    ///
123    /// # Ok(())
124    /// # }
125    /// ```
126    pub fn from_js_iterable(iterable: &JsValue, context: &mut Context) -> JsResult<Self> {
127        // Create a new map object.
128        let map = Self::create_map(context);
129
130        // Let adder be Get(map, "set") per spec. This action should not fail with default map.
131        let adder = map
132            .get(js_string!("set"), context)?
133            .as_function()
134            .ok_or_else(|| {
135                JsNativeError::typ().with_message("property `set` on new `Map` must be callable")
136            })?;
137
138        let _completion_record = add_entries_from_iterable(&map, iterable, &adder, context)?;
139
140        Ok(Self { inner: map })
141    }
142
143    /// Creates a [`JsMap`] from a valid [`JsObject`], or returns a `TypeError` if the provided object is not a [`JsMap`]
144    ///
145    /// # Examples
146    ///
147    /// ### Valid Example - returns a `JsMap` object
148    /// ```
149    /// # use boa_engine::{
150    /// #    builtins::map::ordered_map::OrderedMap,
151    /// #    object::{builtins::JsMap, JsObject},
152    /// #    Context, JsValue, JsResult,
153    /// # };
154    /// # fn main() -> JsResult<()> {
155    /// # let context = &mut Context::default();
156    /// // `some_object` can be any JavaScript `Map` object.
157    /// let some_object = JsObject::from_proto_and_data(
158    ///     context.intrinsics().constructors().map().prototype(),
159    ///     OrderedMap::<JsValue>::new(),
160    /// );
161    ///
162    /// // Create `JsMap` object with incoming object.
163    /// let js_map = JsMap::from_object(some_object)?;
164    /// # Ok(())
165    /// # }
166    /// ```
167    ///
168    /// ### Invalid Example - returns a `TypeError` with the message "object is not a Map"
169    /// ```
170    /// # use boa_engine::{
171    /// #    object::{JsObject, builtins::{JsArray, JsMap}},
172    /// #    Context, JsResult, JsValue,
173    /// # };
174    /// # let context = &mut Context::default();
175    /// let some_object = JsArray::new(context);
176    ///
177    /// // `some_object` is an Array object, not a map object
178    /// assert!(JsMap::from_object(some_object.into()).is_err());
179    /// ```
180    #[inline]
181    pub fn from_object(object: JsObject) -> JsResult<Self> {
182        if object.is::<OrderedMap<JsValue>>() {
183            Ok(Self { inner: object })
184        } else {
185            Err(JsNativeError::typ()
186                .with_message("object is not a Map")
187                .into())
188        }
189    }
190
191    // Utility function to generate the default `Map` object.
192    fn create_map(context: &mut Context) -> JsObject {
193        // Get default Map prototype
194        let prototype = context.intrinsics().constructors().map().prototype();
195
196        // Create a default map object with [[MapData]] as a new empty list
197        JsObject::from_proto_and_data_with_shared_shape(
198            context.root_shape(),
199            prototype,
200            <OrderedMap<JsValue>>::new(),
201        )
202    }
203
204    /// Returns a new [`JsMapIterator`] object that yields the `[key, value]` pairs within the [`JsMap`] in insertion order.
205    #[inline]
206    pub fn entries(&self, context: &mut Context) -> JsResult<JsMapIterator> {
207        let iterator_record = Map::entries(&self.inner.clone().into(), &[], context)?
208            .get_iterator(IteratorHint::Sync, context)?;
209        let map_iterator_object = iterator_record.iterator();
210        JsMapIterator::from_object(map_iterator_object.clone())
211    }
212
213    /// Returns a new [`JsMapIterator`] object that yields the `key` for each element within the [`JsMap`] in insertion order.
214    #[inline]
215    pub fn keys(&self, context: &mut Context) -> JsResult<JsMapIterator> {
216        let iterator_record = Map::keys(&self.inner.clone().into(), &[], context)?
217            .get_iterator(IteratorHint::Sync, context)?;
218        let map_iterator_object = iterator_record.iterator();
219        JsMapIterator::from_object(map_iterator_object.clone())
220    }
221
222    /// Inserts a new entry into the [`JsMap`] object
223    ///
224    /// # Example
225    ///
226    /// ```
227    /// # use boa_engine::{
228    /// #    object::builtins::JsMap,
229    /// #    Context, JsValue, JsResult, js_string
230    /// # };
231    /// # fn main() -> JsResult<()> {
232    /// # let context = &mut Context::default();
233    /// let js_map = JsMap::new(context);
234    ///
235    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
236    /// js_map.set(2, 4, context)?;
237    ///
238    /// assert_eq!(
239    ///     js_map.get(js_string!("foo"), context)?,
240    ///     js_string!("bar").into()
241    /// );
242    /// assert_eq!(js_map.get(2, context)?, 4.into());
243    /// # Ok(())
244    /// # }
245    /// ```
246    pub fn set<K, V>(&self, key: K, value: V, context: &mut Context) -> JsResult<JsValue>
247    where
248        K: Into<JsValue>,
249        V: Into<JsValue>,
250    {
251        Map::set(
252            &self.inner.clone().into(),
253            &[key.into(), value.into()],
254            context,
255        )
256    }
257
258    /// Gets the size of the [`JsMap`] object.
259    ///
260    /// # Example
261    ///
262    /// ```
263    /// # use boa_engine::{
264    /// #    object::builtins::JsMap,
265    /// #    Context, JsValue, JsResult, js_string
266    /// # };
267    /// # fn main() -> JsResult<()> {
268    /// # let context = &mut Context::default();
269    /// let js_map = JsMap::new(context);
270    ///
271    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
272    ///
273    /// let map_size = js_map.get_size(context)?;
274    ///
275    /// assert_eq!(map_size, 1.into());
276    /// # Ok(())
277    /// # }
278    /// ```
279    #[inline]
280    pub fn get_size(&self, context: &mut Context) -> JsResult<JsValue> {
281        Map::get_size(&self.inner.clone().into(), &[], context)
282    }
283
284    /// Removes element from [`JsMap`] with a matching `key` value.
285    ///
286    /// # Example
287    ///
288    /// ```
289    /// # use boa_engine::{
290    /// #    object::builtins::JsMap,
291    /// #    Context, JsValue, JsResult, js_string
292    /// # };
293    /// # fn main() -> JsResult<()> {
294    /// # let context = &mut Context::default();
295    /// let js_map = JsMap::new(context);
296    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
297    /// js_map.set(js_string!("hello"), js_string!("world"), context)?;
298    ///
299    /// js_map.delete(js_string!("foo"), context)?;
300    ///
301    /// assert_eq!(js_map.get_size(context)?, 1.into());
302    /// assert_eq!(
303    ///     js_map.get(js_string!("foo"), context)?,
304    ///     JsValue::undefined()
305    /// );
306    /// # Ok(())
307    /// # }
308    /// ```
309    pub fn delete<T>(&self, key: T, context: &mut Context) -> JsResult<JsValue>
310    where
311        T: Into<JsValue>,
312    {
313        Map::delete(&self.inner.clone().into(), &[key.into()], context)
314    }
315
316    /// Gets the value associated with the specified key within the [`JsMap`], or `undefined` if the key does not exist.
317    ///
318    /// # Example
319    ///
320    /// ```
321    /// # use boa_engine::{
322    /// #    object::builtins::JsMap,
323    /// #    Context, JsValue, JsResult, js_string
324    /// # };
325    /// # fn main() -> JsResult<()> {
326    /// # let context = &mut Context::default();
327    /// let js_map = JsMap::new(context);
328    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
329    ///
330    /// let retrieved_value = js_map.get(js_string!("foo"), context)?;
331    ///
332    /// assert_eq!(retrieved_value, js_string!("bar").into());
333    /// # Ok(())
334    /// # }
335    /// ```
336    pub fn get<T>(&self, key: T, context: &mut Context) -> JsResult<JsValue>
337    where
338        T: Into<JsValue>,
339    {
340        Map::get(&self.inner.clone().into(), &[key.into()], context)
341    }
342
343    /// Removes all entries from the [`JsMap`].
344    ///
345    /// # Example
346    ///
347    /// ```
348    /// # use boa_engine::{
349    /// #    object::builtins::JsMap,
350    /// #    Context, JsValue, JsResult, js_string
351    /// # };
352    /// # fn main() -> JsResult<()> {
353    /// # let context = &mut Context::default();
354    /// let js_map = JsMap::new(context);
355    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
356    /// js_map.set(js_string!("hello"), js_string!("world"), context)?;
357    ///
358    /// js_map.clear(context)?;
359    ///
360    /// assert_eq!(js_map.get_size(context)?, 0.into());
361    /// # Ok(())
362    /// # }
363    /// ```
364    #[inline]
365    pub fn clear(&self, context: &mut Context) -> JsResult<JsValue> {
366        Map::clear(&self.inner.clone().into(), &[], context)
367    }
368
369    /// Checks if [`JsMap`] has an entry with the provided `key` value.
370    ///
371    /// # Example
372    ///
373    /// ```
374    /// # use boa_engine::{
375    /// #    object::builtins::JsMap,
376    /// #    Context, JsValue, JsResult, js_string
377    /// # };
378    /// # fn main() -> JsResult<()> {
379    /// # let context = &mut Context::default();
380    /// let js_map = JsMap::new(context);
381    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
382    ///
383    /// let has_key = js_map.has(js_string!("foo"), context)?;
384    ///
385    /// assert_eq!(has_key, true.into());
386    /// # Ok(())
387    /// # }
388    /// ```
389    pub fn has<T>(&self, key: T, context: &mut Context) -> JsResult<JsValue>
390    where
391        T: Into<JsValue>,
392    {
393        Map::has(&self.inner.clone().into(), &[key.into()], context)
394    }
395
396    /// Executes the provided callback function for each key-value pair within the [`JsMap`].
397    #[inline]
398    pub fn for_each(
399        &self,
400        callback: JsFunction,
401        this_arg: JsValue,
402        context: &mut Context,
403    ) -> JsResult<JsValue> {
404        Map::for_each(
405            &self.inner.clone().into(),
406            &[callback.into(), this_arg],
407            context,
408        )
409    }
410
411    /// Executes the provided callback function for each key-value pair within the [`JsMap`].
412    #[inline]
413    pub fn for_each_native<F>(&self, f: F) -> JsResult<()>
414    where
415        F: FnMut(JsValue, JsValue) -> JsResult<()>,
416    {
417        let this = self.inner.clone().into();
418        Map::for_each_native(&this, f)
419    }
420
421    /// Returns a new [`JsMapIterator`] object that yields the `value` for each element within the [`JsMap`] in insertion order.
422    #[inline]
423    pub fn values(&self, context: &mut Context) -> JsResult<JsMapIterator> {
424        let iterator_record = Map::values(&self.inner.clone().into(), &[], context)?
425            .get_iterator(IteratorHint::Sync, context)?;
426        let map_iterator_object = iterator_record.iterator();
427        JsMapIterator::from_object(map_iterator_object.clone())
428    }
429}
430
431impl From<JsMap> for JsObject {
432    #[inline]
433    fn from(o: JsMap) -> Self {
434        o.inner.clone()
435    }
436}
437
438impl From<JsMap> for JsValue {
439    #[inline]
440    fn from(o: JsMap) -> Self {
441        o.inner.clone().into()
442    }
443}
444
445impl Deref for JsMap {
446    type Target = JsObject;
447
448    #[inline]
449    fn deref(&self) -> &Self::Target {
450        &self.inner
451    }
452}
453
454impl TryFromJs for JsMap {
455    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
456        if let Some(o) = value.as_object() {
457            Self::from_object(o.clone())
458        } else {
459            Err(JsNativeError::typ()
460                .with_message("value is not a Map object")
461                .into())
462        }
463    }
464}