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}