workflow_wasm/
convert.rs

1//!
2//! WASM bindgen casting and utility functions.
3//!
4//! This module provides a `CastFromJs` trait and derive macro
5//! that allows for easy casting of JavaScript objects into Rust.
6//! The secondary goal of this module is to provide the ability
7//! to dynamically interpret user-supplied JavaScript data that
8//! instead of a Rust object may container other data that can
9//! be used (interpreted) to create a Rust object.
10//!
11//! To accommodate this a [`TryCastFromJs`] trait is provided
12//! where user needs to implement `try_cast_from` function that
13//! can attempt to cast a JsValue into a Rust object or interpret
14//! the source data and create a temporary struct owned by by the
15//! [`Cast`] enum.
16//!
17//!
18
19use crate::error::Error;
20use crate::extensions::ObjectExtension;
21use js_sys::Object;
22pub use std::borrow::Borrow;
23pub use std::ops::Deref;
24use wasm_bindgen::convert::{LongRefFromWasmAbi, RefFromWasmAbi, RefMutFromWasmAbi};
25use wasm_bindgen::prelude::*;
26pub use workflow_wasm_macros::CastFromJs;
27
28#[wasm_bindgen(typescript_custom_section)]
29const IWASM32_BINDINGS_CONFIG: &'static str = r#"
30/**
31 * Interface for configuring workflow-rs WASM32 bindings.
32 * 
33 * @category General
34 */
35export interface IWASM32BindingsConfig {
36    /**
37     * This option can be used to disable the validation of class names
38     * for instances of classes exported by Rust WASM32 when passing
39     * these classes to WASM32 functions.
40     * 
41     * This can be useful to programmatically disable checks when using
42     * a bundler that mangles class symbol names.
43     */
44    validateClassNames : boolean;
45}
46"#;
47
48#[wasm_bindgen]
49extern "C" {
50    #[wasm_bindgen(extends = Object, typescript_type = "IWASM32BindingsConfig")]
51    pub type IWASM32BindingsConfig;
52}
53
54static mut VALIDATE_CLASS_NAMES: bool = true;
55/// Configuration for the WASM32 bindings runtime interface.
56/// @see {@link IWASM32BindingsConfig}
57/// @category General
58#[wasm_bindgen(js_name = "initWASM32Bindings")]
59pub fn init_wasm32_bindings(config: IWASM32BindingsConfig) -> std::result::Result<(), Error> {
60    if let Some(enable) = config.try_get_bool("validateClassNames")? {
61        unsafe {
62            VALIDATE_CLASS_NAMES = enable;
63        }
64    }
65    Ok(())
66}
67#[inline(always)]
68pub fn validate_class_names() -> bool {
69    unsafe { VALIDATE_CLASS_NAMES }
70}
71
72/// A wrapper for a Rust object that can be either a reference or a value.
73/// This wrapper is used to carry a Rust (WASM ABI) reference provided by
74/// `wasm_bindgen`, but at the same time allows creation of a temporary
75/// object that can be created by interpreting the source user-supplied data.
76/// [`Cast`] then provides [`Cast::as_ref()`] to obtain the internally held
77/// reference and [`Cast::into_owned()`] where the latter will consume the
78/// value or clone the reference.
79pub enum Cast<'a, T>
80where
81    T: RefFromWasmAbi<Abi = u32> + LongRefFromWasmAbi<Abi = u32> + 'a,
82{
83    Ref {
84        anchor: <T as RefFromWasmAbi>::Anchor,
85    },
86    OwnedRef {
87        js_value: Option<JsValue>,
88        anchor: Option<<T as RefFromWasmAbi>::Anchor>,
89    },
90    LongRef {
91        anchor: <T as LongRefFromWasmAbi>::Anchor,
92    },
93    Value {
94        value: Option<T>,
95    },
96    _Unreachable(std::convert::Infallible, &'a std::marker::PhantomData<T>),
97}
98
99impl<'a, T> Drop for Cast<'a, T>
100where
101    T: RefFromWasmAbi<Abi = u32> + LongRefFromWasmAbi<Abi = u32> + 'a,
102{
103    fn drop(&mut self) {
104        if let Cast::OwnedRef { js_value, anchor } = self {
105            // ensure anchor is dropped before js_value
106            // as anchor holds a borrow, while js_value Drop impl requires a borrow
107            drop(anchor.take());
108            drop(js_value.take());
109        }
110    }
111}
112
113impl<'a, T> Deref for Cast<'a, T>
114where
115    T: RefFromWasmAbi<Abi = u32> + LongRefFromWasmAbi<Abi = u32> + Deref,
116{
117    type Target = T;
118    fn deref(&self) -> &Self::Target {
119        match self {
120            Cast::Ref { anchor } => anchor,
121            Cast::OwnedRef { anchor, .. } => anchor.as_ref().unwrap(),
122            Cast::LongRef { anchor } => anchor.borrow(),
123            Cast::Value { value } => value.as_ref().unwrap(),
124            Cast::_Unreachable(_, _) => unreachable!(),
125        }
126    }
127}
128
129impl<'a, T> AsRef<T> for Cast<'a, T>
130where
131    T: RefFromWasmAbi<Abi = u32> + LongRefFromWasmAbi<Abi = u32>,
132{
133    /// Obtain a reference to the internally held value.
134    fn as_ref(&self) -> &T {
135        match self {
136            Cast::Ref { anchor } => anchor,
137            Cast::OwnedRef { anchor, .. } => anchor.as_ref().unwrap(),
138            Cast::LongRef { anchor } => anchor.borrow(),
139            Cast::Value { value } => value.as_ref().unwrap(),
140            Cast::_Unreachable(_, _) => unreachable!(),
141        }
142    }
143}
144
145impl<'a, T> Cast<'a, T>
146where
147    T: RefFromWasmAbi<Abi = u32> + LongRefFromWasmAbi<Abi = u32> + Clone,
148{
149    /// Consume the [`Cast`] and return the owned value. If the
150    /// [`Cast`] holds a reference, it will be cloned.
151    pub fn into_owned(mut self) -> T {
152        match &mut self {
153            Cast::Ref { anchor } => (*anchor).clone(),
154            Cast::OwnedRef { js_value, anchor } => {
155                let value = (*anchor.as_ref().unwrap()).clone();
156                drop(anchor.take());
157                drop(js_value.take());
158                value
159            }
160            Cast::LongRef { anchor } => (*anchor).borrow().clone(),
161            Cast::Value { value } => value.take().unwrap(),
162            Cast::_Unreachable(_, _) => unreachable!(),
163        }
164    }
165
166    pub fn value(value: T) -> Self {
167        Cast::Value { value: Some(value) }
168    }
169
170    // pub fn captured_ref(js_value : impl AsRef<JsValue>)
171}
172
173/// Cast T value (struct) into `Cast<T>`
174impl<'a, T> From<T> for Cast<'a, T>
175where
176    T: RefFromWasmAbi<Abi = u32> + LongRefFromWasmAbi<Abi = u32>,
177{
178    fn from(value: T) -> Cast<'a, T> {
179        Cast::Value { value: Some(value) }
180    }
181}
182
183/// `CastFromJs` trait is automatically implemented by deriving
184/// the `CastFromJs` derive macro. This trait provides functions
185/// for accessing Rust references from the WASM ABI.
186pub trait CastFromJs
187where
188    Self: Sized + RefFromWasmAbi<Abi = u32> + LongRefFromWasmAbi<Abi = u32>,
189{
190    /// Obtain safe reference from [`JsValue`]
191    fn try_ref_from_js_value<'a, R>(
192        js_value: &'a R,
193    ) -> std::result::Result<<Self as RefFromWasmAbi>::Anchor, Error>
194    where
195        R: AsRef<JsValue> + 'a;
196
197    fn try_ref_from_js_value_as_cast<'a, R>(
198        js_value: &'a R,
199    ) -> std::result::Result<Cast<'a, Self>, Error>
200    where
201        R: AsRef<JsValue> + 'a,
202    {
203        Self::try_ref_from_js_value(js_value).map(|anchor| Cast::Ref { anchor })
204    }
205
206    /// Obtain safe long reference from [`JsValue`]
207    fn try_long_ref_from_js_value<'a, R>(
208        js: &'a R,
209    ) -> std::result::Result<<Self as LongRefFromWasmAbi>::Anchor, Error>
210    where
211        R: AsRef<JsValue> + 'a;
212
213    fn try_long_ref_from_js_value_as_cast<'a, R>(
214        js: &'a R,
215    ) -> std::result::Result<Cast<Self>, Error>
216    where
217        R: AsRef<JsValue> + 'a,
218    {
219        Self::try_long_ref_from_js_value(js).map(|anchor| Cast::LongRef { anchor })
220    }
221}
222
223/// `TryCastFromJs` trait is meant to be implemented by the developer
224/// on any struct implementing `CastFromJs` trait. This trait provides
225/// a way to attempt to cast a JsValue into a Rust object or interpret
226/// the source data and create a temporary struct owned by by the [`Cast`].
227pub trait TryCastFromJs
228where
229    Self: CastFromJs + RefFromWasmAbi<Abi = u32> + LongRefFromWasmAbi<Abi = u32> + Clone,
230{
231    type Error: std::fmt::Display + From<Error>;
232
233    /// Try to cast a JsValue into a Rust object.
234    /// This should be user-defined function that
235    /// attempts to cast a JsValue into a Rust object
236    /// or interpret a source data and create a
237    /// temporary struct owned by by the [`Cast`].
238    fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result<Cast<'a, Self>, Self::Error>
239    where
240        R: AsRef<JsValue> + 'a;
241
242    /// Perform a user cast and consume the [`Cast`] container.
243    /// This function will return a temporary user-created
244    /// object created during [`try_cast_from`] or a clone of the casted reference.
245    fn try_owned_from(value: impl AsRef<JsValue>) -> std::result::Result<Self, Self::Error> {
246        Self::try_cast_from(&value).map(|c| c.into_owned())
247    }
248
249    fn try_captured_cast_from(
250        js_value: impl AsRef<JsValue>,
251    ) -> std::result::Result<Cast<'static, Self>, Self::Error> {
252        let js_value = js_value.as_ref().clone();
253        Ok(
254            Self::try_ref_from_js_value(&js_value).map(|anchor| Cast::OwnedRef {
255                js_value: Some(js_value),
256                anchor: Some(anchor),
257            })?,
258        )
259    }
260
261    /// Try to cast a JsValue into a Rust object, in cast of failure
262    /// invoke a user-supplied closure that can try to create an instance
263    /// of the object based on the supplied JsValue.
264    fn resolve<'a, R>(
265        js: &'a R,
266        create: impl FnOnce() -> std::result::Result<Self, Self::Error>,
267    ) -> std::result::Result<Cast<Self>, Self::Error>
268    where
269        R: AsRef<JsValue> + 'a,
270    {
271        Self::try_ref_from_js_value(js)
272            .map(|anchor| Cast::Ref { anchor })
273            .or_else(|_| create().map(|value| Cast::Value { value: Some(value) }))
274    }
275
276    /// Try to cast a JsValue into a Rust object, in cast of failure
277    /// invoke a user-supplied closure that can try to create an instance
278    /// of the object based on the supplied JsValue. Unlike the [`resolve`]
279    /// function, this function expects `create` closure to return a [`Cast`].
280    /// This is useful when routing the creation of the object to another
281    /// function that is capable of creating a compatible Cast wrapper.
282    fn resolve_cast<'a, R>(
283        js: &'a R,
284        create: impl FnOnce() -> std::result::Result<Cast<'a, Self>, Self::Error>,
285    ) -> std::result::Result<Cast<'a, Self>, Self::Error>
286    where
287        R: AsRef<JsValue> + 'a,
288    {
289        Self::try_ref_from_js_value(js)
290            .map(|anchor| Cast::Ref { anchor })
291            .or_else(|_| create())
292    }
293}
294
295pub trait TryCastJsInto<T>
296where
297    T: TryCastFromJs,
298{
299    type Error: From<Error>;
300    fn try_into_cast(&self) -> std::result::Result<Cast<T>, Self::Error>;
301    fn try_into_owned(&self) -> std::result::Result<T, Self::Error>;
302}
303
304impl<T> TryCastJsInto<T> for JsValue
305where
306    T: TryCastFromJs,
307    <T as TryCastFromJs>::Error: From<Error>,
308{
309    type Error = <T as TryCastFromJs>::Error;
310    fn try_into_cast(&self) -> std::result::Result<Cast<T>, Self::Error> {
311        T::try_cast_from(self)
312    }
313
314    fn try_into_owned(&self) -> std::result::Result<T, Self::Error> {
315        T::try_owned_from(self)
316    }
317}
318
319/// Obtain a WASM bingen ABI pointer from a supplied JsValue.
320/// This function validates the acquired object ptr by comparing its
321/// `constructor.name` value to the supplied `class` name.
322fn get_ptr_u32_safe(
323    class: &str,
324    js: impl AsRef<JsValue>,
325) -> std::result::Result<Option<u32>, Error> {
326    let js = js.as_ref();
327
328    if js.is_undefined() || js.is_null() {
329        return Ok(None);
330    } else if !js.is_object() {
331        return Err(Error::NotAnObjectOfClass(class.to_string()));
332    }
333
334    if validate_class_names() {
335        let ctor = ::js_sys::Reflect::get(js, &JsValue::from_str("constructor"))?;
336        if ctor.is_undefined() {
337            return Err(Error::NoConstructorOfClass(class.to_string()));
338        } else {
339            let name = ::js_sys::Reflect::get(&ctor, &JsValue::from_str("name"))?;
340            if name.is_undefined() {
341                return Err(Error::UnableToObtainConstructorName(class.to_string()));
342            } else {
343                let name = name
344                    .as_string()
345                    .ok_or(Error::UnableToObtainConstructorName(class.to_string()))?;
346                if name != class {
347                    return Err(Error::ClassConstructorMatch(name, class.to_string()));
348                }
349            }
350        }
351    }
352
353    let ptr = ::js_sys::Reflect::get(js, &::wasm_bindgen::JsValue::from_str("__wbg_ptr"))?;
354    if ptr.is_undefined() {
355        return Err(Error::NotWasmAbiPointerForClass(class.to_string()));
356    }
357    let ptr_u32: u32 = ptr
358        .as_f64()
359        .ok_or(Error::NotWasmAbiPointerForClass(class.to_string()))? as u32;
360
361    Ok(Some(ptr_u32))
362}
363
364/// Create a reference to a Rust object from a WASM ABI.
365#[inline]
366pub fn try_ref_from_abi_safe<T>(
367    class: &str,
368    js: impl AsRef<JsValue>,
369) -> std::result::Result<<T as RefFromWasmAbi>::Anchor, Error>
370where
371    T: RefFromWasmAbi<Abi = u32>,
372{
373    let ptr_u32 =
374        get_ptr_u32_safe(class, js)?.ok_or_else(|| Error::NotAnObjectOfClass(class.to_string()))?;
375    Ok(unsafe { T::ref_from_abi(ptr_u32) })
376}
377
378#[inline]
379pub fn try_long_ref_from_abi_safe<T>(
380    class: &str,
381    js: impl AsRef<JsValue>,
382) -> std::result::Result<<T as LongRefFromWasmAbi>::Anchor, Error>
383where
384    T: LongRefFromWasmAbi<Abi = u32>,
385{
386    let ptr_u32 =
387        get_ptr_u32_safe(class, js)?.ok_or_else(|| Error::NotAnObjectOfClass(class.to_string()))?;
388    Ok(unsafe { T::long_ref_from_abi(ptr_u32) })
389}
390
391#[inline]
392pub fn try_ref_mut_from_abi_safe<T>(
393    class: &str,
394    js: impl AsRef<JsValue>,
395) -> std::result::Result<<T as RefMutFromWasmAbi>::Anchor, Error>
396where
397    T: RefMutFromWasmAbi<Abi = u32>,
398{
399    let ptr_u32 =
400        get_ptr_u32_safe(class, js)?.ok_or_else(|| Error::NotAnObjectOfClass(class.to_string()))?;
401    Ok(unsafe { T::ref_mut_from_abi(ptr_u32) })
402}
403
404#[inline]
405pub fn try_clone_from_abi_safe<T>(
406    class: &str,
407    js: impl AsRef<JsValue>,
408) -> std::result::Result<T, Error>
409where
410    T: RefFromWasmAbi<Abi = u32> + Clone,
411{
412    try_ref_from_abi_safe::<T>(class, js).map(|r| r.clone())
413}
414
415#[inline]
416pub fn try_copy_from_abi_safe<T>(
417    class: &str,
418    js: impl AsRef<JsValue>,
419) -> std::result::Result<T, Error>
420where
421    T: RefFromWasmAbi<Abi = u32> + Copy,
422{
423    try_ref_from_abi_safe::<T>(class, js).map(|r| *r)
424}
425
426/// Create a reference to a Rust object from a WASM ABI.
427/// Returns None is the supplied value is `null` or `undefined`,
428/// otherwise attempts to cast the object.
429#[inline]
430pub fn try_ref_from_abi_safe_as_option<T>(
431    class: &str,
432    js: impl AsRef<JsValue>,
433) -> std::result::Result<Option<<T as RefFromWasmAbi>::Anchor>, JsValue>
434where
435    T: RefFromWasmAbi<Abi = u32>,
436{
437    Ok(get_ptr_u32_safe(class, js)?.map(|ptr_u32| unsafe { T::ref_from_abi(ptr_u32) }))
438}
439
440#[inline]
441pub fn try_ref_mut_from_abi_safe_as_option<T>(
442    class: &str,
443    js: impl AsRef<JsValue>,
444) -> std::result::Result<Option<<T as RefMutFromWasmAbi>::Anchor>, JsValue>
445where
446    T: RefMutFromWasmAbi<Abi = u32>,
447{
448    Ok(get_ptr_u32_safe(class, js)?.map(|ptr_u32| unsafe { T::ref_mut_from_abi(ptr_u32) }))
449}
450
451#[inline]
452pub fn try_clone_from_abi_safe_as_option<T>(
453    class: &str,
454    js: impl AsRef<JsValue>,
455) -> std::result::Result<Option<T>, JsValue>
456where
457    T: RefFromWasmAbi<Abi = u32> + Clone,
458{
459    Ok(get_ptr_u32_safe(class, js)?.map(|ptr_u32| unsafe { T::ref_from_abi(ptr_u32).clone() }))
460}
461
462#[inline]
463pub fn try_copy_from_abi_safe_as_option<T>(
464    class: &str,
465    js: impl AsRef<JsValue>,
466) -> std::result::Result<Option<T>, JsValue>
467where
468    T: RefFromWasmAbi<Abi = u32> + Copy,
469{
470    Ok(get_ptr_u32_safe(class, js)?.map(|ptr_u32| unsafe { *T::ref_from_abi(ptr_u32) }))
471}