Skip to main content

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    /// # fn main() -> JsResult<()> {
175    /// # let context = &mut Context::default();
176    /// let some_object = JsArray::new(context)?;
177    ///
178    /// // `some_object` is an Array object, not a map object
179    /// assert!(JsMap::from_object(some_object.into()).is_err());
180    /// # Ok(())
181    /// # }
182    /// ```
183    #[inline]
184    pub fn from_object(object: JsObject) -> JsResult<Self> {
185        if object.is::<OrderedMap<JsValue>>() {
186            Ok(Self { inner: object })
187        } else {
188            Err(JsNativeError::typ()
189                .with_message("object is not a Map")
190                .into())
191        }
192    }
193
194    // Utility function to generate the default `Map` object.
195    fn create_map(context: &mut Context) -> JsObject {
196        // Get default Map prototype
197        let prototype = context.intrinsics().constructors().map().prototype();
198
199        // Create a default map object with [[MapData]] as a new empty list
200        JsObject::from_proto_and_data_with_shared_shape(
201            context.root_shape(),
202            prototype,
203            <OrderedMap<JsValue>>::new(),
204        )
205        .upcast()
206    }
207
208    /// Returns a new [`JsMapIterator`] object that yields the `[key, value]` pairs within the [`JsMap`] in insertion order.
209    #[inline]
210    pub fn entries(&self, context: &mut Context) -> JsResult<JsMapIterator> {
211        let iterator_record = Map::entries(&self.inner.clone().into(), &[], context)?
212            .get_iterator(IteratorHint::Sync, context)?;
213        let map_iterator_object = iterator_record.iterator();
214        JsMapIterator::from_object(map_iterator_object.clone())
215    }
216
217    /// Returns a new [`JsMapIterator`] object that yields the `key` for each element within the [`JsMap`] in insertion order.
218    #[inline]
219    pub fn keys(&self, context: &mut Context) -> JsResult<JsMapIterator> {
220        let iterator_record = Map::keys(&self.inner.clone().into(), &[], context)?
221            .get_iterator(IteratorHint::Sync, context)?;
222        let map_iterator_object = iterator_record.iterator();
223        JsMapIterator::from_object(map_iterator_object.clone())
224    }
225
226    /// Inserts a new entry into the [`JsMap`] object
227    ///
228    /// # Example
229    ///
230    /// ```
231    /// # use boa_engine::{
232    /// #    object::builtins::JsMap,
233    /// #    Context, JsValue, JsResult, js_string
234    /// # };
235    /// # fn main() -> JsResult<()> {
236    /// # let context = &mut Context::default();
237    /// let js_map = JsMap::new(context);
238    ///
239    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
240    /// js_map.set(2, 4, context)?;
241    ///
242    /// assert_eq!(
243    ///     js_map.get(js_string!("foo"), context)?,
244    ///     js_string!("bar").into()
245    /// );
246    /// assert_eq!(js_map.get(2, context)?, 4.into());
247    /// # Ok(())
248    /// # }
249    /// ```
250    pub fn set<K, V>(&self, key: K, value: V, context: &mut Context) -> JsResult<JsValue>
251    where
252        K: Into<JsValue>,
253        V: Into<JsValue>,
254    {
255        Map::set(
256            &self.inner.clone().into(),
257            &[key.into(), value.into()],
258            context,
259        )
260    }
261
262    /// Gets the size of the [`JsMap`] object.
263    ///
264    /// # Example
265    ///
266    /// ```
267    /// # use boa_engine::{
268    /// #    object::builtins::JsMap,
269    /// #    Context, JsValue, JsResult, js_string
270    /// # };
271    /// # fn main() -> JsResult<()> {
272    /// # let context = &mut Context::default();
273    /// let js_map = JsMap::new(context);
274    ///
275    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
276    ///
277    /// let map_size = js_map.get_size(context)?;
278    ///
279    /// assert_eq!(map_size, 1.into());
280    /// # Ok(())
281    /// # }
282    /// ```
283    #[inline]
284    pub fn get_size(&self, context: &mut Context) -> JsResult<JsValue> {
285        Map::get_size(&self.inner.clone().into(), &[], context)
286    }
287
288    /// Removes element from [`JsMap`] with a matching `key` value.
289    ///
290    /// # Example
291    ///
292    /// ```
293    /// # use boa_engine::{
294    /// #    object::builtins::JsMap,
295    /// #    Context, JsValue, JsResult, js_string
296    /// # };
297    /// # fn main() -> JsResult<()> {
298    /// # let context = &mut Context::default();
299    /// let js_map = JsMap::new(context);
300    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
301    /// js_map.set(js_string!("hello"), js_string!("world"), context)?;
302    ///
303    /// js_map.delete(js_string!("foo"), context)?;
304    ///
305    /// assert_eq!(js_map.get_size(context)?, 1.into());
306    /// assert_eq!(
307    ///     js_map.get(js_string!("foo"), context)?,
308    ///     JsValue::undefined()
309    /// );
310    /// # Ok(())
311    /// # }
312    /// ```
313    pub fn delete<T>(&self, key: T, context: &mut Context) -> JsResult<JsValue>
314    where
315        T: Into<JsValue>,
316    {
317        Map::delete(&self.inner.clone().into(), &[key.into()], context)
318    }
319
320    /// Gets the value associated with the specified key within the [`JsMap`], or `undefined` if the key does not exist.
321    ///
322    /// # Example
323    ///
324    /// ```
325    /// # use boa_engine::{
326    /// #    object::builtins::JsMap,
327    /// #    Context, JsValue, JsResult, js_string
328    /// # };
329    /// # fn main() -> JsResult<()> {
330    /// # let context = &mut Context::default();
331    /// let js_map = JsMap::new(context);
332    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
333    ///
334    /// let retrieved_value = js_map.get(js_string!("foo"), context)?;
335    ///
336    /// assert_eq!(retrieved_value, js_string!("bar").into());
337    /// # Ok(())
338    /// # }
339    /// ```
340    pub fn get<T>(&self, key: T, context: &mut Context) -> JsResult<JsValue>
341    where
342        T: Into<JsValue>,
343    {
344        Map::get(&self.inner.clone().into(), &[key.into()], context)
345    }
346
347    /// Removes all entries from the [`JsMap`].
348    ///
349    /// # Example
350    ///
351    /// ```
352    /// # use boa_engine::{
353    /// #    object::builtins::JsMap,
354    /// #    Context, JsValue, JsResult, js_string
355    /// # };
356    /// # fn main() -> JsResult<()> {
357    /// # let context = &mut Context::default();
358    /// let js_map = JsMap::new(context);
359    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
360    /// js_map.set(js_string!("hello"), js_string!("world"), context)?;
361    ///
362    /// js_map.clear(context)?;
363    ///
364    /// assert_eq!(js_map.get_size(context)?, 0.into());
365    /// # Ok(())
366    /// # }
367    /// ```
368    #[inline]
369    pub fn clear(&self, context: &mut Context) -> JsResult<JsValue> {
370        Map::clear(&self.inner.clone().into(), &[], context)
371    }
372
373    /// Checks if [`JsMap`] has an entry with the provided `key` value.
374    ///
375    /// # Example
376    ///
377    /// ```
378    /// # use boa_engine::{
379    /// #    object::builtins::JsMap,
380    /// #    Context, JsValue, JsResult, js_string
381    /// # };
382    /// # fn main() -> JsResult<()> {
383    /// # let context = &mut Context::default();
384    /// let js_map = JsMap::new(context);
385    /// js_map.set(js_string!("foo"), js_string!("bar"), context)?;
386    ///
387    /// let has_key = js_map.has(js_string!("foo"), context)?;
388    ///
389    /// assert_eq!(has_key, true.into());
390    /// # Ok(())
391    /// # }
392    /// ```
393    pub fn has<T>(&self, key: T, context: &mut Context) -> JsResult<JsValue>
394    where
395        T: Into<JsValue>,
396    {
397        Map::has(&self.inner.clone().into(), &[key.into()], context)
398    }
399
400    /// Executes the provided callback function for each key-value pair within the [`JsMap`].
401    #[inline]
402    pub fn for_each(
403        &self,
404        callback: JsFunction,
405        this_arg: JsValue,
406        context: &mut Context,
407    ) -> JsResult<JsValue> {
408        Map::for_each(
409            &self.inner.clone().into(),
410            &[callback.into(), this_arg],
411            context,
412        )
413    }
414
415    /// Executes the provided callback function for each key-value pair within the [`JsMap`].
416    #[inline]
417    pub fn for_each_native<F>(&self, f: F) -> JsResult<()>
418    where
419        F: FnMut(JsValue, JsValue) -> JsResult<()>,
420    {
421        let this = self.inner.clone().into();
422        Map::for_each_native(&this, f)
423    }
424
425    /// Returns a new [`JsMapIterator`] object that yields the `value` for each element within the [`JsMap`] in insertion order.
426    #[inline]
427    pub fn values(&self, context: &mut Context) -> JsResult<JsMapIterator> {
428        let iterator_record = Map::values(&self.inner.clone().into(), &[], context)?
429            .get_iterator(IteratorHint::Sync, context)?;
430        let map_iterator_object = iterator_record.iterator();
431        JsMapIterator::from_object(map_iterator_object.clone())
432    }
433}
434
435impl From<JsMap> for JsObject {
436    #[inline]
437    fn from(o: JsMap) -> Self {
438        o.inner.clone()
439    }
440}
441
442impl From<JsMap> for JsValue {
443    #[inline]
444    fn from(o: JsMap) -> Self {
445        o.inner.clone().into()
446    }
447}
448
449impl Deref for JsMap {
450    type Target = JsObject;
451
452    #[inline]
453    fn deref(&self) -> &Self::Target {
454        &self.inner
455    }
456}
457
458impl TryFromJs for JsMap {
459    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
460        if let Some(o) = value.as_object() {
461            Self::from_object(o.clone())
462        } else {
463            Err(JsNativeError::typ()
464                .with_message("value is not a Map object")
465                .into())
466        }
467    }
468}