use crate::{
function::IntoJsFunc, qjs, Ctx, Function, IntoAtom, IntoJs, Object, Result, Undefined, Value,
};
impl<'js> Object<'js> {
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "properties")))]
pub fn prop<K, V, P>(&self, key: K, prop: V) -> Result<()>
where
K: IntoAtom<'js>,
V: AsProperty<'js, P>,
{
let ctx = self.ctx();
let key = key.into_atom(ctx)?;
let (flags, value, getter, setter) = prop.config(ctx)?;
let flags = flags | (qjs::JS_PROP_THROW as PropertyFlags);
unsafe {
let res = qjs::JS_DefineProperty(
ctx.as_ptr(),
self.0.as_js_value(),
key.atom,
value.as_js_value(),
getter.as_js_value(),
setter.as_js_value(),
flags,
);
if res < 0 {
return Err(self.0.ctx.raise_exception());
}
}
Ok(())
}
}
pub type PropertyFlags = qjs::c_int;
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "properties")))]
pub trait AsProperty<'js, P> {
fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)>;
}
macro_rules! wrapper_impls {
($($(#[$type_meta:meta])* $type:ident<$($param:ident),*>($($field:ident)*; $($flag:ident)*))*) => {
$(
$(#[$type_meta])*
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "properties")))]
#[derive(Debug, Clone, Copy)]
pub struct $type<$($param),*> {
flags: PropertyFlags,
$($field: $param,)*
}
impl<$($param),*> $type<$($param),*> {
$(wrapper_impls!{@flag $flag concat!("Make the property to be ", stringify!($flag))})*
}
)*
};
(@flag $flag:ident $doc:expr) => {
#[doc = $doc]
#[must_use]
pub fn $flag(mut self) -> Self {
self.flags |= wrapper_impls!(@flag $flag);
self
}
};
(@flag $flag:ident) => { wrapper_impls!{@_flag $flag} as PropertyFlags };
(@_flag configurable) => { qjs::JS_PROP_CONFIGURABLE };
(@_flag enumerable) => { qjs::JS_PROP_ENUMERABLE };
(@_flag writable) => { qjs::JS_PROP_WRITABLE };
(@_flag value) => { qjs::JS_PROP_HAS_VALUE };
(@_flag get) => { qjs::JS_PROP_HAS_GET };
(@_flag set) => { qjs::JS_PROP_HAS_SET };
}
impl<'js, T> AsProperty<'js, T> for T
where
T: IntoJs<'js>,
{
fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
Ok((
wrapper_impls!(@flag value),
self.into_js(ctx)?,
Undefined.into_js(ctx)?,
Undefined.into_js(ctx)?,
))
}
}
wrapper_impls! {
Property<T>(value; writable configurable enumerable)
Accessor<G, S>(get set; configurable enumerable)
}
impl<T> From<T> for Property<T> {
fn from(value: T) -> Self {
Self {
flags: wrapper_impls!(@flag value),
value,
}
}
}
impl<'js, T> AsProperty<'js, T> for Property<T>
where
T: IntoJs<'js>,
{
fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
Ok((
self.flags,
self.value.into_js(ctx)?,
Undefined.into_js(ctx)?,
Undefined.into_js(ctx)?,
))
}
}
impl<G> From<G> for Accessor<G, ()> {
fn from(get: G) -> Self {
Self {
get,
set: (),
flags: wrapper_impls!(@flag get),
}
}
}
impl<G> Accessor<G, ()> {
pub fn new_get(get: G) -> Self {
Self {
flags: wrapper_impls!(@flag get),
get,
set: (),
}
}
pub fn set<S>(self, set: S) -> Accessor<G, S> {
Accessor {
flags: self.flags | wrapper_impls!(@flag set),
get: self.get,
set,
}
}
}
impl<S> Accessor<(), S> {
pub fn new_set(set: S) -> Self {
Self {
flags: wrapper_impls!(@flag set),
get: (),
set,
}
}
pub fn get<G>(self, get: G) -> Accessor<G, S> {
Accessor {
flags: self.flags | wrapper_impls!(@flag get),
get,
set: self.set,
}
}
}
impl<G, S> Accessor<G, S> {
pub fn new(get: G, set: S) -> Self {
Self {
flags: wrapper_impls!(@flag get) | wrapper_impls!(@flag set),
get,
set,
}
}
}
impl<'js, G, GA> AsProperty<'js, (GA, (), ())> for Accessor<G, ()>
where
G: IntoJsFunc<'js, GA> + 'js,
{
fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
Ok((
self.flags,
Undefined.into_js(ctx)?,
Function::new(ctx.clone(), self.get)?.into_value(),
Undefined.into_js(ctx)?,
))
}
}
impl<'js, S, SA> AsProperty<'js, ((), (), SA)> for Accessor<(), S>
where
S: IntoJsFunc<'js, SA> + 'js,
{
fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
Ok((
self.flags,
Undefined.into_js(ctx)?,
Undefined.into_js(ctx)?,
Function::new(ctx.clone(), self.set)?.into_value(),
))
}
}
impl<'js, G, GA, S, SA> AsProperty<'js, (GA, SA)> for Accessor<G, S>
where
G: IntoJsFunc<'js, GA> + 'js,
S: IntoJsFunc<'js, SA> + 'js,
{
fn config(self, ctx: &Ctx<'js>) -> Result<(PropertyFlags, Value<'js>, Value<'js>, Value<'js>)> {
Ok((
self.flags,
Undefined.into_js(ctx)?,
Function::new(ctx.clone(), self.get)?.into_value(),
Function::new(ctx.clone(), self.set)?.into_value(),
))
}
}
#[cfg(test)]
mod test {
use crate::{object::*, *};
#[test]
fn property_with_undefined() {
test_with(|ctx| {
let obj = Object::new(ctx.clone()).unwrap();
obj.prop("key", ()).unwrap();
let _: () = obj.get("key").unwrap();
if let Err(Error::Exception) = obj.set("key", "") {
let exception = Exception::from_js(&ctx, ctx.catch()).unwrap();
assert_eq!(exception.message().as_deref(), Some("'key' is read-only"));
} else {
panic!("Should fail");
}
});
}
#[test]
fn property_with_value() {
test_with(|ctx| {
let obj = Object::new(ctx.clone()).unwrap();
obj.prop("key", "str").unwrap();
let s: StdString = obj.get("key").unwrap();
assert_eq!(s, "str");
if let Err(Error::Exception) = obj.set("key", "") {
let exception = Exception::from_js(&ctx, ctx.catch()).unwrap();
assert_eq!(exception.message().as_deref(), Some("'key' is read-only"));
} else {
panic!("Should fail");
}
});
}
#[test]
fn property_with_data_descriptor() {
test_with(|ctx| {
let obj = Object::new(ctx).unwrap();
obj.prop("key", Property::from("str")).unwrap();
let s: StdString = obj.get("key").unwrap();
assert_eq!(s, "str");
});
}
#[test]
#[should_panic(expected = "Error: 'key' is read-only")]
fn property_with_data_descriptor_readonly() {
test_with(|ctx| {
let obj = Object::new(ctx.clone()).unwrap();
obj.prop("key", Property::from("str")).unwrap();
obj.set("key", "text")
.catch(&ctx)
.map_err(|error| panic!("{}", error))
.unwrap();
});
}
#[test]
fn property_with_data_descriptor_writable() {
test_with(|ctx| {
let obj = Object::new(ctx).unwrap();
obj.prop("key", Property::from("str").writable()).unwrap();
obj.set("key", "text").unwrap();
});
}
#[test]
#[should_panic(expected = "Error: property is not configurable")]
fn property_with_data_descriptor_not_configurable() {
test_with(|ctx| {
let obj = Object::new(ctx.clone()).unwrap();
obj.prop("key", Property::from("str")).unwrap();
obj.prop("key", Property::from(39))
.catch(&ctx)
.map_err(|error| panic!("{}", error))
.unwrap();
});
}
#[test]
fn property_with_data_descriptor_configurable() {
test_with(|ctx| {
let obj = Object::new(ctx).unwrap();
obj.prop("key", Property::from("str").configurable())
.unwrap();
obj.prop("key", Property::from(39)).unwrap();
});
}
#[test]
fn property_with_data_descriptor_not_enumerable() {
test_with(|ctx| {
let obj = Object::new(ctx).unwrap();
obj.prop("key", Property::from("str")).unwrap();
let keys: Vec<StdString> = obj
.own_keys(object::Filter::new().string())
.collect::<Result<_>>()
.unwrap();
assert_eq!(keys.len(), 1);
assert_eq!(&keys[0], "key");
let keys: Vec<StdString> = obj.keys().collect::<Result<_>>().unwrap();
assert_eq!(keys.len(), 0);
});
}
#[test]
fn property_with_data_descriptor_enumerable() {
test_with(|ctx| {
let obj = Object::new(ctx).unwrap();
obj.prop("key", Property::from("str").enumerable()).unwrap();
let keys: Vec<StdString> = obj.keys().collect::<Result<_>>().unwrap();
assert_eq!(keys.len(), 1);
assert_eq!(&keys[0], "key");
});
}
#[test]
fn property_with_getter_only() {
test_with(|ctx| {
let obj = Object::new(ctx.clone()).unwrap();
obj.prop("key", Accessor::from(|| "str")).unwrap();
let s: StdString = obj.get("key").unwrap();
assert_eq!(s, "str");
if let Err(Error::Exception) = obj.set("key", "") {
let exception = Exception::from_js(&ctx, ctx.catch()).unwrap();
assert_eq!(
exception.message().as_deref(),
Some("no setter for property")
);
} else {
panic!("Should fail");
}
});
}
#[test]
fn property_with_getter_and_setter() {
test_with(|ctx| {
let val = Ref::new(Mut::new(StdString::new()));
let obj = Object::new(ctx).unwrap();
obj.prop(
"key",
Accessor::from({
let val = val.clone();
move || val.lock().clone()
})
.set({
let val = val.clone();
move |s| {
*val.lock() = s;
}
}),
)
.unwrap();
let s: StdString = obj.get("key").unwrap();
assert_eq!(s, "");
obj.set("key", "str").unwrap();
assert_eq!(val.lock().clone(), "str");
let s: StdString = obj.get("key").unwrap();
assert_eq!(s, "str");
obj.set("key", "").unwrap();
let s: StdString = obj.get("key").unwrap();
assert_eq!(s, "");
assert_eq!(val.lock().clone(), "");
});
}
}