rust_jsc/
class.rs

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    /// TODO: implement static values
54    /// TODO: implement static functions
55
56    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    /// Creates a new class builder.
146    ///
147    /// # Arguments
148    /// - `name`: The name of the class.
149    ///
150    /// # Example
151    /// ```rust,ignore
152    /// use rust_jsc::{JSClass, JSClassBuilder};
153    ///
154    /// let builder = JSClass::builder("Test");
155    ///
156    /// let class = builder
157    ///     .set_version(1)
158    ///     .set_attributes(JSClassAttribute::None.into())
159    ///     .set_initialize(None)
160    ///     .build()
161    ///     .expect("Failed to create class");
162    /// ```
163    ///
164    /// With constructor:
165    ///
166    /// ```rust,ignore
167    /// use rust_jsc_macros::constructor;
168    /// use rust_jsc::{JSClass, JSClassBuilder, JSClassAttribute, JSResult, JSValue, JSObject, JSContext};
169    ///
170    /// #[constructor]
171    /// fn constructor(
172    ///    _ctx: JSContext,
173    ///   this: JSObject,
174    ///  _arguments: &[JSValue],
175    /// ) -> JSResult<JSValue> {
176    ///    let value = JSValue::string(&_ctx, "John");
177    ///   this.set_property(&"name".into(), &value, Default::default())
178    ///      .unwrap();
179    ///
180    ///   Ok(this.into())
181    /// }
182    ///
183    /// let builder = JSClass::builder("Test");
184    ///
185    /// let class = builder
186    ///    .set_version(1)
187    ///    .set_attributes(JSClassAttribute::None.into())
188    ///    .set_initialize(None)
189    ///    .set_finalize(None)
190    ///    .has_property(None)
191    ///    .get_property(None)
192    ///    .set_property(None)
193    ///    .delete_property(None)
194    ///    .get_property_names(None)
195    ///    .call_as_function(None)
196    ///    .call_as_constructor(Some(constructor))
197    ///    .has_instance(None)
198    ///    .convert_to_type(None)
199    ///    .build()
200    ///    .expect("Failed to create class");
201    /// ```
202    ///
203    /// # Returns
204    /// A new class builder.
205    pub fn builder(name: &str) -> JSClassBuilder {
206        JSClassBuilder::new(name)
207    }
208
209    pub fn name(&self) -> &str {
210        &self.name
211    }
212
213    /// Creates a new object of the class.
214    /// The object will be created in the given context.
215    /// The object will have the given data associated with it.
216    /// The data will be passed to the initialize callback.
217    ///
218    /// # Arguments
219    /// - `ctx`: The JavaScript context to create the object in.
220    /// - `data`: The data to associate with the object.
221    ///
222    /// # Example
223    /// ```
224    /// use rust_jsc::{JSClass, JSContext};
225    ///
226    /// let ctx = JSContext::default();
227    /// let class = JSClass::builder("Test")
228    ///    .set_version(1)
229    ///     .build()
230    ///    .unwrap();
231    ///
232    /// let object = class.object::<i32>(&ctx, Some(Box::new(42)));
233    /// ```
234    ///
235    /// # Returns
236    /// A new object of the class.
237    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    /// Registers the class in the global object.
249    /// This will make the class available in JavaScript.
250    /// The class will be available as a constructor function.
251    /// The class name will be the same as the class name in Rust.
252    ///
253    /// # Arguments
254    /// - `ctx`: The JavaScript context to register the class in.
255    ///
256    /// # Example
257    /// ```
258    /// use rust_jsc::{JSClass, JSContext, JSClassAttribute};
259    ///
260    /// let ctx = JSContext::default();
261    /// let class = JSClass::builder("Test")
262    ///     .set_version(1)
263    ///     .set_attributes(JSClassAttribute::None.into())
264    ///     .set_initialize(None)
265    ///     .set_finalize(None)
266    ///     .has_property(None)
267    ///     .get_property(None)
268    ///     .set_property(None)
269    ///     .delete_property(None)
270    ///     .get_property_names(None)
271    ///     .call_as_function(None)
272    ///     .call_as_constructor(None)
273    ///     .has_instance(None)
274    ///     .convert_to_type(None)
275    ///     .build()
276    ///     .unwrap();
277    ///
278    /// class.register(&ctx).unwrap();
279    /// ```
280    ///
281    /// # Errors
282    /// If an error occurs while registering the class.
283    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}