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}