use crate::{
function::Params,
qjs::{self},
value::Constructor,
Atom, Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value,
};
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::{hash::Hash, marker::PhantomData, mem, ops::Deref, ptr::NonNull};
mod cell;
mod trace;
pub(crate) mod ffi;
pub use cell::{
Borrow, BorrowMut, JsCell, Mutability, OwnedBorrow, OwnedBorrowMut, Readable, Writable,
};
use ffi::{ClassCell, VTable};
pub use trace::{Trace, Tracer};
#[doc(hidden)]
pub mod impl_;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClassKind {
Plain,
Callable,
Exotic,
}
pub struct PropertyDescriptor<'js> {
pub value: Value<'js>,
pub getter: Value<'js>,
pub setter: Value<'js>,
pub configurable: bool,
pub enumerable: bool,
pub writable: bool,
pub is_getset: bool,
}
impl<'js> PropertyDescriptor<'js> {
pub fn new_value(
value: Value<'js>,
configurable: bool,
enumerable: bool,
writable: bool,
) -> Self {
let ctx = value.ctx().clone();
PropertyDescriptor {
value,
getter: Value::new_undefined(ctx.clone()),
setter: Value::new_undefined(ctx),
configurable,
enumerable,
writable,
is_getset: false,
}
}
}
pub struct PropertyName<'js> {
pub atom: Atom<'js>,
pub is_enumerable: bool,
}
pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized {
const NAME: &'static str;
const KIND: ClassKind = ClassKind::Plain;
type Mutable: Mutability;
fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
Object::new(ctx.clone()).map(Some)
}
fn constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>>;
fn call<'a>(this: &JsCell<'js, Self>, params: Params<'a, 'js>) -> Result<Value<'js>> {
let _ = this;
Ok(Value::new_undefined(params.ctx().clone()))
}
fn exotic_get_property(
this: &JsCell<'js, Self>,
ctx: &Ctx<'js>,
_atom: Atom<'js>,
_receiver: Value<'js>,
) -> Result<Value<'js>> {
let _ = this;
Ok(Value::new_undefined(ctx.clone()))
}
fn exotic_set_property(
this: &JsCell<'js, Self>,
_ctx: &Ctx<'js>,
_atom: Atom<'js>,
_receiver: Value<'js>,
_value: Value<'js>,
) -> Result<bool> {
let _ = this;
Ok(false)
}
fn exotic_delete_property(
this: &JsCell<'js, Self>,
_ctx: &Ctx<'js>,
_atom: Atom<'js>,
) -> Result<bool> {
let _ = this;
Ok(false)
}
fn exotic_has_property(
this: &JsCell<'js, Self>,
_ctx: &Ctx<'js>,
_atom: Atom<'js>,
) -> Result<bool> {
let _ = this;
Ok(false)
}
fn exotic_get_own_property(
this: &JsCell<'js, Self>,
_ctx: &Ctx<'js>,
_atom: Atom<'js>,
) -> Result<Option<PropertyDescriptor<'js>>> {
let _ = this;
Ok(None)
}
fn exotic_get_own_property_names(
this: &JsCell<'js, Self>,
_ctx: &Ctx<'js>,
) -> Result<Vec<PropertyName<'js>>> {
let _ = this;
Ok(Vec::new())
}
}
#[repr(transparent)]
pub struct Class<'js, C: JsClass<'js>>(pub(crate) Object<'js>, PhantomData<C>);
impl<'js, C: JsClass<'js>> Clone for Class<'js, C> {
fn clone(&self) -> Self {
Class(self.0.clone(), PhantomData)
}
}
impl<'js, C: JsClass<'js>> PartialEq for Class<'js, C> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<'js, C: JsClass<'js>> Eq for Class<'js, C> {}
impl<'js, C: JsClass<'js>> Hash for Class<'js, C> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state)
}
}
unsafe impl<'js, C> JsLifetime<'js> for Class<'js, C>
where
C: JsClass<'js> + JsLifetime<'js>,
for<'to> C::Changed<'to>: JsClass<'to>,
{
type Changed<'to> = Class<'to, C::Changed<'to>>;
}
impl<'js, C: JsClass<'js>> Deref for Class<'js, C> {
type Target = Object<'js>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'js, C: JsClass<'js>> Class<'js, C> {
pub fn instance(ctx: Ctx<'js>, value: C) -> Result<Class<'js, C>> {
let id = unsafe { class_id::<C>(&ctx)? };
let prototype = Self::prototype(&ctx)?;
let prototype = prototype.map(|x| x.as_js_value()).unwrap_or(qjs::JS_NULL);
let val = unsafe {
ctx.handle_exception(qjs::JS_NewObjectProtoClass(ctx.as_ptr(), prototype, id))?
};
let ptr = Box::into_raw(Box::new(ClassCell::new(value)));
unsafe { qjs::JS_SetOpaque(val, ptr.cast()) };
Ok(Self(
unsafe { Object::from_js_value(ctx, val) },
PhantomData,
))
}
pub fn instance_proto(value: C, proto: Object<'js>) -> Result<Class<'js, C>> {
let id = unsafe { class_id::<C>(proto.ctx())? };
let val = unsafe {
proto.ctx.handle_exception(qjs::JS_NewObjectProtoClass(
proto.ctx().as_ptr(),
proto.0.as_js_value(),
id,
))?
};
let ptr = Box::into_raw(Box::new(ClassCell::new(value)));
unsafe { qjs::JS_SetOpaque(val, ptr.cast()) };
Ok(Self(
unsafe { Object::from_js_value(proto.ctx.clone(), val) },
PhantomData,
))
}
pub fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
unsafe { ctx.get_opaque().get_or_insert_prototype::<C>(ctx) }
}
pub fn create_constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>> {
C::constructor(ctx)
}
pub fn define(object: &Object<'js>) -> Result<()> {
if let Some(constructor) = Self::create_constructor(object.ctx())? {
object.set(C::NAME, constructor)?;
}
Ok(())
}
#[inline]
pub(crate) fn get_class_cell<'a>(&self) -> &'a ClassCell<JsCell<'js, C>> {
unsafe { self.get_class_ptr().as_ref() }
}
#[inline]
pub fn get_cell<'a>(&self) -> &'a JsCell<'js, C> {
&self.get_class_cell().data
}
#[inline]
pub fn borrow<'a>(&'a self) -> Borrow<'a, 'js, C> {
self.get_cell().borrow()
}
#[inline]
pub fn borrow_mut<'a>(&'a self) -> BorrowMut<'a, 'js, C> {
self.get_cell().borrow_mut()
}
#[inline]
pub fn try_borrow<'a>(&'a self) -> Result<Borrow<'a, 'js, C>> {
self.get_cell().try_borrow().map_err(Error::ClassBorrow)
}
#[inline]
pub fn try_borrow_mut<'a>(&'a self) -> Result<BorrowMut<'a, 'js, C>> {
self.get_cell().try_borrow_mut().map_err(Error::ClassBorrow)
}
#[inline]
pub(crate) fn get_class_ptr(&self) -> NonNull<ClassCell<JsCell<'js, C>>> {
let id = unsafe { class_id::<C>(&self.ctx).expect("invalid class") };
let ptr = unsafe { qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0 .0.as_js_value(), id) };
NonNull::new(ptr.cast()).expect("invalid class object, object didn't have opaque value")
}
#[inline]
pub fn into_inner(self) -> Object<'js> {
self.0
}
#[inline]
pub fn as_inner(&self) -> &Object<'js> {
&self.0
}
#[inline]
pub fn from_value(value: &Value<'js>) -> Result<Self> {
if let Some(cls) = value.as_object().and_then(Self::from_object) {
return Ok(cls);
}
Err(Error::FromJs {
from: value.type_name(),
to: C::NAME,
message: None,
})
}
#[inline]
pub fn into_value(self) -> Value<'js> {
self.0.into_value()
}
#[inline]
pub fn from_object(object: &Object<'js>) -> Option<Self> {
object.into_class().ok()
}
}
impl<'js> Object<'js> {
pub fn instance_of<C: JsClass<'js>>(&self) -> bool {
let Ok(id) = (unsafe { class_id::<C>(&self.ctx) }) else {
return false;
};
let Some(x) = NonNull::new(unsafe {
qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0.as_js_value(), id)
}) else {
return false;
};
let v_table = unsafe { x.cast::<ClassCell<()>>().as_ref().v_table };
if core::ptr::eq(v_table, VTable::get::<C>()) {
return true;
}
v_table.is_of_class::<C>()
}
pub fn into_class<C: JsClass<'js>>(&self) -> core::result::Result<Class<'js, C>, &Self> {
if self.instance_of::<C>() {
Ok(Class(self.clone(), PhantomData))
} else {
Err(self)
}
}
pub fn as_class<C: JsClass<'js>>(&self) -> Option<&Class<'js, C>> {
if self.instance_of::<C>() {
unsafe { Some(mem::transmute::<&Object<'js>, &Class<'js, C>>(self)) }
} else {
None
}
}
}
impl<'js, C: JsClass<'js>> FromJs<'js> for Class<'js, C> {
fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
Self::from_value(&value)
}
}
impl<'js, C: JsClass<'js>> IntoJs<'js> for Class<'js, C> {
fn into_js(self, _ctx: &Ctx<'js>) -> Result<Value<'js>> {
Ok(self.0 .0)
}
}
unsafe fn class_id<'js, C: JsClass<'js>>(ctx: &Ctx<'js>) -> Result<qjs::JSClassID> {
match C::KIND {
ClassKind::Plain => Ok(ctx.get_opaque().get_class_id()),
ClassKind::Callable => Ok(ctx.get_opaque().get_callable_id()),
ClassKind::Exotic => Ok(ctx.get_opaque().get_exotic_id()),
}
}
#[cfg(test)]
mod test {
use core::sync::atomic::AtomicI32;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use crate::{
class::{ClassKind, JsClass, Readable, Trace, Tracer, Writable},
function::This,
test_with,
value::Constructor,
CatchResultExt, Class, Context, FromIteratorJs, FromJs, Function, IntoJs, JsLifetime,
Object, Runtime,
};
#[test]
fn trace() {
pub struct Container<'js> {
inner: Vec<Class<'js, Container<'js>>>,
test: Arc<AtomicBool>,
}
impl<'js> Drop for Container<'js> {
fn drop(&mut self) {
self.test.store(true, Ordering::SeqCst);
}
}
impl<'js> Trace<'js> for Container<'js> {
fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
self.inner.iter().for_each(|x| x.trace(tracer))
}
}
unsafe impl<'js> JsLifetime<'js> for Container<'js> {
type Changed<'to> = Container<'to>;
}
impl<'js> JsClass<'js> for Container<'js> {
const NAME: &'static str = "Container";
type Mutable = Writable;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
Ok(Some(Object::new(ctx.clone())?))
}
fn constructor(
_ctx: &crate::Ctx<'js>,
) -> crate::Result<Option<crate::value::Constructor<'js>>> {
Ok(None)
}
}
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
let drop_test = Arc::new(AtomicBool::new(false));
ctx.with(|ctx| {
let cls = Class::instance(
ctx.clone(),
Container {
inner: Vec::new(),
test: drop_test.clone(),
},
)
.unwrap();
assert!(cls.instance_of::<Container>());
let cls_clone = cls.clone();
cls.borrow_mut().inner.push(cls_clone);
});
rt.run_gc();
assert!(drop_test.load(Ordering::SeqCst));
ctx.with(|ctx| {
let cls = Class::instance(
ctx.clone(),
Container {
inner: Vec::new(),
test: drop_test.clone(),
},
)
.unwrap();
let cls_clone = cls.clone();
cls.borrow_mut().inner.push(cls_clone);
ctx.globals().set("t", cls).unwrap();
});
}
#[derive(Clone, Copy)]
pub struct Vec3 {
x: f32,
y: f32,
z: f32,
}
impl Vec3 {
pub fn new(x: f32, y: f32, z: f32) -> Self {
Vec3 { x, y, z }
}
pub fn add(self, v: Vec3) -> Self {
Vec3 {
x: self.x + v.x,
y: self.y + v.y,
z: self.z + v.z,
}
}
}
impl<'js> Trace<'js> for Vec3 {
fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
}
impl<'js> FromJs<'js> for Vec3 {
fn from_js(ctx: &crate::Ctx<'js>, value: crate::Value<'js>) -> crate::Result<Self> {
Ok(*Class::<Vec3>::from_js(ctx, value)?.try_borrow()?)
}
}
impl<'js> IntoJs<'js> for Vec3 {
fn into_js(self, ctx: &crate::Ctx<'js>) -> crate::Result<crate::Value<'js>> {
Class::instance(ctx.clone(), self).into_js(ctx)
}
}
unsafe impl<'js> JsLifetime<'js> for Vec3 {
type Changed<'to> = Vec3;
}
impl<'js> JsClass<'js> for Vec3 {
const NAME: &'static str = "Vec3";
type Mutable = Writable;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
let proto = Object::new(ctx.clone())?;
let func = Function::new(ctx.clone(), |this: This<Vec3>, other: Vec3| this.add(other))?
.with_name("add")?;
proto.set("add", func)?;
Ok(Some(proto))
}
fn constructor(
ctx: &crate::Ctx<'js>,
) -> crate::Result<Option<crate::value::Constructor<'js>>> {
let constr =
Constructor::new_class::<Vec3, _, _>(ctx.clone(), |x: f32, y: f32, z: f32| {
Vec3::new(x, y, z)
})?;
Ok(Some(constr))
}
}
#[test]
fn constructor() {
test_with(|ctx| {
Class::<Vec3>::define(&ctx.globals()).unwrap();
let v = ctx
.eval::<Vec3, _>(
r"
let a = new Vec3(1,2,3);
let b = new Vec3(4,2,8);
a.add(b)
",
)
.catch(&ctx)
.unwrap();
approx::assert_abs_diff_eq!(v.x, 5.0);
approx::assert_abs_diff_eq!(v.y, 4.0);
approx::assert_abs_diff_eq!(v.z, 11.0);
let name: String = ctx.eval("new Vec3(1,2,3).constructor.name").unwrap();
assert_eq!(name, Vec3::NAME);
})
}
#[test]
fn extend_class() {
test_with(|ctx| {
Class::<Vec3>::define(&ctx.globals()).unwrap();
let v = ctx
.eval::<Vec3, _>(
r"
class Vec4 extends Vec3 {
w = 0;
constructor(x,y,z,w){
super(x,y,z);
this.w
}
}
new Vec4(1,2,3,4);
",
)
.catch(&ctx)
.unwrap();
approx::assert_abs_diff_eq!(v.x, 1.0);
approx::assert_abs_diff_eq!(v.y, 2.0);
approx::assert_abs_diff_eq!(v.z, 3.0);
})
}
#[test]
fn get_prototype() {
pub struct X;
impl<'js> Trace<'js> for X {
fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
}
unsafe impl<'js> JsLifetime<'js> for X {
type Changed<'to> = X;
}
impl<'js> JsClass<'js> for X {
const NAME: &'static str = "X";
type Mutable = Readable;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
let object = Object::new(ctx.clone())?;
object.set("foo", "bar")?;
Ok(Some(object))
}
fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
Ok(None)
}
}
test_with(|ctx| {
let proto = Class::<X>::prototype(&ctx).unwrap().unwrap();
assert_eq!(proto.get::<_, String>("foo").unwrap(), "bar")
})
}
#[test]
fn generic_types() {
pub struct DebugPrinter<D: std::fmt::Debug> {
d: D,
}
impl<'js, D: std::fmt::Debug> Trace<'js> for DebugPrinter<D> {
fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
}
unsafe impl<'js, D: std::fmt::Debug + 'static> JsLifetime<'js> for DebugPrinter<D> {
type Changed<'to> = DebugPrinter<D>;
}
impl<'js, D: std::fmt::Debug + 'static> JsClass<'js> for DebugPrinter<D> {
const NAME: &'static str = "DebugPrinter";
type Mutable = Readable;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
let object = Object::new(ctx.clone())?;
object.set(
"to_debug_string",
Function::new(
ctx.clone(),
|this: This<Class<DebugPrinter<D>>>| -> crate::Result<String> {
Ok(format!("{:?}", this.0.borrow().d))
},
),
)?;
Ok(Some(object))
}
fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
Ok(None)
}
}
test_with(|ctx| {
let a = Class::instance(ctx.clone(), DebugPrinter { d: 42usize });
let b = Class::instance(
ctx.clone(),
DebugPrinter {
d: "foo".to_string(),
},
);
ctx.globals().set("a", a).unwrap();
ctx.globals().set("b", b).unwrap();
assert_eq!(
ctx.eval::<String, _>(r#" a.to_debug_string() "#)
.catch(&ctx)
.unwrap(),
"42"
);
assert_eq!(
ctx.eval::<String, _>(r#" b.to_debug_string() "#)
.catch(&ctx)
.unwrap(),
"\"foo\""
);
if ctx
.globals()
.get::<_, Class<DebugPrinter<String>>>("a")
.is_ok()
{
panic!("Conversion should fail")
}
if ctx
.globals()
.get::<_, Class<DebugPrinter<usize>>>("b")
.is_ok()
{
panic!("Conversion should fail")
}
ctx.globals()
.get::<_, Class<DebugPrinter<usize>>>("a")
.unwrap();
ctx.globals()
.get::<_, Class<DebugPrinter<String>>>("b")
.unwrap();
})
}
#[test]
fn exotic() {
pub struct ExoticIterator {
curr_state: Arc<AtomicI32>,
}
impl<'js> Trace<'js> for ExoticIterator {
fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
}
unsafe impl<'js> JsLifetime<'js> for ExoticIterator {
type Changed<'to> = ExoticIterator;
}
impl<'js> JsClass<'js> for ExoticIterator {
const NAME: &'static str = "ExoticIterator";
type Mutable = Readable;
const KIND: ClassKind = ClassKind::Exotic;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
Ok(Some(crate::Object::new(ctx.clone())?))
}
fn constructor(
_ctx: &crate::Ctx<'js>,
) -> crate::Result<Option<crate::value::Constructor<'js>>> {
Ok(None)
}
fn exotic_get_property(
this: &crate::class::JsCell<'js, Self>,
ctx: &crate::Ctx<'js>,
atom: crate::Atom<'js>,
_receiver: crate::Value<'js>,
) -> crate::Result<crate::Value<'js>> {
println!("Get property [iter]: {}", atom.to_string()?);
if atom.to_string()? == "next" {
let state = this.borrow().curr_state.clone();
Ok(Function::new(ctx.clone(), move |ctx: crate::Ctx<'js>| {
if state.load(Ordering::SeqCst) <= 1 {
state.store(2, Ordering::SeqCst);
let val = crate::Object::from_iter_js(
&ctx,
[
("done", false.into_js(&ctx)?),
("value", vec!["hello", "1292"].into_js(&ctx)?),
],
)?
.into_value();
Ok::<crate::Value<'_>, crate::Error>(val)
} else if state.load(Ordering::SeqCst) == 2 {
state.fetch_add(1, Ordering::SeqCst);
let val = crate::Object::from_iter_js(
&ctx,
[
("done", false.into_js(&ctx)?),
(
"value",
vec!["i".into_js(&ctx)?, 43.into_js(&ctx)?]
.into_js(&ctx)?,
),
],
)?
.into_value();
Ok(val)
} else {
state.fetch_add(1, Ordering::SeqCst);
let val = crate::Object::from_iter_js(
&ctx,
[
("done", true.into_js(&ctx)?),
("value", crate::Value::new_undefined(ctx.clone())),
],
)?
.into_value();
Ok(val)
}
})?
.into_value())
} else {
Ok(crate::Value::new_undefined(ctx.clone()))
}
}
fn exotic_has_property(
this: &super::JsCell<'js, Self>,
_ctx: &crate::Ctx<'js>,
atom: crate::Atom<'js>,
) -> crate::Result<bool> {
let _ = this;
if atom.to_string()? == "next" {
return Ok(true);
}
Ok(false)
}
}
#[derive(Clone)]
pub struct Exotic {
pub i: i32,
}
impl<'js> Trace<'js> for Exotic {
fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
}
unsafe impl<'js> JsLifetime<'js> for Exotic {
type Changed<'to> = Exotic;
}
impl<'js> JsClass<'js> for Exotic {
const NAME: &'static str = "Exotic";
type Mutable = Writable;
const KIND: ClassKind = ClassKind::Exotic;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
Ok(Some(crate::Object::new(ctx.clone())?))
}
fn constructor(
_ctx: &crate::Ctx<'js>,
) -> crate::Result<Option<crate::value::Constructor<'js>>> {
Ok(None)
}
fn exotic_get_property(
this: &crate::class::JsCell<'js, Self>,
ctx: &crate::Ctx<'js>,
atom: crate::Atom<'js>,
_receiver: crate::Value<'js>,
) -> crate::Result<crate::Value<'js>> {
let symbol_iterator = crate::Atom::from_predefined(
ctx.clone(),
crate::atom::PredefinedAtom::SymbolIterator,
);
println!("Get property: {}", atom.to_string()?);
if atom.to_string()? == "hello" {
assert!(this.borrow().i == 42);
Ok("world".into_js(ctx)?)
} else if atom.to_string()? == "toString" {
Ok(Function::new(ctx.clone(), || {
let f = "class Exotic { [native code] }";
Ok::<&'static str, crate::Error>(f)
})?
.into_value())
} else if atom == symbol_iterator {
println!("Getting iterator");
let exotic = Class::<ExoticIterator>::instance(
ctx.clone(),
ExoticIterator {
curr_state: Arc::default(),
},
)?;
println!("Returning ExoticIterator");
Ok(Function::new(ctx.clone(), move || {
Ok::<crate::Value<'_>, crate::Error>(exotic.clone().into_value())
})?
.into_value())
} else {
Ok(crate::Value::new_null(ctx.clone()))
}
}
fn exotic_set_property(
this: &super::JsCell<'js, Self>,
ctx: &crate::Ctx<'js>,
atom: crate::Atom<'js>,
_receiver: crate::Value<'js>,
_value: crate::Value<'js>,
) -> crate::Result<bool> {
let _ = this;
if atom.to_string()? == "i" {
let Some(new_i) = _value.as_int() else {
let err_val = crate::String::from_str(ctx.clone(), "i must be an integer")?
.into_value();
return Err(ctx.throw(err_val));
};
this.borrow_mut().i = new_i;
return Ok(true);
}
let err_val =
crate::String::from_str(ctx.clone(), "Properties are read-only")?.into_value();
Err(ctx.throw(err_val))
}
fn exotic_has_property(
this: &super::JsCell<'js, Self>,
_ctx: &crate::Ctx<'js>,
atom: crate::Atom<'js>,
) -> crate::Result<bool> {
let _ = this;
println!("Got atom: {}", atom.to_string()?);
if atom.to_string()? == "hello"
|| atom.to_string()? == "i"
|| atom.to_string()? == "toString"
{
return Ok(true);
}
Ok(false)
}
fn exotic_delete_property(
_this: &super::JsCell<'js, Self>,
ctx: &crate::Ctx<'js>,
_atom: crate::Atom<'js>,
) -> crate::Result<bool> {
let err_val = crate::String::from_str(ctx.clone(), "Properties cannot be deleted")?
.into_value();
Err(ctx.throw(err_val))
}
fn exotic_get_own_property(
this: &super::JsCell<'js, Self>,
ctx: &crate::Ctx<'js>,
atom: crate::Atom<'js>,
) -> crate::Result<Option<super::PropertyDescriptor<'js>>> {
let name = atom.to_string()?;
if name == "hello" || name == "i" {
let value = if name == "hello" {
"world".into_js(ctx)?
} else {
this.borrow().i.into_js(ctx)?
};
Ok(Some(super::PropertyDescriptor::new_value(
value, true, true, false,
)))
} else {
Ok(None)
}
}
fn exotic_get_own_property_names(
_this: &super::JsCell<'js, Self>,
ctx: &crate::Ctx<'js>,
) -> crate::Result<Vec<super::PropertyName<'js>>> {
Ok(vec![
super::PropertyName {
atom: crate::Atom::from_str(ctx.clone(), "hello")?,
is_enumerable: true,
},
super::PropertyName {
atom: crate::Atom::from_str(ctx.clone(), "i")?,
is_enumerable: true,
},
])
}
}
test_with(|ctx| {
let exotic = Class::<Exotic>::instance(ctx.clone(), Exotic { i: 0 }).unwrap();
ctx.globals().set("exotic", exotic).unwrap();
ctx.globals()
.set(
"assert",
Function::new(
ctx.clone(),
|ctx: crate::Ctx<'_>, cond: bool, msg: String| {
if !cond {
let err_val =
crate::String::from_str(ctx.clone(), &msg)?.into_value();
return Err(ctx.throw(err_val));
}
Ok(())
},
),
)
.unwrap();
let v = ctx
.eval::<String, _>(
r"
if(exotic.foo !== null) {
throw new Error('foo should be null');
}
try {
exotic.foo = 1
} catch(e) {
if (e?.toString() !== 'Properties are read-only') {
throw new Error('wrong error message: ' + e?.toString());
}
}
if (exotic.foo !== null) {
throw new Error('foo should be null');
}
exotic.i = 42;
if (exotic.hello === 42) {
throw new Error('i should be 42');
}
assert(exotic?.toString() === 'class Exotic { [native code] }', `exotic.toString() should be 'class Exotic { [native code] }' but is ${exotic?.toString()}`);
assert('i' in exotic, 'i should be in exotic');
assert('hello' in exotic, 'hello should be in exotic');
assert(!('foo' in exotic), 'foo should not be in exotic');
try {
delete exotic.i;
} catch(e) {
if (e?.toString() !== 'Properties cannot be deleted') {
throw new Error('wrong error message: ' + e?.toString());
}
}
let resp = []
for (let [objKey, value] of exotic) {
if (objKey !== 'i' && objKey !== 'hello') {
throw new Error('only i and hello should be enumerable, got ' + objKey);
}
resp.push(`${objKey}:${value}`);
}
assert(resp.toString() === 'hello:1292,i:43', `${resp.toString()} with length ${resp.length} should be [] as properties are not enumerable`);
// Test Object.getOwnPropertyNames() (uses get_own_property_names)
let ownNames = Object.getOwnPropertyNames(exotic);
assert(ownNames.length === 2, `getOwnPropertyNames should return 2, got ${ownNames.length}`);
assert(ownNames.includes('hello'), 'getOwnPropertyNames should include hello');
assert(ownNames.includes('i'), 'getOwnPropertyNames should include i');
// Test Object.keys() (uses get_own_property_names + get_own_property)
let keys = Object.keys(exotic);
assert(keys.length === 2, `Object.keys should return 2 keys, got ${keys.length}`);
// Test Object.getOwnPropertyDescriptor() (uses get_own_property)
let desc = Object.getOwnPropertyDescriptor(exotic, 'hello');
assert(desc !== undefined, 'descriptor for hello should exist');
assert(desc.value === 'world', `descriptor value should be world, got ${desc.value}`);
assert(desc.configurable === true, 'hello should be configurable');
assert(desc.enumerable === true, 'hello should be enumerable');
assert(desc.writable === false, 'hello should not be writable');
// Non-existent property returns undefined descriptor
assert(Object.getOwnPropertyDescriptor(exotic, 'nonexistent') === undefined, 'nonexistent should be undefined');
exotic.hello
",
)
.catch(&ctx)
.unwrap();
assert_eq!(v, "world");
})
}
}