use std::cell::RefCell;
use std::ffi::c_void;
use std::rc::Rc;
use crate::objects::property_map::PropertyMap;
use crate::objects::value::JsValue;
pub type NamedGetterResult = Option<JsValue>;
pub type NamedSetterResult = bool;
pub type NamedGetterCallback = Box<dyn Fn(&str, *mut c_void) -> NamedGetterResult>;
pub type NamedSetterCallback = Box<dyn Fn(&str, &JsValue, *mut c_void) -> NamedSetterResult>;
pub type NamedQueryCallback = Box<dyn Fn(&str, *mut c_void) -> Option<u32>>;
pub type NamedDeleterCallback = Box<dyn Fn(&str, *mut c_void) -> bool>;
pub type NamedEnumeratorCallback = Box<dyn Fn(*mut c_void) -> Vec<String>>;
pub type IndexedGetterResult = Option<JsValue>;
pub type IndexedSetterResult = bool;
pub type IndexedGetterCallback = Box<dyn Fn(u32, *mut c_void) -> IndexedGetterResult>;
pub type IndexedSetterCallback = Box<dyn Fn(u32, &JsValue, *mut c_void) -> IndexedSetterResult>;
pub type IndexedQueryCallback = Box<dyn Fn(u32, *mut c_void) -> Option<u32>>;
pub type IndexedLengthCallback = Box<dyn Fn(*mut c_void) -> u32>;
type NamedGetterRef<'a> = Option<&'a dyn Fn(&str, *mut c_void) -> NamedGetterResult>;
type NamedSetterRef<'a> = Option<&'a dyn Fn(&str, &JsValue, *mut c_void) -> NamedSetterResult>;
type NamedQueryRef<'a> = Option<&'a dyn Fn(&str, *mut c_void) -> Option<u32>>;
type NamedDeleterRef<'a> = Option<&'a dyn Fn(&str, *mut c_void) -> bool>;
type IndexedSetterRef<'a> = Option<&'a dyn Fn(u32, &JsValue, *mut c_void) -> IndexedSetterResult>;
pub struct NamedPropertyHandlerConfig {
getter: Option<NamedGetterCallback>,
setter: Option<NamedSetterCallback>,
query: Option<NamedQueryCallback>,
deleter: Option<NamedDeleterCallback>,
enumerator: Option<NamedEnumeratorCallback>,
}
impl NamedPropertyHandlerConfig {
pub fn builder() -> NamedPropertyHandlerConfigBuilder {
NamedPropertyHandlerConfigBuilder::default()
}
pub fn getter(&self) -> NamedGetterRef<'_> {
self.getter.as_deref()
}
pub fn setter(&self) -> NamedSetterRef<'_> {
self.setter.as_deref()
}
pub fn query(&self) -> NamedQueryRef<'_> {
self.query.as_deref()
}
pub fn deleter(&self) -> NamedDeleterRef<'_> {
self.deleter.as_deref()
}
pub fn enumerator(&self) -> Option<&dyn Fn(*mut c_void) -> Vec<String>> {
self.enumerator.as_deref()
}
}
impl std::fmt::Debug for NamedPropertyHandlerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NamedPropertyHandlerConfig")
.field("has_getter", &self.getter.is_some())
.field("has_setter", &self.setter.is_some())
.field("has_query", &self.query.is_some())
.field("has_deleter", &self.deleter.is_some())
.field("has_enumerator", &self.enumerator.is_some())
.finish()
}
}
#[derive(Default)]
pub struct NamedPropertyHandlerConfigBuilder {
getter: Option<NamedGetterCallback>,
setter: Option<NamedSetterCallback>,
query: Option<NamedQueryCallback>,
deleter: Option<NamedDeleterCallback>,
enumerator: Option<NamedEnumeratorCallback>,
}
impl NamedPropertyHandlerConfigBuilder {
pub fn getter(mut self, cb: impl Fn(&str, *mut c_void) -> NamedGetterResult + 'static) -> Self {
self.getter = Some(Box::new(cb));
self
}
pub fn setter(
mut self,
cb: impl Fn(&str, &JsValue, *mut c_void) -> NamedSetterResult + 'static,
) -> Self {
self.setter = Some(Box::new(cb));
self
}
pub fn query(mut self, cb: impl Fn(&str, *mut c_void) -> Option<u32> + 'static) -> Self {
self.query = Some(Box::new(cb));
self
}
pub fn deleter(mut self, cb: impl Fn(&str, *mut c_void) -> bool + 'static) -> Self {
self.deleter = Some(Box::new(cb));
self
}
pub fn enumerator(mut self, cb: impl Fn(*mut c_void) -> Vec<String> + 'static) -> Self {
self.enumerator = Some(Box::new(cb));
self
}
pub fn build(self) -> NamedPropertyHandlerConfig {
NamedPropertyHandlerConfig {
getter: self.getter,
setter: self.setter,
query: self.query,
deleter: self.deleter,
enumerator: self.enumerator,
}
}
}
pub struct IndexedPropertyHandlerConfig {
getter: Option<IndexedGetterCallback>,
setter: Option<IndexedSetterCallback>,
query: Option<IndexedQueryCallback>,
length: Option<IndexedLengthCallback>,
}
impl IndexedPropertyHandlerConfig {
pub fn builder() -> IndexedPropertyHandlerConfigBuilder {
IndexedPropertyHandlerConfigBuilder::default()
}
pub fn getter(&self) -> Option<&dyn Fn(u32, *mut c_void) -> IndexedGetterResult> {
self.getter.as_deref()
}
pub fn setter(&self) -> IndexedSetterRef<'_> {
self.setter.as_deref()
}
pub fn query(&self) -> Option<&dyn Fn(u32, *mut c_void) -> Option<u32>> {
self.query.as_deref()
}
pub fn length(&self) -> Option<&dyn Fn(*mut c_void) -> u32> {
self.length.as_deref()
}
}
impl std::fmt::Debug for IndexedPropertyHandlerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IndexedPropertyHandlerConfig")
.field("has_getter", &self.getter.is_some())
.field("has_setter", &self.setter.is_some())
.field("has_query", &self.query.is_some())
.field("has_length", &self.length.is_some())
.finish()
}
}
#[derive(Default)]
pub struct IndexedPropertyHandlerConfigBuilder {
getter: Option<IndexedGetterCallback>,
setter: Option<IndexedSetterCallback>,
query: Option<IndexedQueryCallback>,
length: Option<IndexedLengthCallback>,
}
impl IndexedPropertyHandlerConfigBuilder {
pub fn getter(
mut self,
cb: impl Fn(u32, *mut c_void) -> IndexedGetterResult + 'static,
) -> Self {
self.getter = Some(Box::new(cb));
self
}
pub fn setter(
mut self,
cb: impl Fn(u32, &JsValue, *mut c_void) -> IndexedSetterResult + 'static,
) -> Self {
self.setter = Some(Box::new(cb));
self
}
pub fn query(mut self, cb: impl Fn(u32, *mut c_void) -> Option<u32> + 'static) -> Self {
self.query = Some(Box::new(cb));
self
}
pub fn length(mut self, cb: impl Fn(*mut c_void) -> u32 + 'static) -> Self {
self.length = Some(Box::new(cb));
self
}
pub fn build(self) -> IndexedPropertyHandlerConfig {
IndexedPropertyHandlerConfig {
getter: self.getter,
setter: self.setter,
query: self.query,
length: self.length,
}
}
}
pub const MAX_INTERNAL_FIELDS: usize = 16;
pub struct DomObjectWrap {
properties: Rc<RefCell<PropertyMap>>,
internal_fields: Vec<*mut c_void>,
named_handler: Option<NamedPropertyHandlerConfig>,
indexed_handler: Option<IndexedPropertyHandlerConfig>,
}
unsafe impl Send for DomObjectWrap {}
impl DomObjectWrap {
pub fn new(field_count: usize) -> Self {
assert!(
field_count <= MAX_INTERNAL_FIELDS,
"field_count ({field_count}) exceeds MAX_INTERNAL_FIELDS ({MAX_INTERNAL_FIELDS})"
);
Self {
properties: Rc::new(RefCell::new(PropertyMap::new())),
internal_fields: vec![std::ptr::null_mut(); field_count],
named_handler: None,
indexed_handler: None,
}
}
pub fn internal_field_count(&self) -> usize {
self.internal_fields.len()
}
pub fn set_internal_field(&mut self, index: usize, ptr: *mut c_void) {
assert!(
index < self.internal_fields.len(),
"internal field index {index} out of range (count = {})",
self.internal_fields.len()
);
self.internal_fields[index] = ptr;
}
pub fn get_internal_field(&self, index: usize) -> *mut c_void {
self.internal_fields
.get(index)
.copied()
.unwrap_or(std::ptr::null_mut())
}
fn data_ptr(&self) -> *mut c_void {
self.internal_fields
.first()
.copied()
.unwrap_or(std::ptr::null_mut())
}
pub fn get_property(&self, key: &str) -> JsValue {
if let Some(cfg) = &self.named_handler
&& let Some(getter) = cfg.getter()
&& let Some(val) = getter(key, self.data_ptr())
{
return val;
}
self.properties
.borrow()
.get(key)
.cloned()
.unwrap_or(JsValue::Undefined)
}
pub fn set_property(&mut self, key: &str, value: JsValue) {
if let Some(cfg) = &self.named_handler
&& let Some(setter) = cfg.setter()
&& setter(key, &value, self.data_ptr())
{
return;
}
self.properties.borrow_mut().insert(key.to_string(), value);
}
pub fn set_intercepted_property(&self, key: &str, value: JsValue) -> bool {
if let Some(cfg) = &self.named_handler
&& let Some(setter) = cfg.setter()
{
return setter(key, &value, self.data_ptr());
}
false
}
pub fn has_property(&self, key: &str) -> bool {
if let Some(cfg) = &self.named_handler
&& let Some(query) = cfg.query()
&& query(key, self.data_ptr()).is_some()
{
return true;
}
self.properties.borrow().contains_key(key)
}
pub fn delete_property(&mut self, key: &str) -> bool {
if let Some(cfg) = &self.named_handler
&& let Some(deleter) = cfg.deleter()
&& deleter(key, self.data_ptr())
{
return true;
}
self.properties.borrow_mut().remove(key).is_some()
}
pub fn property_names(&self) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
if let Some(cfg) = &self.named_handler
&& let Some(enumerator) = cfg.enumerator()
{
names.extend(enumerator(self.data_ptr()));
}
let mut own: Vec<String> = self
.properties
.borrow()
.keys()
.map(|k| k.to_string())
.collect();
own.sort();
names.extend(own);
names
}
pub fn get_indexed(&self, index: u32) -> JsValue {
if let Some(cfg) = &self.indexed_handler
&& let Some(getter) = cfg.getter()
&& let Some(val) = getter(index, self.data_ptr())
{
return val;
}
JsValue::Undefined
}
pub fn set_indexed(&mut self, index: u32, value: &JsValue) -> bool {
if let Some(cfg) = &self.indexed_handler
&& let Some(setter) = cfg.setter()
{
return setter(index, value, self.data_ptr());
}
false
}
pub fn indexed_length(&self) -> u32 {
if let Some(cfg) = &self.indexed_handler
&& let Some(length_cb) = cfg.length()
{
return length_cb(self.data_ptr());
}
0
}
pub fn set_named_handler(&mut self, config: NamedPropertyHandlerConfig) {
self.named_handler = Some(config);
}
pub fn has_named_handler(&self) -> bool {
self.named_handler.is_some()
}
pub fn set_indexed_handler(&mut self, config: IndexedPropertyHandlerConfig) {
self.indexed_handler = Some(config);
}
pub fn has_indexed_handler(&self) -> bool {
self.indexed_handler.is_some()
}
pub fn as_js_value(&self) -> JsValue {
JsValue::PlainObject(Rc::clone(&self.properties))
}
}
impl std::fmt::Debug for DomObjectWrap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DomObjectWrap")
.field("internal_field_count", &self.internal_fields.len())
.field("property_count", &self.properties.borrow().len())
.field("has_named_handler", &self.named_handler.is_some())
.field("has_indexed_handler", &self.indexed_handler.is_some())
.finish()
}
}
pub type DomWeakCallback = Box<dyn FnOnce(*mut c_void)>;
pub struct DomWeakRef {
data: *mut c_void,
callback: RefCell<Option<DomWeakCallback>>,
alive: RefCell<bool>,
}
unsafe impl Send for DomWeakRef {}
impl DomWeakRef {
pub fn new(wrap: &DomObjectWrap, callback: impl FnOnce(*mut c_void) + 'static) -> Self {
let data = wrap.get_internal_field(0);
Self {
data,
callback: RefCell::new(Some(Box::new(callback))),
alive: RefCell::new(true),
}
}
pub fn is_alive(&self) -> bool {
*self.alive.borrow()
}
pub fn data(&self) -> *mut c_void {
self.data
}
pub fn invoke_callback(&self) {
if let Some(cb) = self.callback.borrow_mut().take() {
cb(self.data);
}
*self.alive.borrow_mut() = false;
}
pub fn clear(&self) {
self.callback.borrow_mut().take();
*self.alive.borrow_mut() = false;
}
}
impl std::fmt::Debug for DomWeakRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DomWeakRef")
.field("alive", &*self.alive.borrow())
.field("data", &self.data)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[test]
fn test_wrap_new_zero_fields() {
let wrap = DomObjectWrap::new(0);
assert_eq!(wrap.internal_field_count(), 0);
}
#[test]
fn test_wrap_new_with_fields() {
let wrap = DomObjectWrap::new(3);
assert_eq!(wrap.internal_field_count(), 3);
for i in 0..3 {
assert!(wrap.get_internal_field(i).is_null());
}
}
#[test]
#[should_panic(expected = "exceeds MAX_INTERNAL_FIELDS")]
fn test_wrap_new_too_many_fields() {
let _wrap = DomObjectWrap::new(MAX_INTERNAL_FIELDS + 1);
}
#[test]
fn test_wrap_set_get_internal_field() {
let mut wrap = DomObjectWrap::new(2);
let sentinel: usize = 0xDEAD_BEEF;
wrap.set_internal_field(0, sentinel as *mut c_void);
assert_eq!(wrap.get_internal_field(0) as usize, sentinel);
assert!(wrap.get_internal_field(1).is_null());
}
#[test]
#[should_panic(expected = "out of range")]
fn test_wrap_set_internal_field_out_of_range() {
let mut wrap = DomObjectWrap::new(1);
wrap.set_internal_field(1, std::ptr::null_mut());
}
#[test]
fn test_wrap_get_internal_field_out_of_range_returns_null() {
let wrap = DomObjectWrap::new(1);
assert!(wrap.get_internal_field(99).is_null());
}
#[test]
fn test_wrap_property_crud() {
let mut wrap = DomObjectWrap::new(0);
assert_eq!(wrap.get_property("x"), JsValue::Undefined);
assert!(!wrap.has_property("x"));
wrap.set_property("x", JsValue::Smi(42));
assert_eq!(wrap.get_property("x"), JsValue::Smi(42));
assert!(wrap.has_property("x"));
assert!(wrap.delete_property("x"));
assert!(!wrap.has_property("x"));
}
#[test]
fn test_wrap_property_names_sorted() {
let mut wrap = DomObjectWrap::new(0);
wrap.set_property("z", JsValue::Smi(1));
wrap.set_property("a", JsValue::Smi(2));
wrap.set_property("m", JsValue::Smi(3));
assert_eq!(wrap.property_names(), vec!["a", "m", "z"]);
}
#[test]
fn test_wrap_named_getter_interceptor() {
let mut wrap = DomObjectWrap::new(1);
wrap.set_named_handler(
NamedPropertyHandlerConfig::builder()
.getter(|name, _data| {
if name == "id" {
Some(JsValue::String("my-div".into()))
} else {
None
}
})
.build(),
);
assert_eq!(wrap.get_property("id"), JsValue::String("my-div".into()));
assert_eq!(wrap.get_property("class"), JsValue::Undefined);
}
#[test]
fn test_wrap_named_setter_interceptor() {
let intercepted = Rc::new(Cell::new(false));
let flag = Rc::clone(&intercepted);
let mut wrap = DomObjectWrap::new(1);
wrap.set_named_handler(
NamedPropertyHandlerConfig::builder()
.setter(move |name, _val, _data| {
if name == "id" {
flag.set(true);
true } else {
false
}
})
.build(),
);
wrap.set_property("id", JsValue::String("new-id".into()));
assert!(intercepted.get());
assert!(!wrap.properties.borrow().contains_key("id"));
wrap.set_property("class", JsValue::String("foo".into()));
assert!(wrap.properties.borrow().contains_key("class"));
}
#[test]
fn test_wrap_named_query_interceptor() {
let mut wrap = DomObjectWrap::new(1);
wrap.set_named_handler(
NamedPropertyHandlerConfig::builder()
.query(|name, _data| {
if name == "id" {
Some(0) } else {
None
}
})
.build(),
);
assert!(wrap.has_property("id"));
assert!(!wrap.has_property("class"));
}
#[test]
fn test_wrap_named_enumerator() {
let mut wrap = DomObjectWrap::new(1);
wrap.set_property("ownProp", JsValue::Smi(1));
wrap.set_named_handler(
NamedPropertyHandlerConfig::builder()
.enumerator(|_data| vec!["id".to_string(), "className".to_string()])
.build(),
);
let names = wrap.property_names();
assert_eq!(names, vec!["id", "className", "ownProp"]);
}
#[test]
fn test_wrap_indexed_getter() {
let items = vec![JsValue::Smi(10), JsValue::Smi(20), JsValue::Smi(30)];
let items_clone = items.clone();
let mut wrap = DomObjectWrap::new(1);
wrap.set_indexed_handler(
IndexedPropertyHandlerConfig::builder()
.getter(move |idx, _data| items_clone.get(idx as usize).cloned())
.length(|_data| 3)
.build(),
);
assert_eq!(wrap.get_indexed(0), JsValue::Smi(10));
assert_eq!(wrap.get_indexed(2), JsValue::Smi(30));
assert_eq!(wrap.get_indexed(3), JsValue::Undefined);
assert_eq!(wrap.indexed_length(), 3);
}
#[test]
fn test_wrap_indexed_setter() {
let stored = Rc::new(RefCell::new(Vec::<(u32, JsValue)>::new()));
let stored_clone = Rc::clone(&stored);
let mut wrap = DomObjectWrap::new(1);
wrap.set_indexed_handler(
IndexedPropertyHandlerConfig::builder()
.setter(move |idx, val, _data| {
stored_clone.borrow_mut().push((idx, val.clone()));
true
})
.build(),
);
assert!(wrap.set_indexed(0, &JsValue::Smi(99)));
assert_eq!(stored.borrow().len(), 1);
assert_eq!(stored.borrow()[0], (0, JsValue::Smi(99)));
}
#[test]
fn test_wrap_no_indexed_handler() {
let wrap = DomObjectWrap::new(0);
assert_eq!(wrap.get_indexed(0), JsValue::Undefined);
assert_eq!(wrap.indexed_length(), 0);
}
#[test]
fn test_wrap_as_js_value_shares_properties() {
let mut wrap = DomObjectWrap::new(0);
wrap.set_property("x", JsValue::Smi(1));
let jv = wrap.as_js_value();
if let JsValue::PlainObject(map) = &jv {
assert_eq!(map.borrow().get("x"), Some(&JsValue::Smi(1)));
map.borrow_mut().insert("y".to_string(), JsValue::Smi(2));
} else {
panic!("expected PlainObject");
}
assert_eq!(wrap.get_property("y"), JsValue::Smi(2));
}
#[test]
fn test_wrap_debug() {
let wrap = DomObjectWrap::new(2);
let s = format!("{wrap:?}");
assert!(s.contains("DomObjectWrap"));
assert!(s.contains("internal_field_count: 2"));
}
#[test]
fn test_weak_ref_invoke() {
let invoked = Rc::new(Cell::new(false));
let flag = Rc::clone(&invoked);
let wrap = DomObjectWrap::new(1);
let weak = DomWeakRef::new(&wrap, move |_ptr| {
flag.set(true);
});
assert!(weak.is_alive());
weak.invoke_callback();
assert!(!weak.is_alive());
assert!(invoked.get());
}
#[test]
fn test_weak_ref_invoke_idempotent() {
let count = Rc::new(Cell::new(0u32));
let ctr = Rc::clone(&count);
let wrap = DomObjectWrap::new(1);
let weak = DomWeakRef::new(&wrap, move |_ptr| {
ctr.set(ctr.get() + 1);
});
weak.invoke_callback();
weak.invoke_callback(); assert_eq!(count.get(), 1);
}
#[test]
fn test_weak_ref_clear() {
let invoked = Rc::new(Cell::new(false));
let flag = Rc::clone(&invoked);
let wrap = DomObjectWrap::new(1);
let weak = DomWeakRef::new(&wrap, move |_ptr| {
flag.set(true);
});
weak.clear();
assert!(!weak.is_alive());
weak.invoke_callback(); assert!(!invoked.get());
}
#[test]
fn test_weak_ref_captures_data_pointer() {
let sentinel: usize = 0xCAFE;
let mut wrap = DomObjectWrap::new(1);
wrap.set_internal_field(0, sentinel as *mut c_void);
let captured = Rc::new(Cell::new(0usize));
let cap = Rc::clone(&captured);
let weak = DomWeakRef::new(&wrap, move |ptr| {
cap.set(ptr as usize);
});
assert_eq!(weak.data() as usize, sentinel);
weak.invoke_callback();
assert_eq!(captured.get(), sentinel);
}
#[test]
fn test_weak_ref_debug() {
let wrap = DomObjectWrap::new(0);
let weak = DomWeakRef::new(&wrap, |_| {});
let s = format!("{weak:?}");
assert!(s.contains("DomWeakRef"));
assert!(s.contains("alive: true"));
}
#[test]
fn test_named_handler_config_builder_empty() {
let cfg = NamedPropertyHandlerConfig::builder().build();
assert!(cfg.getter().is_none());
assert!(cfg.setter().is_none());
assert!(cfg.query().is_none());
assert!(cfg.deleter().is_none());
assert!(cfg.enumerator().is_none());
}
#[test]
fn test_named_handler_config_debug() {
let cfg = NamedPropertyHandlerConfig::builder()
.getter(|_, _| None)
.build();
let s = format!("{cfg:?}");
assert!(s.contains("has_getter: true"));
assert!(s.contains("has_setter: false"));
}
#[test]
fn test_indexed_handler_config_builder_empty() {
let cfg = IndexedPropertyHandlerConfig::builder().build();
assert!(cfg.getter().is_none());
assert!(cfg.setter().is_none());
assert!(cfg.query().is_none());
assert!(cfg.length().is_none());
}
#[test]
fn test_indexed_handler_config_debug() {
let cfg = IndexedPropertyHandlerConfig::builder()
.getter(|_, _| None)
.length(|_| 0)
.build();
let s = format!("{cfg:?}");
assert!(s.contains("has_getter: true"));
assert!(s.contains("has_length: true"));
}
}