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}