rquickjs_core/value/object/
property.rs

1use crate::{
2    function::IntoJsFunc, qjs, Ctx, Function, IntoAtom, IntoJs, Object, Result, Undefined, Value,
3};
4
5impl<'js> Object<'js> {
6    /// Define a property of an object
7    ///
8    /// ```
9    /// # use rquickjs::{Runtime, Context, Object, object::{Property, Accessor}};
10    /// # let rt = Runtime::new().unwrap();
11    /// # let ctx = Context::full(&rt).unwrap();
12    /// # ctx.with(|ctx| {
13    /// # let obj = Object::new(ctx).unwrap();
14    /// // Define readonly property without value
15    /// obj.prop("no_val", ()).unwrap();
16    /// // Define readonly property with value
17    /// obj.prop("ro_str", "Some const text").unwrap();
18    /// // Define readonly property with value and make it to be writable
19    /// obj.prop("ro_str2", Property::from("Some const text").writable()).unwrap();
20    /// // Define readonly property using getter and make it to be enumerable
21    /// obj.prop("ro_str_get", Accessor::from(|| "Some readable text").enumerable()).unwrap();
22    /// // Define readonly property using getter and setter
23    /// obj.prop("ro_str_get_set",
24    ///     Accessor::from(|| "Some text")
25    ///         .set(|new_val: String| { /* do something */ })
26    /// ).unwrap();
27    /// # })
28    /// ```
29    pub fn prop<K, V, P>(&self, key: K, prop: V) -> Result<()>
30    where
31        K: IntoAtom<'js>,
32        V: AsProperty<'js, P>,
33    {
34        let ctx = self.ctx();
35        let key = key.into_atom(ctx)?;
36        let (flags, value, getter, setter) = prop.config(ctx)?;
37        let flags = flags | (qjs::JS_PROP_THROW as PropertyFlags);
38        unsafe {
39            let res = qjs::JS_DefineProperty(
40                ctx.as_ptr(),
41                self.0.as_js_value(),
42                key.atom,
43                value.as_js_value(),
44                getter.as_js_value(),
45                setter.as_js_value(),
46                flags,
47            );
48            if res < 0 {
49                return Err(self.0.ctx.raise_exception());
50            }
51        }
52        Ok(())
53    }
54}
55
56pub type PropertyFlags = qjs::c_int;
57
58/// The property interface
59pub trait AsProperty<'js, P> {
60    /// Property configuration
61    ///
62    /// Returns the tuple which includes the following:
63    /// - flags
64    /// - value or undefined when no value is here
65    /// - getter or undefined if the property hasn't getter
66    /// - setter or undefined if the property hasn't setter
67    fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)>;
68}
69
70macro_rules! wrapper_impls {
71	  ($($(#[$type_meta:meta])* $type:ident<$($param:ident),*>($($field:ident)*; $($flag:ident)*))*) => {
72        $(
73            $(#[$type_meta])*
74            #[derive(Debug, Clone, Copy)]
75            pub struct $type<$($param),*> {
76                flags: PropertyFlags,
77                $($field: $param,)*
78            }
79
80            impl<$($param),*> $type<$($param),*> {
81                $(wrapper_impls!{@flag $flag concat!("Make the property to be ", stringify!($flag))})*
82            }
83        )*
84	  };
85
86    (@flag $flag:ident $doc:expr) => {
87        #[doc = $doc]
88        #[must_use]
89        pub fn $flag(mut self) -> Self {
90            self.flags |= wrapper_impls!(@flag $flag);
91            self
92        }
93    };
94
95    (@flag $flag:ident) => { wrapper_impls!{@_flag $flag} as PropertyFlags };
96    (@_flag configurable) => { qjs::JS_PROP_CONFIGURABLE };
97    (@_flag enumerable) => { qjs::JS_PROP_ENUMERABLE };
98    (@_flag writable) => { qjs::JS_PROP_WRITABLE };
99    (@_flag value) => { qjs::JS_PROP_HAS_VALUE };
100    (@_flag get) => { qjs::JS_PROP_HAS_GET };
101    (@_flag set) => { qjs::JS_PROP_HAS_SET };
102}
103
104impl<'js, T> AsProperty<'js, T> for T
105where
106    T: IntoJs<'js>,
107{
108    fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
109        Ok((
110            wrapper_impls!(@flag value),
111            self.into_js(ctx)?,
112            Undefined.into_js(ctx)?,
113            Undefined.into_js(ctx)?,
114        ))
115    }
116}
117
118wrapper_impls! {
119    /// The data descriptor of a property
120    Property<T>(value; writable configurable enumerable)
121    /// The accessor descriptor of a readonly property
122    Accessor<G, S>(get set; configurable enumerable)
123}
124
125/// Create property data descriptor from value
126impl<T> From<T> for Property<T> {
127    fn from(value: T) -> Self {
128        Self {
129            flags: wrapper_impls!(@flag value),
130            value,
131        }
132    }
133}
134
135impl<'js, T> AsProperty<'js, T> for Property<T>
136where
137    T: IntoJs<'js>,
138{
139    fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
140        Ok((
141            self.flags,
142            self.value.into_js(ctx)?,
143            Undefined.into_js(ctx)?,
144            Undefined.into_js(ctx)?,
145        ))
146    }
147}
148
149impl<G> From<G> for Accessor<G, ()> {
150    fn from(get: G) -> Self {
151        Self {
152            get,
153            set: (),
154            flags: wrapper_impls!(@flag get),
155        }
156    }
157}
158
159impl<G> Accessor<G, ()> {
160    /// Create accessor from getter
161    pub fn new_get(get: G) -> Self {
162        Self {
163            flags: wrapper_impls!(@flag get),
164            get,
165            set: (),
166        }
167    }
168
169    /// Add setter to accessor
170    pub fn set<S>(self, set: S) -> Accessor<G, S> {
171        Accessor {
172            flags: self.flags | wrapper_impls!(@flag set),
173            get: self.get,
174            set,
175        }
176    }
177}
178
179impl<S> Accessor<(), S> {
180    /// Create accessor from setter
181    pub fn new_set(set: S) -> Self {
182        Self {
183            flags: wrapper_impls!(@flag set),
184            get: (),
185            set,
186        }
187    }
188
189    /// Add getter to accessor
190    pub fn get<G>(self, get: G) -> Accessor<G, S> {
191        Accessor {
192            flags: self.flags | wrapper_impls!(@flag get),
193            get,
194            set: self.set,
195        }
196    }
197}
198
199impl<G, S> Accessor<G, S> {
200    /// Create accessor from getter and setter
201    pub fn new(get: G, set: S) -> Self {
202        Self {
203            flags: wrapper_impls!(@flag get) | wrapper_impls!(@flag set),
204            get,
205            set,
206        }
207    }
208}
209
210/// A property with getter only
211impl<'js, G, GA> AsProperty<'js, (GA, (), ())> for Accessor<G, ()>
212where
213    G: IntoJsFunc<'js, GA> + 'js,
214{
215    fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
216        Ok((
217            self.flags,
218            Undefined.into_js(ctx)?,
219            Function::new(ctx.clone(), self.get)?.into_value(),
220            Undefined.into_js(ctx)?,
221        ))
222    }
223}
224
225/// A property with setter only
226impl<'js, S, SA> AsProperty<'js, ((), (), SA)> for Accessor<(), S>
227where
228    S: IntoJsFunc<'js, SA> + 'js,
229{
230    fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
231        Ok((
232            self.flags,
233            Undefined.into_js(ctx)?,
234            Undefined.into_js(ctx)?,
235            Function::new(ctx.clone(), self.set)?.into_value(),
236        ))
237    }
238}
239
240/// A property with getter and setter
241impl<'js, G, GA, S, SA> AsProperty<'js, (GA, SA)> for Accessor<G, S>
242where
243    G: IntoJsFunc<'js, GA> + 'js,
244    S: IntoJsFunc<'js, SA> + 'js,
245{
246    fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
247        Ok((
248            self.flags,
249            Undefined.into_js(ctx)?,
250            Function::new(ctx.clone(), self.get)?.into_value(),
251            Function::new(ctx.clone(), self.set)?.into_value(),
252        ))
253    }
254}
255
256#[cfg(test)]
257mod test {
258    use crate::{object::*, *};
259
260    #[test]
261    fn property_with_undefined() {
262        test_with(|ctx| {
263            let obj = Object::new(ctx.clone()).unwrap();
264            obj.prop("key", ()).unwrap();
265
266            let _: () = obj.get("key").unwrap();
267
268            if let Err(Error::Exception) = obj.set("key", "") {
269                let exception = Exception::from_js(&ctx, ctx.catch()).unwrap();
270                assert_eq!(exception.message().as_deref(), Some("'key' is read-only"));
271            } else {
272                panic!("Should fail");
273            }
274        });
275    }
276
277    #[test]
278    fn property_with_value() {
279        test_with(|ctx| {
280            let obj = Object::new(ctx.clone()).unwrap();
281            obj.prop("key", "str").unwrap();
282
283            let s: StdString = obj.get("key").unwrap();
284            assert_eq!(s, "str");
285
286            if let Err(Error::Exception) = obj.set("key", "") {
287                let exception = Exception::from_js(&ctx, ctx.catch()).unwrap();
288                assert_eq!(exception.message().as_deref(), Some("'key' is read-only"));
289            } else {
290                panic!("Should fail");
291            }
292        });
293    }
294
295    #[test]
296    fn property_with_data_descriptor() {
297        test_with(|ctx| {
298            let obj = Object::new(ctx).unwrap();
299            obj.prop("key", Property::from("str")).unwrap();
300
301            let s: StdString = obj.get("key").unwrap();
302            assert_eq!(s, "str");
303        });
304    }
305
306    #[test]
307    #[should_panic(expected = "Error: 'key' is read-only")]
308    fn property_with_data_descriptor_readonly() {
309        test_with(|ctx| {
310            let obj = Object::new(ctx.clone()).unwrap();
311            obj.prop("key", Property::from("str")).unwrap();
312            obj.set("key", "text")
313                .catch(&ctx)
314                .map_err(|error| panic!("{}", error))
315                .unwrap();
316        });
317    }
318
319    #[test]
320    fn property_with_data_descriptor_writable() {
321        test_with(|ctx| {
322            let obj = Object::new(ctx).unwrap();
323            obj.prop("key", Property::from("str").writable()).unwrap();
324            obj.set("key", "text").unwrap();
325        });
326    }
327
328    #[test]
329    #[should_panic(expected = "Error: property is not configurable")]
330    fn property_with_data_descriptor_not_configurable() {
331        test_with(|ctx| {
332            let obj = Object::new(ctx.clone()).unwrap();
333            obj.prop("key", Property::from("str")).unwrap();
334            obj.prop("key", Property::from(39))
335                .catch(&ctx)
336                .map_err(|error| panic!("{}", error))
337                .unwrap();
338        });
339    }
340
341    #[test]
342    fn property_with_data_descriptor_configurable() {
343        test_with(|ctx| {
344            let obj = Object::new(ctx).unwrap();
345            obj.prop("key", Property::from("str").configurable())
346                .unwrap();
347            obj.prop("key", Property::from(39)).unwrap();
348        });
349    }
350
351    #[test]
352    fn property_with_data_descriptor_not_enumerable() {
353        test_with(|ctx| {
354            let obj = Object::new(ctx).unwrap();
355            obj.prop("key", Property::from("str")).unwrap();
356            let keys: Vec<StdString> = obj
357                .own_keys(object::Filter::new().string())
358                .collect::<Result<_>>()
359                .unwrap();
360            assert_eq!(keys.len(), 1);
361            assert_eq!(&keys[0], "key");
362            let keys: Vec<StdString> = obj.keys().collect::<Result<_>>().unwrap();
363            assert_eq!(keys.len(), 0);
364        });
365    }
366
367    #[test]
368    fn property_with_data_descriptor_enumerable() {
369        test_with(|ctx| {
370            let obj = Object::new(ctx).unwrap();
371            obj.prop("key", Property::from("str").enumerable()).unwrap();
372            let keys: Vec<StdString> = obj.keys().collect::<Result<_>>().unwrap();
373            assert_eq!(keys.len(), 1);
374            assert_eq!(&keys[0], "key");
375        });
376    }
377
378    #[test]
379    fn property_with_getter_only() {
380        test_with(|ctx| {
381            let obj = Object::new(ctx.clone()).unwrap();
382            obj.prop("key", Accessor::from(|| "str")).unwrap();
383
384            let s: StdString = obj.get("key").unwrap();
385            assert_eq!(s, "str");
386
387            if let Err(Error::Exception) = obj.set("key", "") {
388                let exception = Exception::from_js(&ctx, ctx.catch()).unwrap();
389                assert_eq!(
390                    exception.message().as_deref(),
391                    Some("no setter for property")
392                );
393            } else {
394                panic!("Should fail");
395            }
396        });
397    }
398
399    #[test]
400    fn property_with_getter_and_setter() {
401        test_with(|ctx| {
402            let val = Ref::new(Mut::new(StdString::new()));
403            let obj = Object::new(ctx).unwrap();
404            obj.prop(
405                "key",
406                Accessor::from({
407                    let val = val.clone();
408                    move || val.lock().clone()
409                })
410                .set({
411                    let val = val.clone();
412                    move |s| {
413                        *val.lock() = s;
414                    }
415                }),
416            )
417            .unwrap();
418
419            let s: StdString = obj.get("key").unwrap();
420            assert_eq!(s, "");
421
422            obj.set("key", "str").unwrap();
423            assert_eq!(val.lock().clone(), "str");
424
425            let s: StdString = obj.get("key").unwrap();
426            assert_eq!(s, "str");
427
428            obj.set("key", "").unwrap();
429            let s: StdString = obj.get("key").unwrap();
430            assert_eq!(s, "");
431            assert_eq!(val.lock().clone(), "");
432        });
433    }
434}