use std::{ffi::CString, ptr};
use crate::{sys, JSClass, JSContext, JSException, JSObject, JSValue};
use thiserror::Error;
#[derive(Debug, Error)]
enum JSClassError {
#[error("classname was invalid (e.g. it contains a NULL character)")]
InvalidName,
#[error("class could not be created")]
FailedToCreateClass,
#[error("class could not be retained")]
FailedToRetainClass,
}
impl JSClass {
pub fn builder<N>(ctx: &JSContext, name: N) -> Result<JSClassBuilder, JSException>
where
N: Into<Vec<u8>>,
{
let Ok(name) = CString::new(name) else {
return Err(JSValue::new_string(ctx, JSClassError::InvalidName.to_string()).into());
};
let class_definition = sys::JSClassDefinition {
className: name.as_ptr(),
..Default::default()
};
Ok(JSClassBuilder {
ctx,
name,
class_definition,
})
}
unsafe fn from_raw(ctx: sys::JSContextRef, raw: sys::JSClassRef, name: CString) -> Self {
Self { ctx, raw, name }
}
pub fn new_object(&self) -> JSObject {
unsafe {
JSObject::from_raw(
self.ctx,
sys::JSObjectMake(self.ctx, self.raw, ptr::null_mut()),
)
}
}
}
impl Drop for JSClass {
fn drop(&mut self) {
unsafe { sys::JSClassRelease(self.raw) }
}
}
#[must_use]
pub struct JSClassBuilder<'a> {
ctx: &'a JSContext,
name: CString,
class_definition: sys::JSClassDefinition,
}
impl<'a> JSClassBuilder<'a> {
pub fn constructor(mut self, constructor: sys::JSObjectCallAsConstructorCallback) -> Self {
self.class_definition.callAsConstructor = constructor;
self
}
pub fn build(self) -> Result<JSClass, JSException> {
let class = unsafe { sys::JSClassCreate(&self.class_definition) };
if class.is_null() {
return Err(JSValue::new_string(
self.ctx,
JSClassError::FailedToCreateClass.to_string(),
)
.into());
}
let class = unsafe { sys::JSClassRetain(class) };
if class.is_null() {
return Err(JSValue::new_string(
self.ctx,
JSClassError::FailedToRetainClass.to_string(),
)
.into());
}
Ok(unsafe { JSClass::from_raw(self.ctx.raw, class, self.name) })
}
}
#[cfg(test)]
mod tests {
use crate::{constructor_callback, evaluate_script, function_callback};
use super::*;
#[test]
fn class_with_no_constructor() -> Result<(), JSException> {
let ctx = JSContext::default();
let class = JSClass::builder(&ctx, "Foo")?.build()?;
let object = class.new_object();
assert!(object.is_object_of_class(&class));
assert!(object.call_as_constructor(&[]).is_err());
Ok(())
}
#[test]
fn class_with_constructor() -> Result<(), JSException> {
use crate as javascriptcore;
#[constructor_callback]
fn foo_ctor(
ctx: &JSContext,
constructor: &JSObject,
_arguments: &[JSValue],
) -> Result<JSValue, JSException> {
#[function_callback]
fn bar(
ctx: &JSContext,
_function: Option<&JSObject>,
_this_object: Option<&JSObject>,
_arguments: &[JSValue],
) -> Result<JSValue, JSException> {
Ok(JSValue::new_number(ctx, 42.))
}
constructor.set_property("bar", JSValue::new_function(ctx, "bar", Some(bar)))?;
Ok(constructor.into())
}
let ctx = JSContext::default();
let class = JSClass::builder(&ctx, "Foo")?
.constructor(Some(foo_ctor))
.build()?;
let object = class.new_object();
assert!(object.is_object_of_class(&class));
let object = object.call_as_constructor(&[])?.as_object()?;
assert_eq!(
object
.get_property("bar")
.as_object()?
.call_as_function(None, &[])?
.as_number()?,
42.
);
let global_object = ctx.global_object()?;
global_object.set_property("Foo", object.into())?;
let result = evaluate_script(&ctx, "const foo = new Foo(); foo.bar()", None, "test.js", 1);
assert_eq!(result?.as_number()?, 42.);
Ok(())
}
}