boa/
class.rs

1//! Traits and structs for implementing native classes.
2//!
3//! Native classes are implemented through the [`Class`][class-trait] trait.
4//! ```
5//!# use boa::{
6//!#    property::Attribute,
7//!#    class::{Class, ClassBuilder},
8//!#    gc::{Finalize, Trace},
9//!#    Context, JsResult, JsValue,
10//!#    builtins::JsArgs,
11//!# };
12//!#
13//! // This does not have to be an enum it can also be a struct.
14//! #[derive(Debug, Trace, Finalize)]
15//! enum Animal {
16//!     Cat,
17//!     Dog,
18//!     Other,
19//! }
20//!
21//! impl Class for Animal {
22//!     // we set the binging name of this function to be `"Animal"`.
23//!     const NAME: &'static str = "Animal";
24//!
25//!     // We set the length to `1` since we accept 1 arguments in the constructor.
26//!     const LENGTH: usize = 1;
27//!
28//!     // This is what is called when we do `new Animal()`
29//!     fn constructor(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<Self> {
30//!         // This is equivalent to `String(arg)`.
31//!         let kind = args.get_or_undefined(0).to_string(context)?;
32//!
33//!         let animal = match kind.as_str() {
34//!             "cat" => Self::Cat,
35//!             "dog" => Self::Dog,
36//!             _ => Self::Other,
37//!         };
38//!
39//!         Ok(animal)
40//!     }
41//!
42//!     /// This is where the object is intitialized.
43//!     fn init(class: &mut ClassBuilder) -> JsResult<()> {
44//!         class.method("speak", 0, |this, _args, _ctx| {
45//!             if let Some(object) = this.as_object() {
46//!                 if let Some(animal) = object.downcast_ref::<Animal>() {
47//!                     match &*animal {
48//!                         Self::Cat => println!("meow"),
49//!                         Self::Dog => println!("woof"),
50//!                         Self::Other => println!(r"¯\_(ツ)_/¯"),
51//!                     }
52//!                 }
53//!             }
54//!             Ok(JsValue::undefined())
55//!         });
56//!
57//!         Ok(())
58//!     }
59//! }
60//! ```
61//!
62//! [class-trait]: ./trait.Class.html
63
64use crate::{
65    builtins::function::NativeFunction,
66    object::{ConstructorBuilder, JsObject, NativeObject, ObjectData, PROTOTYPE},
67    property::{Attribute, PropertyDescriptor, PropertyKey},
68    Context, JsResult, JsValue,
69};
70
71/// Native class.
72pub trait Class: NativeObject + Sized {
73    /// The binding name of the object.
74    const NAME: &'static str;
75    /// The amount of arguments the class `constructor` takes, default is `0`.
76    const LENGTH: usize = 0;
77    /// The attibutes the class will be binded with, default is `writable`, `enumerable`, `configurable`.
78    const ATTRIBUTES: Attribute = Attribute::all();
79
80    /// The constructor of the class.
81    fn constructor(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<Self>;
82
83    /// Initializes the internals and the methods of the class.
84    fn init(class: &mut ClassBuilder<'_>) -> JsResult<()>;
85}
86
87/// This is a wrapper around `Class::constructor` that sets the internal data of a class.
88///
89/// This is automatically implemented, when a type implements `Class`.
90pub trait ClassConstructor: Class {
91    /// The raw constructor that matches the `NativeFunction` signature.
92    fn raw_constructor(
93        this: &JsValue,
94        args: &[JsValue],
95        context: &mut Context,
96    ) -> JsResult<JsValue>
97    where
98        Self: Sized;
99}
100
101impl<T: Class> ClassConstructor for T {
102    fn raw_constructor(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue>
103    where
104        Self: Sized,
105    {
106        if this.is_undefined() {
107            return context.throw_type_error(format!(
108                "cannot call constructor of native class `{}` without new",
109                T::NAME
110            ));
111        }
112
113        let class_constructor =
114            if let Some(obj) = context.global_object().get(T::NAME, context)?.as_object() {
115                obj
116            } else {
117                return context.throw_type_error(format!(
118                    "invalid constructor for native class `{}` ",
119                    T::NAME
120                ));
121            };
122        let class_prototype =
123            if let Some(obj) = class_constructor.get(PROTOTYPE, context)?.as_object() {
124                obj
125            } else {
126                return context.throw_type_error(format!(
127                    "invalid default prototype for native class `{}`",
128                    T::NAME
129                ));
130            };
131
132        let prototype = this
133            .as_object()
134            .and_then(|obj| {
135                obj.get(PROTOTYPE, context)
136                    .map(|o| o.as_object())
137                    .transpose()
138            })
139            .transpose()?
140            .unwrap_or(class_prototype);
141
142        let native_instance = Self::constructor(this, args, context)?;
143        let object_instance = context.construct_object();
144        object_instance.set_prototype_instance(prototype.into());
145        object_instance.borrow_mut().data = ObjectData::native_object(Box::new(native_instance));
146        Ok(object_instance.into())
147    }
148}
149
150/// Class builder which allows adding methods and static methods to the class.
151#[derive(Debug)]
152pub struct ClassBuilder<'context> {
153    builder: ConstructorBuilder<'context>,
154}
155
156impl<'context> ClassBuilder<'context> {
157    #[inline]
158    pub(crate) fn new<T>(context: &'context mut Context) -> Self
159    where
160        T: ClassConstructor,
161    {
162        let mut builder = ConstructorBuilder::new(context, T::raw_constructor);
163        builder.name(T::NAME);
164        builder.length(T::LENGTH);
165        Self { builder }
166    }
167
168    #[inline]
169    pub(crate) fn build(mut self) -> JsObject {
170        self.builder.build()
171    }
172
173    /// Add a method to the class.
174    ///
175    /// It is added to `prototype`.
176    #[inline]
177    pub fn method<N>(&mut self, name: N, length: usize, function: NativeFunction) -> &mut Self
178    where
179        N: AsRef<str>,
180    {
181        self.builder.method(function, name.as_ref(), length);
182        self
183    }
184
185    /// Add a static method to the class.
186    ///
187    /// It is added to class object itself.
188    #[inline]
189    pub fn static_method<N>(
190        &mut self,
191        name: N,
192        length: usize,
193        function: NativeFunction,
194    ) -> &mut Self
195    where
196        N: AsRef<str>,
197    {
198        self.builder.static_method(function, name.as_ref(), length);
199        self
200    }
201
202    /// Add a data property to the class, with the specified attribute.
203    ///
204    /// It is added to `prototype`.
205    #[inline]
206    pub fn property<K, V>(&mut self, key: K, value: V, attribute: Attribute) -> &mut Self
207    where
208        K: Into<PropertyKey>,
209        V: Into<JsValue>,
210    {
211        self.builder.property(key, value, attribute);
212        self
213    }
214
215    /// Add a static data property to the class, with the specified attribute.
216    ///
217    /// It is added to class object itself.
218    #[inline]
219    pub fn static_property<K, V>(&mut self, key: K, value: V, attribute: Attribute) -> &mut Self
220    where
221        K: Into<PropertyKey>,
222        V: Into<JsValue>,
223    {
224        self.builder.static_property(key, value, attribute);
225        self
226    }
227
228    /// Add an accessor property to the class, with the specified attribute.
229    ///
230    /// It is added to `prototype`.
231    #[inline]
232    pub fn accessor<K>(
233        &mut self,
234        key: K,
235        get: Option<JsObject>,
236        set: Option<JsObject>,
237        attribute: Attribute,
238    ) -> &mut Self
239    where
240        K: Into<PropertyKey>,
241    {
242        self.builder.accessor(key, get, set, attribute);
243        self
244    }
245
246    /// Add a static accessor property to the class, with the specified attribute.
247    ///
248    /// It is added to class object itself.
249    #[inline]
250    pub fn static_accessor<K>(
251        &mut self,
252        key: K,
253        get: Option<JsObject>,
254        set: Option<JsObject>,
255        attribute: Attribute,
256    ) -> &mut Self
257    where
258        K: Into<PropertyKey>,
259    {
260        self.builder.static_accessor(key, get, set, attribute);
261        self
262    }
263
264    /// Add a property descriptor to the class, with the specified attribute.
265    ///
266    /// It is added to `prototype`.
267    #[inline]
268    pub fn property_descriptor<K, P>(&mut self, key: K, property: P) -> &mut Self
269    where
270        K: Into<PropertyKey>,
271        P: Into<PropertyDescriptor>,
272    {
273        self.builder.property_descriptor(key, property);
274        self
275    }
276
277    /// Add a static property descriptor to the class, with the specified attribute.
278    ///
279    /// It is added to class object itself.
280    #[inline]
281    pub fn static_property_descriptor<K, P>(&mut self, key: K, property: P) -> &mut Self
282    where
283        K: Into<PropertyKey>,
284        P: Into<PropertyDescriptor>,
285    {
286        self.builder.static_property_descriptor(key, property);
287        self
288    }
289
290    /// Return the current context.
291    #[inline]
292    pub fn context(&mut self) -> &'_ mut Context {
293        self.builder.context()
294    }
295}