Skip to main content

wowlab_types/
wasm.rs

1// #t(file: rust_pub_api_foreign_types) wasm-bindgen boundary must expose JavaScript ABI types
2
3use wasm_bindgen::prelude::*;
4
5#[doc(hidden)]
6pub mod _private {
7    pub use js_sys;
8    pub use serde_wasm_bindgen;
9    pub use wasm_bindgen;
10}
11
12/// Define a WASM error enum with `Display`, `From<Self>` for `JsValue`, and `From<serde_wasm_bindgen::Error>`.
13#[macro_export]
14macro_rules! wasm_error {
15    (
16        $(#[$meta:meta])*
17        $vis:vis enum $name:ident {
18            $(
19                $(#[$vmeta:meta])*
20                $variant:ident($inner:ty) => $js_name:literal
21            ),+ $(,)?
22        }
23    ) => {
24        $(#[$meta])*
25        $vis enum $name {
26            $( $(#[$vmeta])* $variant($inner), )+
27        }
28
29        impl ::std::fmt::Display for $name {
30            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
31                match self {
32                    $( Self::$variant(msg) => write!(f, "{msg}"), )+
33                }
34            }
35        }
36
37        impl From<$name> for $crate::wasm::_private::wasm_bindgen::JsValue {
38            fn from(err: $name) -> Self {
39                let name = match &err {
40                    $( $name::$variant(_) => $js_name, )+
41                };
42                let js_err = $crate::wasm::_private::js_sys::Error::new(&err.to_string());
43                js_err.set_name(name);
44                js_err.into()
45            }
46        }
47
48        impl From<$crate::wasm::_private::serde_wasm_bindgen::Error> for $name {
49            fn from(e: $crate::wasm::_private::serde_wasm_bindgen::Error) -> Self {
50                Self::Serialize(e.to_string())
51            }
52        }
53    };
54}
55
56wasm_error! {
57    #[derive(Debug)]
58    #[non_exhaustive]
59    /// Typed error that converts to a JS `Error` with a `name` property.
60    pub enum WasmCommonError {
61        Parse(String) => "ParseError",
62        Decode(String) => "DecodeError",
63        Serialize(String) => "SerializeError",
64        Config(String) => "ConfigError",
65    }
66}
67
68pub fn js_set(obj: &js_sys::Object, key: &str, value: &JsValue) {
69    let _ = js_sys::Reflect::set(obj, &JsValue::from_str(key), value);
70}
71
72/// Deserialize a JavaScript value.
73///
74/// # Errors
75///
76/// Returns [`WasmCommonError::Decode`] when the value does not match `T`.
77pub fn from_js<T>(val: JsValue) -> Result<T, WasmCommonError>
78where
79    T: serde::de::DeserializeOwned,
80{
81    serde_wasm_bindgen::from_value(val).map_err(|e| WasmCommonError::Decode(e.to_string()))
82}
83
84/// Serialize a value for JavaScript.
85///
86/// # Errors
87///
88/// Returns the serializer error when `val` cannot be represented as a JavaScript value.
89pub fn to_js<T>(val: &T) -> Result<JsValue, serde_wasm_bindgen::Error>
90where
91    T: serde::Serialize,
92{
93    serde_wasm_bindgen::to_value(val)
94}
95
96const HEX_CHARS_PER_BYTE: usize = 2;
97const HEX_RADIX: u32 = 16;
98const HEX_HIGH_NIBBLE_SHIFT: u32 = 4;
99
100/// Decode a PostgreSQL-style hexadecimal byte string.
101///
102/// # Errors
103///
104/// Returns [`WasmCommonError::Decode`] for an odd length or a non-hexadecimal digit.
105pub fn decode_hex_to_bytes(hex: &str) -> Result<Vec<u8>, WasmCommonError> {
106    let hex = hex.strip_prefix("\\x").unwrap_or(hex);
107
108    if hex.len() % HEX_CHARS_PER_BYTE != 0 {
109        return Err(WasmCommonError::Decode("invalid hex length".into()));
110    }
111
112    let mut bytes = Vec::with_capacity(hex.len() / HEX_CHARS_PER_BYTE);
113    let chars: Vec<char> = hex.chars().collect();
114
115    for i in (0..chars.len()).step_by(HEX_CHARS_PER_BYTE) {
116        let hi = chars
117            .get(i)
118            .and_then(|c| c.to_digit(HEX_RADIX))
119            .ok_or_else(|| WasmCommonError::Decode("invalid hex digit".into()))?;
120        let lo = chars
121            .get(i + 1)
122            .and_then(|c| c.to_digit(HEX_RADIX))
123            .ok_or_else(|| WasmCommonError::Decode("invalid hex digit".into()))?;
124        let byte = (hi << HEX_HIGH_NIBBLE_SHIFT) | lo;
125
126        bytes.push(u8::try_from(byte).unwrap_or_default());
127    }
128
129    Ok(bytes)
130}