rquickjs_core/value/
object.rs

1//! Module for types dealing with JS objects.
2
3use crate::{
4    convert::FromIteratorJs, qjs, Array, Atom, Ctx, FromAtom, FromJs, IntoAtom, IntoJs, Result,
5    Value,
6};
7use std::{iter::FusedIterator, marker::PhantomData, mem};
8
9mod property;
10pub use property::{Accessor, AsProperty, Property, PropertyFlags};
11
12/// Rust representation of a JavaScript object.
13#[derive(Debug, PartialEq, Clone, Hash, Eq)]
14#[repr(transparent)]
15pub struct Object<'js>(pub(crate) Value<'js>);
16
17impl<'js> Object<'js> {
18    /// Create a new JavaScript object
19    pub fn new(ctx: Ctx<'js>) -> Result<Self> {
20        Ok(unsafe {
21            let val = qjs::JS_NewObject(ctx.as_ptr());
22            let val = ctx.handle_exception(val)?;
23            Object::from_js_value(ctx, val)
24        })
25    }
26
27    /// Get a new value
28    pub fn get<K: IntoAtom<'js>, V: FromJs<'js>>(&self, k: K) -> Result<V> {
29        let atom = k.into_atom(self.ctx())?;
30        V::from_js(self.ctx(), unsafe {
31            let val = qjs::JS_GetProperty(self.0.ctx.as_ptr(), self.0.as_js_value(), atom.atom);
32            let val = self.0.ctx.handle_exception(val)?;
33            Value::from_js_value(self.0.ctx.clone(), val)
34        })
35    }
36
37    /// check whether the object contains a certain key.
38    pub fn contains_key<K>(&self, k: K) -> Result<bool>
39    where
40        K: IntoAtom<'js>,
41    {
42        let atom = k.into_atom(self.ctx())?;
43        unsafe {
44            let res = qjs::JS_HasProperty(self.0.ctx.as_ptr(), self.0.as_js_value(), atom.atom);
45            if res < 0 {
46                return Err(self.0.ctx.raise_exception());
47            }
48            Ok(res == 1)
49        }
50    }
51
52    /// Set a member of an object to a certain value
53    pub fn set<K: IntoAtom<'js>, V: IntoJs<'js>>(&self, key: K, value: V) -> Result<()> {
54        let atom = key.into_atom(self.ctx())?;
55        let val = value.into_js(self.ctx())?;
56        unsafe {
57            if qjs::JS_SetProperty(
58                self.0.ctx.as_ptr(),
59                self.0.as_js_value(),
60                atom.atom,
61                val.into_js_value(),
62            ) < 0
63            {
64                return Err(self.0.ctx.raise_exception());
65            }
66        }
67        Ok(())
68    }
69
70    /// Remove a member of an object
71    pub fn remove<K: IntoAtom<'js>>(&self, key: K) -> Result<()> {
72        let atom = key.into_atom(self.ctx())?;
73        unsafe {
74            if qjs::JS_DeleteProperty(
75                self.0.ctx.as_ptr(),
76                self.0.as_js_value(),
77                atom.atom,
78                qjs::JS_PROP_THROW as _,
79            ) < 0
80            {
81                return Err(self.0.ctx.raise_exception());
82            }
83        }
84        Ok(())
85    }
86
87    /// Check the object for empty
88    pub fn is_empty(&self) -> bool {
89        self.keys::<Atom>().next().is_none()
90    }
91
92    /// Get the number of properties
93    pub fn len(&self) -> usize {
94        self.keys::<Atom>().count()
95    }
96
97    /// Get own string enumerable property names of an object
98    pub fn keys<K: FromAtom<'js>>(&self) -> ObjectKeysIter<'js, K> {
99        self.own_keys(Filter::default())
100    }
101
102    /// Get own property names of an object
103    pub fn own_keys<K: FromAtom<'js>>(&self, filter: Filter) -> ObjectKeysIter<'js, K> {
104        ObjectKeysIter {
105            state: Some(IterState::new(&self.0, filter.flags)),
106            marker: PhantomData,
107        }
108    }
109
110    /// Get own string enumerable properties of an object
111    pub fn props<K: FromAtom<'js>, V: FromJs<'js>>(&self) -> ObjectIter<'js, K, V> {
112        self.own_props(Filter::default())
113    }
114
115    /// Get own properties of an object
116    pub fn own_props<K: FromAtom<'js>, V: FromJs<'js>>(
117        &self,
118        filter: Filter,
119    ) -> ObjectIter<'js, K, V> {
120        ObjectIter {
121            state: Some(IterState::new(&self.0, filter.flags)),
122            object: self.clone(),
123            marker: PhantomData,
124        }
125    }
126
127    /// Get own string enumerable property values of an object
128    pub fn values<K: FromAtom<'js>>(&self) -> ObjectValuesIter<'js, K> {
129        self.own_values(Filter::default())
130    }
131
132    /// Get own property values of an object
133    pub fn own_values<K: FromAtom<'js>>(&self, filter: Filter) -> ObjectValuesIter<'js, K> {
134        ObjectValuesIter {
135            state: Some(IterState::new(&self.0, filter.flags)),
136            object: self.clone(),
137            marker: PhantomData,
138        }
139    }
140
141    /// Get an object prototype
142    ///
143    /// Objects can have no prototype, in this case this function will return null.
144    pub fn get_prototype(&self) -> Option<Object<'js>> {
145        unsafe {
146            let proto = qjs::JS_GetPrototype(self.0.ctx.as_ptr(), self.0.as_js_value());
147            if qjs::JS_IsNull(proto) {
148                None
149            } else {
150                Some(Object::from_js_value(self.0.ctx.clone(), proto))
151            }
152        }
153    }
154
155    /// Set an object prototype
156    ///
157    /// If called with None the function will set the prototype of the object to null.
158    ///
159    /// This function will error if setting the prototype causes a cycle in the prototype chain.
160    pub fn set_prototype(&self, proto: Option<&Object<'js>>) -> Result<()> {
161        let proto = proto.map(|x| x.as_js_value()).unwrap_or(qjs::JS_NULL);
162        unsafe {
163            if 1 != qjs::JS_SetPrototype(self.0.ctx.as_ptr(), self.0.as_js_value(), proto) {
164                Err(self.0.ctx.raise_exception())
165            } else {
166                Ok(())
167            }
168        }
169    }
170
171    /// Check instance of object
172    pub fn is_instance_of(&self, class: impl AsRef<Value<'js>>) -> bool {
173        let class = class.as_ref();
174        0 != unsafe {
175            qjs::JS_IsInstanceOf(
176                self.0.ctx.as_ptr(),
177                self.0.as_js_value(),
178                class.as_js_value(),
179            )
180        }
181    }
182
183    /// Convert into an array
184    pub fn into_array(self) -> Option<Array<'js>> {
185        if self.is_array() {
186            Some(Array(self))
187        } else {
188            None
189        }
190    }
191}
192
193/// The property filter
194#[derive(Debug, Clone, Copy)]
195#[repr(transparent)]
196pub struct Filter {
197    flags: qjs::c_int,
198}
199
200/// Include only enumerable string properties by default
201impl Default for Filter {
202    fn default() -> Self {
203        Self::new().string().enum_only()
204    }
205}
206
207impl Filter {
208    /// Create filter which includes nothing
209    pub fn new() -> Self {
210        Self { flags: 0 }
211    }
212
213    /// Include string properties
214    #[must_use]
215    pub fn string(mut self) -> Self {
216        self.flags |= qjs::JS_GPN_STRING_MASK as qjs::c_int;
217        self
218    }
219
220    /// Include symbol properties
221    #[must_use]
222    pub fn symbol(mut self) -> Self {
223        self.flags |= qjs::JS_GPN_SYMBOL_MASK as qjs::c_int;
224        self
225    }
226
227    /// Include private properties
228    #[must_use]
229    pub fn private(mut self) -> Self {
230        self.flags |= qjs::JS_GPN_PRIVATE_MASK as qjs::c_int;
231        self
232    }
233
234    /// Include only enumerable properties
235    #[must_use]
236    pub fn enum_only(mut self) -> Self {
237        self.flags |= qjs::JS_GPN_ENUM_ONLY as qjs::c_int;
238        self
239    }
240}
241
242struct IterState<'js> {
243    ctx: Ctx<'js>,
244    enums: *mut qjs::JSPropertyEnum,
245    index: u32,
246    count: u32,
247}
248
249impl<'js> IterState<'js> {
250    fn new(obj: &Value<'js>, flags: qjs::c_int) -> Result<Self> {
251        let ctx = obj.ctx();
252
253        let mut enums = mem::MaybeUninit::uninit();
254        let mut count = mem::MaybeUninit::uninit();
255
256        let (enums, count) = unsafe {
257            if qjs::JS_GetOwnPropertyNames(
258                ctx.as_ptr(),
259                enums.as_mut_ptr(),
260                count.as_mut_ptr(),
261                obj.value,
262                flags,
263            ) < 0
264            {
265                return Err(ctx.raise_exception());
266            }
267            let enums = enums.assume_init();
268            let count = count.assume_init();
269            (enums, count)
270        };
271
272        Ok(Self {
273            ctx: ctx.clone(),
274            enums,
275            count,
276            index: 0,
277        })
278    }
279}
280
281impl<'js> Drop for IterState<'js> {
282    fn drop(&mut self) {
283        // Free atoms which doesn't consumed by the iterator
284        for index in self.index..self.count {
285            let elem = unsafe { &*self.enums.offset(index as isize) };
286            unsafe { qjs::JS_FreeAtom(self.ctx.as_ptr(), elem.atom) };
287        }
288
289        // This is safe because iterator cannot outlive ctx
290        unsafe { qjs::js_free(self.ctx.as_ptr(), self.enums as _) };
291    }
292}
293
294impl<'js> Iterator for IterState<'js> {
295    type Item = Atom<'js>;
296
297    fn next(&mut self) -> Option<Self::Item> {
298        if self.index < self.count {
299            let elem = unsafe { &*self.enums.offset(self.index as _) };
300            self.index += 1;
301            let atom = unsafe { Atom::from_atom_val(self.ctx.clone(), elem.atom) };
302            Some(atom)
303        } else {
304            None
305        }
306    }
307
308    fn size_hint(&self) -> (usize, Option<usize>) {
309        let len = self.len();
310        (len, Some(len))
311    }
312}
313
314impl<'js> DoubleEndedIterator for IterState<'js> {
315    fn next_back(&mut self) -> Option<Self::Item> {
316        if self.index < self.count {
317            self.count -= 1;
318            let elem = unsafe { &*self.enums.offset(self.count as _) };
319            let atom = unsafe { Atom::from_atom_val(self.ctx.clone(), elem.atom) };
320            Some(atom)
321        } else {
322            None
323        }
324    }
325}
326
327impl<'js> ExactSizeIterator for IterState<'js> {
328    fn len(&self) -> usize {
329        (self.count - self.index) as _
330    }
331}
332
333impl<'js> FusedIterator for IterState<'js> {}
334
335/// The iterator for an object own keys
336pub struct ObjectKeysIter<'js, K> {
337    state: Option<Result<IterState<'js>>>,
338    marker: PhantomData<K>,
339}
340
341impl<'js, K> Iterator for ObjectKeysIter<'js, K>
342where
343    K: FromAtom<'js>,
344{
345    type Item = Result<K>;
346
347    fn next(&mut self) -> Option<Self::Item> {
348        if let Some(Ok(state)) = &mut self.state {
349            match state.next() {
350                Some(atom) => Some(K::from_atom(atom)),
351                None => {
352                    self.state = None;
353                    None
354                }
355            }
356        } else if self.state.is_none() {
357            None
358        } else if let Some(Err(error)) = self.state.take() {
359            Some(Err(error))
360        } else {
361            unreachable!();
362        }
363    }
364
365    fn size_hint(&self) -> (usize, Option<usize>) {
366        let len = self.len();
367        (len, Some(len))
368    }
369}
370
371impl<'js, K> DoubleEndedIterator for ObjectKeysIter<'js, K>
372where
373    K: FromAtom<'js>,
374{
375    fn next_back(&mut self) -> Option<Self::Item> {
376        if let Some(Ok(state)) = &mut self.state {
377            match state.next_back() {
378                Some(atom) => Some(K::from_atom(atom)),
379                None => {
380                    self.state = None;
381                    None
382                }
383            }
384        } else if self.state.is_none() {
385            None
386        } else if let Some(Err(error)) = self.state.take() {
387            Some(Err(error))
388        } else {
389            unreachable!();
390        }
391    }
392}
393
394impl<'js, K> ExactSizeIterator for ObjectKeysIter<'js, K>
395where
396    K: FromAtom<'js>,
397{
398    fn len(&self) -> usize {
399        if let Some(Ok(state)) = &self.state {
400            state.len()
401        } else {
402            0
403        }
404    }
405}
406
407impl<'js, K> FusedIterator for ObjectKeysIter<'js, K> where K: FromAtom<'js> {}
408
409/// The iterator for an object own properties
410pub struct ObjectIter<'js, K, V> {
411    state: Option<Result<IterState<'js>>>,
412    object: Object<'js>,
413    marker: PhantomData<(K, V)>,
414}
415
416impl<'js, K, V> Iterator for ObjectIter<'js, K, V>
417where
418    K: FromAtom<'js>,
419    V: FromJs<'js>,
420{
421    type Item = Result<(K, V)>;
422
423    fn next(&mut self) -> Option<Self::Item> {
424        if let Some(Ok(state)) = &mut self.state {
425            match state.next() {
426                Some(atom) => Some(
427                    K::from_atom(atom.clone())
428                        .and_then(|key| self.object.get(atom).map(|val| (key, val))),
429                ),
430                None => {
431                    self.state = None;
432                    None
433                }
434            }
435        } else if self.state.is_none() {
436            None
437        } else if let Some(Err(error)) = self.state.take() {
438            Some(Err(error))
439        } else {
440            unreachable!();
441        }
442    }
443
444    fn size_hint(&self) -> (usize, Option<usize>) {
445        let len = self.len();
446        (len, Some(len))
447    }
448}
449
450impl<'js, K, V> DoubleEndedIterator for ObjectIter<'js, K, V>
451where
452    K: FromAtom<'js>,
453    V: FromJs<'js>,
454{
455    fn next_back(&mut self) -> Option<Self::Item> {
456        if let Some(Ok(state)) = &mut self.state {
457            match state.next_back() {
458                Some(atom) => Some(
459                    K::from_atom(atom.clone())
460                        .and_then(|key| self.object.get(atom).map(|val| (key, val))),
461                ),
462                None => {
463                    self.state = None;
464                    None
465                }
466            }
467        } else if self.state.is_none() {
468            None
469        } else if let Some(Err(error)) = self.state.take() {
470            Some(Err(error))
471        } else {
472            unreachable!();
473        }
474    }
475}
476
477impl<'js, K, V> ExactSizeIterator for ObjectIter<'js, K, V>
478where
479    K: FromAtom<'js>,
480    V: FromJs<'js>,
481{
482    fn len(&self) -> usize {
483        if let Some(Ok(state)) = &self.state {
484            state.len()
485        } else {
486            0
487        }
488    }
489}
490
491impl<'js, K, V> FusedIterator for ObjectIter<'js, K, V>
492where
493    K: FromAtom<'js>,
494    V: FromJs<'js>,
495{
496}
497
498/// The iterator for an object own property values
499pub struct ObjectValuesIter<'js, V> {
500    state: Option<Result<IterState<'js>>>,
501    object: Object<'js>,
502    marker: PhantomData<V>,
503}
504
505impl<'js, V> Iterator for ObjectValuesIter<'js, V>
506where
507    V: FromJs<'js>,
508{
509    type Item = Result<V>;
510
511    fn next(&mut self) -> Option<Self::Item> {
512        if let Some(Ok(state)) = &mut self.state {
513            match state.next() {
514                Some(atom) => Some(self.object.get(atom)),
515                None => {
516                    self.state = None;
517                    None
518                }
519            }
520        } else if self.state.is_none() {
521            None
522        } else if let Some(Err(error)) = self.state.take() {
523            Some(Err(error))
524        } else {
525            unreachable!();
526        }
527    }
528
529    fn size_hint(&self) -> (usize, Option<usize>) {
530        let len = self.len();
531        (len, Some(len))
532    }
533}
534
535impl<'js, V> DoubleEndedIterator for ObjectValuesIter<'js, V>
536where
537    V: FromJs<'js>,
538{
539    fn next_back(&mut self) -> Option<Self::Item> {
540        if let Some(Ok(state)) = &mut self.state {
541            match state.next_back() {
542                Some(atom) => Some(self.object.get(atom)),
543                None => {
544                    self.state = None;
545                    None
546                }
547            }
548        } else if self.state.is_none() {
549            None
550        } else if let Some(Err(error)) = self.state.take() {
551            Some(Err(error))
552        } else {
553            unreachable!();
554        }
555    }
556}
557
558impl<'js, V> ExactSizeIterator for ObjectValuesIter<'js, V>
559where
560    V: FromJs<'js>,
561{
562    fn len(&self) -> usize {
563        if let Some(Ok(state)) = &self.state {
564            state.len()
565        } else {
566            0
567        }
568    }
569}
570
571impl<'js, V> FusedIterator for ObjectValuesIter<'js, V> where V: FromJs<'js> {}
572
573impl<'js> IntoIterator for Object<'js> {
574    type Item = Result<(Atom<'js>, Value<'js>)>;
575    type IntoIter = ObjectIter<'js, Atom<'js>, Value<'js>>;
576
577    fn into_iter(self) -> Self::IntoIter {
578        let flags = qjs::JS_GPN_STRING_MASK as _;
579        ObjectIter {
580            state: Some(IterState::new(&self.0, flags)),
581            object: self,
582            marker: PhantomData,
583        }
584    }
585}
586
587impl<'js, K, V> FromIteratorJs<'js, (K, V)> for Object<'js>
588where
589    K: IntoAtom<'js>,
590    V: IntoJs<'js>,
591{
592    type Item = (Atom<'js>, Value<'js>);
593
594    fn from_iter_js<T>(ctx: &Ctx<'js>, iter: T) -> Result<Self>
595    where
596        T: IntoIterator<Item = (K, V)>,
597    {
598        let object = Object::new(ctx.clone())?;
599        for (key, value) in iter {
600            let key = key.into_atom(ctx)?;
601            let value = value.into_js(ctx)?;
602            object.set(key, value)?;
603        }
604        Ok(object)
605    }
606}
607
608#[cfg(test)]
609mod test {
610    use crate::*;
611
612    #[test]
613    fn from_javascript() {
614        test_with(|ctx| {
615            let val: Object = ctx
616                .eval(
617                    r#"
618                let obj = {};
619                obj['a'] = 3;
620                obj[3] = 'a';
621                obj
622            "#,
623                )
624                .unwrap();
625
626            let text: StdString = val.get(3).unwrap();
627            assert_eq!(text, "a");
628            let int: i32 = val.get("a").unwrap();
629            assert_eq!(int, 3);
630            let int: StdString = val.get(3).unwrap();
631            assert_eq!(int, "a");
632            val.set("hallo", "foo").unwrap();
633            let text: StdString = val.get("hallo").unwrap();
634            assert_eq!(text, "foo".to_string());
635            val.remove("hallo").unwrap();
636            let text: Option<StdString> = val.get("hallo").unwrap();
637            assert_eq!(text, None);
638        });
639    }
640
641    #[test]
642    fn types() {
643        test_with(|ctx| {
644            let val: Object = ctx
645                .eval(
646                    r#"
647                let array_3 = [];
648                array_3[3] = "foo";
649                array_3[99] = 4;
650                ({
651                    array_1: [0,1,2,3,4,5],
652                    array_2: [0,"foo",{},undefined,4,5],
653                    array_3: array_3,
654                    func_1: () => 1,
655                    func_2: function(){ return "foo"},
656                    obj_1: {
657                        a: 1,
658                        b: "foo",
659                    },
660                })
661                "#,
662                )
663                .unwrap();
664            assert!(val.get::<_, Object>("array_1").unwrap().is_array());
665            assert!(val.get::<_, Object>("array_2").unwrap().is_array());
666            assert!(val.get::<_, Object>("array_3").unwrap().is_array());
667            assert!(val.get::<_, Object>("func_1").unwrap().is_function());
668            assert!(val.get::<_, Object>("func_2").unwrap().is_function());
669            assert!(!val.get::<_, Object>("obj_1").unwrap().is_function());
670            assert!(!val.get::<_, Object>("obj_1").unwrap().is_array());
671        })
672    }
673
674    #[test]
675    fn own_keys_iter() {
676        test_with(|ctx| {
677            let val: Object = ctx
678                .eval(
679                    r#"
680                   ({
681                     123: 123,
682                     str: "abc",
683                     arr: [],
684                     '': undefined,
685                   })
686                "#,
687                )
688                .unwrap();
689            let keys = val.keys().collect::<Result<Vec<StdString>>>().unwrap();
690            assert_eq!(keys.len(), 4);
691            assert_eq!(keys[0], "123");
692            assert_eq!(keys[1], "str");
693            assert_eq!(keys[2], "arr");
694            assert_eq!(keys[3], "");
695        })
696    }
697
698    #[test]
699    fn own_props_iter() {
700        test_with(|ctx| {
701            let val: Object = ctx
702                .eval(
703                    r#"
704                   ({
705                     123: "",
706                     str: "abc",
707                     '': "def",
708                   })
709                "#,
710                )
711                .unwrap();
712            let pairs = val
713                .props()
714                .collect::<Result<Vec<(StdString, StdString)>>>()
715                .unwrap();
716            assert_eq!(pairs.len(), 3);
717            assert_eq!(pairs[0].0, "123");
718            assert_eq!(pairs[0].1, "");
719            assert_eq!(pairs[1].0, "str");
720            assert_eq!(pairs[1].1, "abc");
721            assert_eq!(pairs[2].0, "");
722            assert_eq!(pairs[2].1, "def");
723        })
724    }
725
726    #[test]
727    fn into_iter() {
728        test_with(|ctx| {
729            let val: Object = ctx
730                .eval(
731                    r#"
732                   ({
733                     123: 123,
734                     str: "abc",
735                     arr: [],
736                     '': undefined,
737                   })
738                "#,
739                )
740                .unwrap();
741            let pairs = val.into_iter().collect::<Result<Vec<_>>>().unwrap();
742            assert_eq!(pairs.len(), 4);
743            assert_eq!(pairs[0].0.clone().to_string().unwrap(), "123");
744            assert_eq!(i32::from_js(&ctx, pairs[0].1.clone()).unwrap(), 123);
745            assert_eq!(pairs[1].0.clone().to_string().unwrap(), "str");
746            assert_eq!(StdString::from_js(&ctx, pairs[1].1.clone()).unwrap(), "abc");
747            assert_eq!(pairs[2].0.clone().to_string().unwrap(), "arr");
748            assert_eq!(Array::from_js(&ctx, pairs[2].1.clone()).unwrap().len(), 0);
749            assert_eq!(pairs[3].0.clone().to_string().unwrap(), "");
750            assert_eq!(
751                Undefined::from_js(&ctx, pairs[3].1.clone()).unwrap(),
752                Undefined
753            );
754        })
755    }
756
757    #[test]
758    fn iter_take() {
759        test_with(|ctx| {
760            let val: Object = ctx
761                .eval(
762                    r#"
763                   ({
764                     123: 123,
765                     str: "abc",
766                     arr: [],
767                     '': undefined,
768                   })
769                "#,
770                )
771                .unwrap();
772            let keys = val
773                .keys()
774                .take(1)
775                .collect::<Result<Vec<StdString>>>()
776                .unwrap();
777            assert_eq!(keys.len(), 1);
778            assert_eq!(keys[0], "123");
779        })
780    }
781
782    #[test]
783    fn collect_js() {
784        test_with(|ctx| {
785            let object = [("a", "bc"), ("$_", ""), ("", "xyz")]
786                .iter()
787                .cloned()
788                .collect_js::<Object>(&ctx)
789                .unwrap();
790            assert_eq!(
791                StdString::from_js(&ctx, object.get("a").unwrap()).unwrap(),
792                "bc"
793            );
794            assert_eq!(
795                StdString::from_js(&ctx, object.get("$_").unwrap()).unwrap(),
796                ""
797            );
798            assert_eq!(
799                StdString::from_js(&ctx, object.get("").unwrap()).unwrap(),
800                "xyz"
801            );
802        })
803    }
804}