1use std::ffi::CString;
2
3use rust_jsc_sys::{
4 kJSClassDefinitionEmpty, JSClassCreate, JSClassDefinition, JSClassRelease,
5 JSClassRetain, JSObjectCallAsConstructorCallback, JSObjectCallAsFunctionCallback,
6 JSObjectConvertToTypeCallback, JSObjectDeletePropertyCallback,
7 JSObjectFinalizeCallback, JSObjectGetPropertyCallback,
8 JSObjectGetPropertyNamesCallback, JSObjectHasInstanceCallback,
9 JSObjectHasPropertyCallback, JSObjectInitializeCallback, JSObjectMake,
10 JSObjectSetPropertyCallback,
11};
12
13use crate::{JSClass, JSContext, JSObject, JSResult};
14
15#[derive(Debug)]
16pub enum ClassError {
17 CreateFailed,
18 RetainFailed,
19}
20
21pub struct JSClassBuilder {
22 definition: JSClassDefinition,
23 name: String,
24}
25
26impl JSClassBuilder {
27 pub fn new(name: &str) -> Self {
28 let mut definition = unsafe { kJSClassDefinitionEmpty };
29
30 let class_name = CString::new(name).unwrap();
31 definition.className = class_name.as_ptr();
32 Self {
33 definition,
34 name: name.to_string(),
35 }
36 }
37
38 pub fn set_version(mut self, version: u32) -> Self {
39 self.definition.version = version as i32;
40 self
41 }
42
43 pub fn set_attributes(mut self, attributes: u32) -> Self {
44 self.definition.attributes = attributes;
45 self
46 }
47
48 pub fn parent_class(mut self, parent_class: &JSClass) -> Self {
49 self.definition.parentClass = parent_class.inner;
50 self
51 }
52
53 pub fn set_initialize(mut self, initialize: JSObjectInitializeCallback) -> Self {
57 self.definition.initialize = initialize;
58 self
59 }
60
61 pub fn set_finalize(mut self, finalize: JSObjectFinalizeCallback) -> Self {
62 self.definition.finalize = finalize;
63 self
64 }
65
66 pub fn has_property(mut self, has_property: JSObjectHasPropertyCallback) -> Self {
67 self.definition.hasProperty = has_property;
68 self
69 }
70
71 pub fn get_property(mut self, get_property: JSObjectGetPropertyCallback) -> Self {
72 self.definition.getProperty = get_property;
73 self
74 }
75
76 pub fn set_property(mut self, set_property: JSObjectSetPropertyCallback) -> Self {
77 self.definition.setProperty = set_property;
78 self
79 }
80
81 pub fn delete_property(
82 mut self,
83 delete_property: JSObjectDeletePropertyCallback,
84 ) -> Self {
85 self.definition.deleteProperty = delete_property;
86 self
87 }
88
89 pub fn get_property_names(
90 mut self,
91 get_property_names: JSObjectGetPropertyNamesCallback,
92 ) -> Self {
93 self.definition.getPropertyNames = get_property_names;
94 self
95 }
96
97 pub fn call_as_function(
98 mut self,
99 call_as_function: JSObjectCallAsFunctionCallback,
100 ) -> Self {
101 self.definition.callAsFunction = call_as_function;
102 self
103 }
104
105 pub fn call_as_constructor(
106 mut self,
107 call_as_constructor: JSObjectCallAsConstructorCallback,
108 ) -> Self {
109 self.definition.callAsConstructor = call_as_constructor;
110 self
111 }
112
113 pub fn has_instance(mut self, has_instance: JSObjectHasInstanceCallback) -> Self {
114 self.definition.hasInstance = has_instance;
115 self
116 }
117
118 pub fn convert_to_type(
119 mut self,
120 convert_to_type: JSObjectConvertToTypeCallback,
121 ) -> Self {
122 self.definition.convertToType = convert_to_type;
123 self
124 }
125
126 pub fn build(self) -> Result<JSClass, ClassError> {
127 let class = unsafe { JSClassCreate(&self.definition) };
128 if class.is_null() {
129 return Err(ClassError::CreateFailed);
130 }
131
132 let class = unsafe { JSClassRetain(class) };
133 if class.is_null() {
134 return Err(ClassError::RetainFailed);
135 }
136
137 Ok(JSClass {
138 inner: class,
139 name: self.name,
140 })
141 }
142}
143
144impl JSClass {
145 pub fn builder(name: &str) -> JSClassBuilder {
206 JSClassBuilder::new(name)
207 }
208
209 pub fn name(&self) -> &str {
210 &self.name
211 }
212
213 pub fn object<T>(&self, ctx: &JSContext, data: Option<Box<T>>) -> JSObject {
238 let data_ptr = if let Some(data) = data {
239 Box::into_raw(data) as *mut std::ffi::c_void
240 } else {
241 std::ptr::null_mut()
242 };
243
244 let inner = unsafe { JSObjectMake(ctx.inner, self.inner, data_ptr) };
245 JSObject::from_ref(inner, ctx.inner)
246 }
247
248 pub fn register(&self, ctx: &JSContext) -> JSResult<()> {
284 ctx.global_object().set_property(
285 self.name(),
286 &self.object::<()>(ctx, None),
287 Default::default(),
288 )
289 }
290}
291
292impl Drop for JSClass {
293 fn drop(&mut self) {
294 unsafe { JSClassRelease(self.inner) };
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use crate::{self as rust_jsc, PrivateData};
301 use rust_jsc_macros::{constructor, finalize, has_instance, initialize};
302
303 use crate::{JSClass, JSClassAttribute, JSContext, JSObject, JSResult, JSValue};
304
305 #[test]
306 fn test_class_builder() {
307 #[constructor]
308 fn constructor(
309 _ctx: JSContext,
310 this: JSObject,
311 _arguments: &[JSValue],
312 ) -> JSResult<JSValue> {
313 let value = JSValue::string(&_ctx, "John");
314 this.set_property("name", &value, Default::default())
315 .unwrap();
316 Ok(this.into())
317 }
318
319 let ctx = JSContext::default();
320 let class = JSClass::builder("Test")
321 .set_version(1)
322 .set_attributes(JSClassAttribute::None.into())
323 .set_initialize(None)
324 .set_finalize(None)
325 .has_property(None)
326 .get_property(None)
327 .set_property(None)
328 .delete_property(None)
329 .get_property_names(None)
330 .call_as_function(None)
331 .call_as_constructor(Some(constructor))
332 .has_instance(None)
333 .convert_to_type(None)
334 .build()
335 .unwrap();
336
337 let object = class.object::<i32>(&ctx, Some(Box::new(42)));
338
339 ctx.global_object()
340 .set_property("Test", &object, Default::default())
341 .unwrap();
342 let result_object = ctx
343 .evaluate_script("const obj = new Test(); obj", None)
344 .unwrap();
345
346 assert!(result_object.is_object_of_class(&class).unwrap());
347 assert!(object.is_object());
348 let object = object.as_object().unwrap();
349 assert!(object.has_property("name"));
350 assert_eq!(
351 object.get_property("name").unwrap(),
352 JSValue::string(&ctx, "John")
353 );
354 }
355
356 #[test]
357 fn test_class_register() {
358 #[constructor]
359 fn constructor(
360 _ctx: JSContext,
361 this: JSObject,
362 _arguments: &[JSValue],
363 ) -> JSResult<JSValue> {
364 let value = JSValue::string(&_ctx, "John");
365 this.set_property("name", &value, Default::default())
366 .unwrap();
367 Ok(this.into())
368 }
369
370 let ctx = JSContext::default();
371 let class = JSClass::builder("Test")
372 .set_version(1)
373 .set_attributes(JSClassAttribute::None.into())
374 .set_initialize(None)
375 .set_finalize(None)
376 .has_property(None)
377 .get_property(None)
378 .set_property(None)
379 .delete_property(None)
380 .get_property_names(None)
381 .call_as_function(None)
382 .call_as_constructor(Some(constructor))
383 .has_instance(None)
384 .convert_to_type(None)
385 .build()
386 .unwrap();
387
388 class.register(&ctx).unwrap();
389 let result_object = ctx
390 .evaluate_script("const obj = new Test(); obj", None)
391 .unwrap();
392
393 assert!(result_object.is_object_of_class(&class).unwrap());
394 }
395
396 #[test]
397 fn test_class_without_constructor() {
398 let ctx = JSContext::default();
399 let class = JSClass::builder("Test")
400 .set_version(1)
401 .set_attributes(JSClassAttribute::None.into())
402 .set_initialize(None)
403 .set_finalize(None)
404 .has_property(None)
405 .get_property(None)
406 .set_property(None)
407 .delete_property(None)
408 .get_property_names(None)
409 .call_as_function(None)
410 .call_as_constructor(None)
411 .has_instance(None)
412 .convert_to_type(None)
413 .build()
414 .unwrap();
415
416 class.register(&ctx).unwrap();
417 let result = ctx.evaluate_script("const obj = new Test(); obj", None);
418
419 assert!(result.is_err());
420
421 let error = result.unwrap_err();
422 assert_eq!(error.name().unwrap(), "TypeError");
423 }
424
425 #[test]
426 fn test_class_initialize() {
427 #[constructor]
428 fn constructor(
429 _ctx: JSContext,
430 this: JSObject,
431 _arguments: &[JSValue],
432 ) -> JSResult<JSValue> {
433 println!("Constructor");
434 let value = JSValue::string(&_ctx, "John");
435 this.set_property("name", &value, Default::default())
436 .unwrap();
437 Ok(this.into())
438 }
439
440 #[initialize]
441 fn initialize(_ctx: JSContext, _object: JSObject) {
442 println!("Initialize");
443 }
444
445 #[finalize]
446 fn finalize(_data_ptr: PrivateData) {
447 println!("Finalize");
448 }
449
450 #[has_instance]
451 fn has_instance(
452 _ctx: JSContext,
453 _constructor: JSObject,
454 _instance: JSValue,
455 ) -> JSResult<bool> {
456 println!("Has instance");
457 let name = _constructor
458 .get_property("name")
459 .unwrap()
460 .as_string()
461 .unwrap();
462
463 println!("Name: {}", name);
464 if name == "John" {
465 Ok(true)
466 } else {
467 Ok(false)
468 }
469 }
470
471 let ctx = JSContext::default();
472 let class = JSClass::builder("Test")
473 .set_version(1)
474 .set_attributes(JSClassAttribute::None.into())
475 .set_initialize(Some(initialize))
476 .set_finalize(Some(finalize))
477 .call_as_function(None)
478 .call_as_constructor(Some(constructor))
479 .has_instance(Some(has_instance))
480 .build()
481 .unwrap();
482
483 class.register(&ctx).unwrap();
484 let result = ctx
485 .evaluate_script(
486 r#"
487 let obj = new Test();
488 obj instanceof Test;
489 "#,
490 None,
491 )
492 .unwrap();
493
494 assert!(result.is_boolean());
495 assert_eq!(result.as_boolean(), true);
496
497 let object = ctx.evaluate_script("obj", None).unwrap();
498 assert!(object.is_object_of_class(&class).unwrap());
499
500 let object = object.as_object().unwrap();
501 let object_data = Box::new(42);
502 let result = object.set_private_data(object_data);
503 assert!(result);
504 assert_eq!(*object.get_private_data::<i32>().unwrap(), 42);
505 }
506}