boa_engine/interop/into_js_arguments.rs
1use boa_engine::value::TryFromJs;
2use boa_engine::{Context, JsNativeError, JsObject, JsResult, JsValue, NativeObject};
3use boa_gc::{GcRef, GcRefMut};
4use std::ops::Deref;
5
6/// Create a Rust value from a JS argument. This trait is used to
7/// convert arguments from JS to Rust types. It allows support
8/// for optional arguments or rest arguments.
9pub trait TryFromJsArgument<'a>: Sized {
10 /// Try to convert a JS argument into a Rust value, returning the
11 /// value and the rest of the arguments to be parsed.
12 ///
13 /// # Errors
14 /// Any parsing errors that may occur during the conversion.
15 fn try_from_js_argument(
16 this: &'a JsValue,
17 rest: &'a [JsValue],
18 context: &mut Context,
19 ) -> JsResult<(Self, &'a [JsValue])>;
20}
21
22impl<'a, T: TryFromJs> TryFromJsArgument<'a> for T {
23 fn try_from_js_argument(
24 _: &'a JsValue,
25 rest: &'a [JsValue],
26 context: &mut Context,
27 ) -> JsResult<(Self, &'a [JsValue])> {
28 match rest.split_first() {
29 Some((first, rest)) => Ok((first.try_js_into(context)?, rest)),
30 None => T::try_from_js(&JsValue::undefined(), context).map(|v| (v, rest)),
31 }
32 }
33}
34
35/// An argument that would be ignored in a JS function. This is equivalent of typing
36/// `()` in Rust functions argument, but more explicit.
37#[derive(Debug, Clone, Copy)]
38pub struct Ignore;
39
40impl<'a> TryFromJsArgument<'a> for Ignore {
41 fn try_from_js_argument(
42 _this: &'a JsValue,
43 rest: &'a [JsValue],
44 _: &mut Context,
45 ) -> JsResult<(Self, &'a [JsValue])> {
46 Ok((Ignore, &rest[1..]))
47 }
48}
49
50/// An argument that when used in a JS function will empty the list
51/// of JS arguments as `JsValue`s. This can be used for having the
52/// rest of the arguments in a function. It should be the last
53/// argument of your function, before the `Context` argument if any.
54///
55/// For example,
56/// ```
57/// # use boa_engine::{Context, JsValue, IntoJsFunctionCopied};
58/// # use boa_engine::interop::JsRest;
59/// # let mut context = Context::default();
60/// let sums = (|args: JsRest, context: &mut Context| -> i32 {
61/// args.iter()
62/// .map(|i| i.try_js_into::<i32>(context).unwrap())
63/// .sum::<i32>()
64/// })
65/// .into_js_function_copied(&mut context);
66///
67/// let result = sums
68/// .call(
69/// &JsValue::undefined(),
70/// &[JsValue::from(1), JsValue::from(2), JsValue::from(3)],
71/// &mut context,
72/// )
73/// .unwrap();
74/// assert_eq!(result, JsValue::new(6));
75/// ```
76#[derive(Debug, Clone)]
77pub struct JsRest<'a>(pub &'a [JsValue]);
78
79#[allow(unused)]
80impl<'a> JsRest<'a> {
81 /// Consumes the `JsRest` and returns the inner list of `JsValue`.
82 #[must_use]
83 pub fn into_inner(self) -> &'a [JsValue] {
84 self.0
85 }
86
87 /// Transforms the `JsRest` into a `Vec<JsValue>`.
88 #[must_use]
89 pub fn to_vec(self) -> Vec<JsValue> {
90 self.0.to_vec()
91 }
92
93 /// Returns an iterator over the inner list of `JsValue`.
94 pub fn iter(&self) -> impl Iterator<Item = &JsValue> {
95 self.0.iter()
96 }
97
98 /// Returns the length of the inner list of `JsValue`.
99 #[must_use]
100 pub fn len(&self) -> usize {
101 self.0.len()
102 }
103
104 /// Returns `true` if the inner list of `JsValue` is empty.
105 #[must_use]
106 pub fn is_empty(&self) -> bool {
107 self.0.is_empty()
108 }
109}
110
111impl<'a> From<&'a [JsValue]> for JsRest<'a> {
112 fn from(values: &'a [JsValue]) -> Self {
113 Self(values)
114 }
115}
116
117impl<'a> IntoIterator for JsRest<'a> {
118 type Item = &'a JsValue;
119 type IntoIter = std::slice::Iter<'a, JsValue>;
120
121 fn into_iter(self) -> Self::IntoIter {
122 self.into_inner().iter()
123 }
124}
125
126/// An argument that when used in a JS function will capture all
127/// the arguments that can be converted to `T`. The first argument
128/// that cannot be converted to `T` will stop the conversion.
129///
130/// For example,
131/// ```
132/// # use boa_engine::{Context, JsValue, IntoJsFunctionCopied};
133/// # use boa_engine::interop::JsAll;
134/// # let mut context = Context::default();
135/// let sums = (|args: JsAll<i32>, context: &mut Context| -> i32 {
136/// args.iter().sum()
137/// })
138/// .into_js_function_copied(&mut context);
139///
140/// let result = sums
141/// .call(
142/// &JsValue::undefined(),
143/// &[
144/// JsValue::from(1),
145/// JsValue::from(2),
146/// JsValue::from(3),
147/// JsValue::from(true),
148/// JsValue::from(4),
149/// ],
150/// &mut context,
151/// )
152/// .unwrap();
153/// assert_eq!(result, JsValue::new(6));
154/// ```
155#[derive(Debug, Clone)]
156pub struct JsAll<T: TryFromJs>(pub Vec<T>);
157
158impl<T: TryFromJs> JsAll<T> {
159 /// Consumes the `JsAll` and returns the inner list of `T`.
160 #[must_use]
161 pub fn into_inner(self) -> Vec<T> {
162 self.0
163 }
164
165 /// Returns an iterator over the inner list of `T`.
166 pub fn iter(&self) -> impl Iterator<Item = &T> {
167 self.0.iter()
168 }
169
170 /// Returns a mutable iterator over the inner list of `T`.
171 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
172 self.0.iter_mut()
173 }
174
175 /// Returns the length of the inner list of `T`.
176 #[must_use]
177 pub fn len(&self) -> usize {
178 self.0.len()
179 }
180
181 /// Returns `true` if the inner list of `T` is empty.
182 #[must_use]
183 pub fn is_empty(&self) -> bool {
184 self.0.is_empty()
185 }
186}
187
188impl<'a, T: TryFromJs> TryFromJsArgument<'a> for JsAll<T> {
189 fn try_from_js_argument(
190 _this: &'a JsValue,
191 mut rest: &'a [JsValue],
192 context: &mut Context,
193 ) -> JsResult<(Self, &'a [JsValue])> {
194 let mut values = Vec::new();
195
196 while !rest.is_empty() {
197 match rest[0].try_js_into(context) {
198 Ok(value) => {
199 values.push(value);
200 rest = &rest[1..];
201 }
202 Err(_) => break,
203 }
204 }
205 Ok((JsAll(values), rest))
206 }
207}
208
209/// Captures the `this` value in a JS function. Although this can be
210/// specified multiple times as argument, it will always be filled
211/// with clone of the same value.
212#[derive(Debug, Clone)]
213pub struct JsThis<T: TryFromJs>(pub T);
214
215impl<'a, T: TryFromJs> TryFromJsArgument<'a> for JsThis<T> {
216 fn try_from_js_argument(
217 this: &'a JsValue,
218 rest: &'a [JsValue],
219 context: &mut Context,
220 ) -> JsResult<(Self, &'a [JsValue])> {
221 Ok((JsThis(this.try_js_into(context)?), rest))
222 }
223}
224
225impl<T: TryFromJs> Deref for JsThis<T> {
226 type Target = T;
227
228 fn deref(&self) -> &Self::Target {
229 &self.0
230 }
231}
232
233/// Captures a class instance from the `this` value in a JS function. The class
234/// will be a non-mutable reference of Rust type `T`, if it is an instance of `T`.
235///
236/// To have more flexibility on the parsing of the `this` value, you can use the
237/// [`JsThis`] capture instead.
238#[derive(Debug, Clone)]
239pub struct JsClass<T: NativeObject> {
240 inner: JsObject<T>,
241}
242
243impl<T: NativeObject> JsClass<T> {
244 /// Returns the inner object directly.
245 #[must_use]
246 pub fn inner(&self) -> JsObject<T> {
247 self.inner.clone()
248 }
249
250 /// Borrow a reference to the class instance of type `T`.
251 ///
252 /// # Panics
253 ///
254 /// Panics if the object is currently borrowed.
255 ///
256 /// This does not panic if the type is wrong, as the type is checked
257 /// during the construction of the `JsClass` instance.
258 #[must_use]
259 pub fn borrow(&self) -> GcRef<'_, T> {
260 GcRef::map(self.inner.borrow(), |obj| obj.data())
261 }
262
263 /// Borrow a mutable reference to the class instance of type `T`.
264 ///
265 /// # Panics
266 ///
267 /// Panics if the object is currently mutably borrowed.
268 #[must_use]
269 pub fn borrow_mut(&self) -> GcRefMut<'_, T> {
270 GcRefMut::map(self.inner.borrow_mut(), |obj| obj.data_mut())
271 }
272}
273
274impl<T: NativeObject + Clone> JsClass<T> {
275 /// Clones the inner class instance.
276 ///
277 /// # Panics
278 ///
279 /// Panics if the inner object is currently borrowed mutably.
280 #[must_use]
281 pub fn clone_inner(&self) -> T {
282 self.inner.borrow().data().clone()
283 }
284}
285
286impl<'a, T: NativeObject + 'static> TryFromJsArgument<'a> for JsClass<T> {
287 fn try_from_js_argument(
288 this: &'a JsValue,
289 rest: &'a [JsValue],
290 _context: &mut Context,
291 ) -> JsResult<(Self, &'a [JsValue])> {
292 let inner = this
293 .as_object()
294 .and_then(|o| o.clone().downcast::<T>().ok())
295 .ok_or_else(|| JsNativeError::typ().with_message("invalid this for class method"))?;
296
297 Ok((JsClass { inner }, rest))
298 }
299}
300
301/// Captures a [`ContextData`] data from the [`Context`] as a JS function argument,
302/// based on its type.
303///
304/// The host defined type must implement [`Clone`], otherwise the borrow
305/// checker would not be able to ensure the safety of the context while
306/// making the function call. Because of this, it is recommended to use
307/// types that are cheap to clone.
308///
309/// For example,
310/// ```
311/// # use boa_engine::{Context, Finalize, JsData, JsValue, Trace, IntoJsFunctionCopied};
312/// # use boa_engine::interop::ContextData;
313///
314/// #[derive(Clone, Debug, Finalize, JsData, Trace)]
315/// struct CustomHostDefinedStruct {
316/// #[unsafe_ignore_trace]
317/// pub counter: usize,
318/// }
319/// let mut context = Context::default();
320/// context.insert_data(CustomHostDefinedStruct { counter: 123 });
321/// let f = (|ContextData(host): ContextData<CustomHostDefinedStruct>| host.counter + 1)
322/// .into_js_function_copied(&mut context);
323///
324/// assert_eq!(
325/// f.call(&JsValue::undefined(), &[], &mut context),
326/// Ok(JsValue::new(124))
327/// );
328/// ```
329#[derive(Debug, Clone)]
330pub struct ContextData<T: Clone>(pub T);
331
332impl<'a, T: NativeObject + Clone> TryFromJsArgument<'a> for ContextData<T> {
333 fn try_from_js_argument(
334 _this: &'a JsValue,
335 rest: &'a [JsValue],
336 context: &mut Context,
337 ) -> JsResult<(Self, &'a [JsValue])> {
338 match context.get_data::<T>() {
339 Some(value) => Ok((ContextData(value.clone()), rest)),
340 None => Err(JsNativeError::typ()
341 .with_message("Context data not found")
342 .into()),
343 }
344 }
345}