shopify_function_wasm_api/
lib.rs

1//! # Shopify Function Wasm API
2//!
3//! This crate provides a high-level API for interfacing with the Shopify Function Wasm API.
4//!
5//! ## Usage
6//!
7//! ```rust,no_run
8//! use shopify_function_wasm_api::{Context, Serialize, Deserialize, Value};
9//! use std::error::Error;
10//!
11//! fn main() {
12//!     run().unwrap();
13//! }
14//!
15//! fn run() -> Result<(), Box<dyn Error>> {
16//!     shopify_function_wasm_api::init_panic_handler();
17//!     let mut context = Context::new();
18//!     let input = context.input_get()?;
19//!     let value: i32 = Deserialize::deserialize(&input)?;
20//!     value.serialize(&mut context)?;
21//!     Ok(())
22//! }
23//! ```
24
25#![warn(missing_docs)]
26
27use shopify_function_wasm_api_core::read::{ErrorCode, NanBox, Val, ValueRef};
28use std::{cell::RefCell, collections::HashMap};
29
30pub mod log;
31pub mod read;
32pub mod write;
33
34pub use read::Deserialize;
35pub use write::Serialize;
36
37#[cfg(target_family = "wasm")]
38#[link(wasm_import_module = "shopify_function_v2")]
39extern "C" {
40    // Read API.
41    fn shopify_function_input_get() -> Val;
42    fn shopify_function_input_get_val_len(scope: Val) -> usize;
43    fn shopify_function_input_read_utf8_str(src: usize, out: *mut u8, len: usize);
44    fn shopify_function_input_get_obj_prop(scope: Val, ptr: *const u8, len: usize) -> Val;
45    fn shopify_function_input_get_interned_obj_prop(
46        scope: Val,
47        interned_string_id: shopify_function_wasm_api_core::InternedStringId,
48    ) -> Val;
49    fn shopify_function_input_get_at_index(scope: Val, index: usize) -> Val;
50    fn shopify_function_input_get_obj_key_at_index(scope: Val, index: usize) -> Val;
51
52    // Write API.
53    fn shopify_function_output_new_bool(bool: u32) -> usize;
54    fn shopify_function_output_new_null() -> usize;
55    fn shopify_function_output_new_i32(int: i32) -> usize;
56    fn shopify_function_output_new_f64(float: f64) -> usize;
57    fn shopify_function_output_new_utf8_str(ptr: *const u8, len: usize) -> usize;
58    fn shopify_function_output_new_interned_utf8_str(
59        id: shopify_function_wasm_api_core::InternedStringId,
60    ) -> usize;
61    fn shopify_function_output_new_object(len: usize) -> usize;
62    fn shopify_function_output_finish_object() -> usize;
63    fn shopify_function_output_new_array(len: usize) -> usize;
64    fn shopify_function_output_finish_array() -> usize;
65
66    // Log API.
67    fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize);
68
69    // Other.
70    fn shopify_function_intern_utf8_str(ptr: *const u8, len: usize) -> usize;
71}
72
73#[cfg(not(target_family = "wasm"))]
74mod provider_fallback {
75    use super::Val;
76    use shopify_function_wasm_api_core::write::WriteResult;
77
78    // Read API.
79    pub(crate) unsafe fn shopify_function_input_get() -> Val {
80        shopify_function_provider::read::shopify_function_input_get()
81    }
82    pub(crate) unsafe fn shopify_function_input_get_val_len(scope: Val) -> usize {
83        shopify_function_provider::read::shopify_function_input_get_val_len(scope)
84    }
85    pub(crate) unsafe fn shopify_function_input_read_utf8_str(
86        src: usize,
87        out: *mut u8,
88        len: usize,
89    ) {
90        let src = shopify_function_provider::read::shopify_function_input_get_utf8_str_addr(src);
91        std::ptr::copy(src as _, out, len);
92    }
93    pub(crate) unsafe fn shopify_function_input_get_obj_prop(
94        scope: Val,
95        ptr: *const u8,
96        len: usize,
97    ) -> Val {
98        shopify_function_provider::read::shopify_function_input_get_obj_prop(scope, ptr as _, len)
99    }
100    pub(crate) unsafe fn shopify_function_input_get_interned_obj_prop(
101        scope: Val,
102        interned_string_id: shopify_function_wasm_api_core::InternedStringId,
103    ) -> Val {
104        shopify_function_provider::read::shopify_function_input_get_interned_obj_prop(
105            scope,
106            interned_string_id,
107        )
108    }
109    pub(crate) unsafe fn shopify_function_input_get_at_index(scope: Val, index: usize) -> Val {
110        shopify_function_provider::read::shopify_function_input_get_at_index(scope, index)
111    }
112    pub(crate) unsafe fn shopify_function_input_get_obj_key_at_index(
113        scope: Val,
114        index: usize,
115    ) -> Val {
116        shopify_function_provider::read::shopify_function_input_get_obj_key_at_index(scope, index)
117    }
118
119    // Write API.
120    pub(crate) unsafe fn shopify_function_output_new_bool(bool: u32) -> usize {
121        shopify_function_provider::write::shopify_function_output_new_bool(bool) as usize
122    }
123    pub(crate) unsafe fn shopify_function_output_new_null() -> usize {
124        shopify_function_provider::write::shopify_function_output_new_null() as usize
125    }
126    pub(crate) unsafe fn shopify_function_output_new_i32(int: i32) -> usize {
127        shopify_function_provider::write::shopify_function_output_new_i32(int) as usize
128    }
129    pub(crate) unsafe fn shopify_function_output_new_f64(float: f64) -> usize {
130        shopify_function_provider::write::shopify_function_output_new_f64(float) as usize
131    }
132    pub(crate) unsafe fn shopify_function_output_new_utf8_str(ptr: *const u8, len: usize) -> usize {
133        let result = shopify_function_provider::write::shopify_function_output_new_utf8_str(len);
134        let write_result = (result >> usize::BITS) as usize;
135        let dst = result as usize;
136        if write_result == WriteResult::Ok as usize {
137            std::ptr::copy(ptr as _, dst as _, len);
138        }
139        write_result
140    }
141    pub(crate) unsafe fn shopify_function_output_new_interned_utf8_str(
142        id: shopify_function_wasm_api_core::InternedStringId,
143    ) -> usize {
144        shopify_function_provider::write::shopify_function_output_new_interned_utf8_str(id) as usize
145    }
146    pub(crate) unsafe fn shopify_function_output_new_object(len: usize) -> usize {
147        shopify_function_provider::write::shopify_function_output_new_object(len) as usize
148    }
149    pub(crate) unsafe fn shopify_function_output_finish_object() -> usize {
150        shopify_function_provider::write::shopify_function_output_finish_object() as usize
151    }
152    pub(crate) unsafe fn shopify_function_output_new_array(len: usize) -> usize {
153        shopify_function_provider::write::shopify_function_output_new_array(len) as usize
154    }
155    pub(crate) unsafe fn shopify_function_output_finish_array() -> usize {
156        shopify_function_provider::write::shopify_function_output_finish_array() as usize
157    }
158
159    // Logging.
160    pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) {
161        let addr = shopify_function_provider::log::shopify_function_log_new_utf8_str(len)
162            as *const [usize; 5];
163        let array = *addr;
164        let source_offset = array[0];
165        let dst_offset1 = array[1];
166        let len1 = array[2];
167        let dst_offset2 = array[3];
168        let len2 = array[4];
169        std::ptr::copy(ptr.add(source_offset) as _, dst_offset1 as _, len1);
170        std::ptr::copy(ptr.add(source_offset).add(len1), dst_offset2 as _, len2);
171    }
172
173    // Other.
174    pub(crate) unsafe fn shopify_function_intern_utf8_str(ptr: *const u8, len: usize) -> usize {
175        let result = shopify_function_provider::shopify_function_intern_utf8_str(len);
176        let id = (result >> usize::BITS) as usize;
177        let dst = result as usize;
178        std::ptr::copy(ptr as _, dst as _, len);
179        id
180    }
181}
182#[cfg(not(target_family = "wasm"))]
183use provider_fallback::*;
184
185/// An identifier for an interned UTF-8 string.
186///
187/// This is returned by [`Context::intern_utf8_str`], and can be used for both reading and writing.
188#[derive(Clone, Copy, PartialEq, Debug)]
189pub struct InternedStringId(shopify_function_wasm_api_core::InternedStringId);
190
191impl InternedStringId {
192    fn as_usize(&self) -> usize {
193        self.0
194    }
195}
196
197// The underlying string interner is thread local so the cache needs to be thread local too
198thread_local! {
199    static INTERNED_STRING_CACHE: RefCell<HashMap::<&'static str, InternedStringId>> = RefCell::new(HashMap::new());
200}
201
202/// A mechanism for caching interned string IDs.
203pub struct CachedInternedStringId {
204    value: &'static str,
205}
206
207impl CachedInternedStringId {
208    /// Create a new cached interned string ID.
209    pub const fn new(value: &'static str) -> Self {
210        Self { value }
211    }
212
213    /// Load the interned string ID.
214    pub fn load(&self) -> InternedStringId {
215        INTERNED_STRING_CACHE.with_borrow_mut(|cache| {
216            *cache.entry(self.value).or_insert_with(|| {
217                InternedStringId(unsafe {
218                    shopify_function_intern_utf8_str(self.value.as_ptr(), self.value.len())
219                })
220            })
221        })
222    }
223}
224
225/// A value read from the input.
226///
227/// This can be any of the following types:
228/// - boolean
229/// - number
230/// - string
231/// - null
232/// - object
233/// - array
234/// - error
235#[derive(Copy, Clone)]
236pub struct Value {
237    nan_box: NanBox,
238}
239
240impl Value {
241    fn new_child(&self, nan_box: NanBox) -> Self {
242        Self { nan_box }
243    }
244
245    /// Intern a string. This is just a convenience method equivalent to calling [`Context::intern_utf8_str`], if you don't have a [`Context`] easily accessible.
246    pub fn intern_utf8_str(&self, s: &str) -> InternedStringId {
247        let len = s.len();
248        let ptr = s.as_ptr();
249        let id = unsafe { shopify_function_intern_utf8_str(ptr, len) };
250        InternedStringId(id)
251    }
252
253    /// Get the value as a boolean, if it is one.
254    pub fn as_bool(&self) -> Option<bool> {
255        match self.nan_box.try_decode() {
256            Ok(ValueRef::Bool(b)) => Some(b),
257            _ => None,
258        }
259    }
260
261    /// Check if the value is null.
262    pub fn is_null(&self) -> bool {
263        matches!(self.nan_box.try_decode(), Ok(ValueRef::Null))
264    }
265
266    /// Get the value as a number, if it is one. Note that this will apply to both integers and floats.
267    pub fn as_number(&self) -> Option<f64> {
268        match self.nan_box.try_decode() {
269            Ok(ValueRef::Number(n)) => Some(n),
270            _ => None,
271        }
272    }
273
274    /// Get the value as a string, if it is one.
275    pub fn as_string(&self) -> Option<String> {
276        match self.nan_box.try_decode() {
277            Ok(ValueRef::String { ptr, len }) => {
278                let len = if len == NanBox::MAX_VALUE_LENGTH {
279                    unsafe { shopify_function_input_get_val_len(self.nan_box.to_bits()) }
280                } else {
281                    len
282                };
283                let mut buf = vec![0; len];
284                unsafe { shopify_function_input_read_utf8_str(ptr as _, buf.as_mut_ptr(), len) };
285                Some(unsafe { String::from_utf8_unchecked(buf) })
286            }
287            _ => None,
288        }
289    }
290
291    /// Check if the value is an object.
292    pub fn is_obj(&self) -> bool {
293        matches!(self.nan_box.try_decode(), Ok(ValueRef::Object { .. }))
294    }
295
296    /// Get a property from the object.
297    pub fn get_obj_prop(&self, prop: &str) -> Self {
298        let scope = unsafe {
299            shopify_function_input_get_obj_prop(self.nan_box.to_bits(), prop.as_ptr(), prop.len())
300        };
301        self.new_child(NanBox::from_bits(scope))
302    }
303
304    /// Get a property from the object by its interned string ID.
305    pub fn get_interned_obj_prop(&self, interned_string_id: InternedStringId) -> Self {
306        let scope = unsafe {
307            shopify_function_input_get_interned_obj_prop(
308                self.nan_box.to_bits(),
309                interned_string_id.as_usize(),
310            )
311        };
312        self.new_child(NanBox::from_bits(scope))
313    }
314
315    /// Check if the value is an array.
316    pub fn is_array(&self) -> bool {
317        matches!(self.nan_box.try_decode(), Ok(ValueRef::Array { .. }))
318    }
319
320    /// Get the length of the array, if it is one.
321    pub fn array_len(&self) -> Option<usize> {
322        match self.nan_box.try_decode() {
323            Ok(ValueRef::Array { len, .. }) => {
324                let len = if len == NanBox::MAX_VALUE_LENGTH {
325                    unsafe { shopify_function_input_get_val_len(self.nan_box.to_bits()) }
326                } else {
327                    len
328                };
329                if len == usize::MAX {
330                    None
331                } else {
332                    Some(len)
333                }
334            }
335            _ => None,
336        }
337    }
338
339    /// Get the length of the object, if it is one.
340    pub fn obj_len(&self) -> Option<usize> {
341        match self.nan_box.try_decode() {
342            Ok(ValueRef::Object { len, .. }) => {
343                let len = if len == NanBox::MAX_VALUE_LENGTH {
344                    unsafe { shopify_function_input_get_val_len(self.nan_box.to_bits()) }
345                } else {
346                    len
347                };
348                if len == usize::MAX {
349                    None
350                } else {
351                    Some(len)
352                }
353            }
354            _ => None,
355        }
356    }
357
358    /// Get an element from the array or object by its index.
359    pub fn get_at_index(&self, index: usize) -> Self {
360        let scope = unsafe { shopify_function_input_get_at_index(self.nan_box.to_bits(), index) };
361        self.new_child(NanBox::from_bits(scope))
362    }
363
364    /// Get the key of an object by its index.
365    pub fn get_obj_key_at_index(&self, index: usize) -> Option<String> {
366        match self.nan_box.try_decode() {
367            Ok(ValueRef::Object { .. }) => {
368                let scope = unsafe {
369                    shopify_function_input_get_obj_key_at_index(self.nan_box.to_bits(), index)
370                };
371                let value = self.new_child(NanBox::from_bits(scope));
372                value.as_string()
373            }
374            _ => None,
375        }
376    }
377
378    /// Get the error code, if it is one.
379    pub fn as_error(&self) -> Option<ErrorCode> {
380        match self.nan_box.try_decode() {
381            Ok(ValueRef::Error(e)) => Some(e),
382            _ => None,
383        }
384    }
385}
386
387/// A context for reading and writing values.
388///
389/// This is created by calling [`Context::new`], and is used to read values from the input and write values to the output.
390pub struct Context;
391
392/// An error that can occur when creating a [`Context`].
393#[derive(Debug)]
394#[non_exhaustive]
395pub enum ContextError {
396    /// The pointer to the context is null.
397    NullPointer,
398}
399
400impl std::error::Error for ContextError {}
401
402impl std::fmt::Display for ContextError {
403    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404        match self {
405            ContextError::NullPointer => write!(f, "Null pointer encountered"),
406        }
407    }
408}
409
410impl Context {
411    /// Create a new context.
412    ///
413    /// This is only intended to be invoked when compiled to a Wasm target.
414    ///
415    /// # Panics
416    /// This will panic if called from a non-Wasm environment.
417    pub fn new() -> Self {
418        #[cfg(not(target_family = "wasm"))]
419        panic!("Cannot run in non-WASM environment; use `new_with_input` instead");
420
421        #[cfg(target_family = "wasm")]
422        {
423            Self
424        }
425    }
426
427    /// Create a new context from a JSON value, which will be the top-level value of the input.
428    ///
429    /// This is only available when compiled to a non-Wasm target, for usage in unit tests.
430    #[cfg(not(target_family = "wasm"))]
431    pub fn new_with_input(input: serde_json::Value) -> Self {
432        let bytes = rmp_serde::to_vec(&input).unwrap();
433        shopify_function_provider::initialize_from_msgpack_bytes(bytes);
434        Self
435    }
436
437    /// Get the top-level value of the input.
438    pub fn input_get(&self) -> Result<Value, ContextError> {
439        let val = unsafe { shopify_function_input_get() };
440        Ok(Value {
441            nan_box: NanBox::from_bits(val),
442        })
443    }
444
445    /// Intern a string. This can lead to performance gains if you are using the same string multiple times,
446    /// as it saves unnecessary string copies. For example, if you are reading the same property from multiple objects,
447    /// or serializing the same key on an object, you can intern the string once and reuse it.
448    pub fn intern_utf8_str(&self, s: &str) -> InternedStringId {
449        let len = s.len();
450        let ptr = s.as_ptr();
451        let id = unsafe { shopify_function_intern_utf8_str(ptr, len) };
452        InternedStringId(id)
453    }
454}
455
456impl Default for Context {
457    fn default() -> Self {
458        Self::new()
459    }
460}
461
462/// Configures panics to write to the logging API.
463pub fn init_panic_handler() {
464    #[cfg(target_family = "wasm")]
465    std::panic::set_hook(Box::new(|info| {
466        let message = format!("{info}\n");
467        log::log_utf8_str(&message);
468    }));
469}
470
471#[cfg(test)]
472mod tests {
473    use std::thread;
474
475    use super::*;
476
477    // A CachedInternedStringId does not have to be static but in practice, the `shopify_function`
478    // macro makes it static so we should test with it being static.
479    static CACHED_INTERNED_STRING_ID: CachedInternedStringId = CachedInternedStringId::new("test");
480
481    #[test]
482    fn test_interned_string_id_cache() {
483        let mut context = Context::new_with_input(serde_json::json!({}));
484        let id = CACHED_INTERNED_STRING_ID.load();
485        let id2 = CACHED_INTERNED_STRING_ID.load();
486        context.write_interned_utf8_str(id).unwrap();
487        assert_eq!(id, id2);
488
489        // Test writing again with new context in same test to test same thread execution.
490        let mut context = Context::new_with_input(serde_json::json!({}));
491        context.write_interned_utf8_str(id).unwrap();
492    }
493
494    #[test]
495    fn test_interned_string_id_in_another_test() {
496        let mut context = Context::new_with_input(serde_json::json!({}));
497        let id = CACHED_INTERNED_STRING_ID.load();
498        context.write_interned_utf8_str(id).unwrap();
499    }
500
501    #[test]
502    fn test_interned_string_in_new_thread() {
503        let mut context = Context::new_with_input(serde_json::json!({}));
504        let id = CACHED_INTERNED_STRING_ID.load();
505        context.write_interned_utf8_str(id).unwrap();
506        // Test this still works across multiple threads.
507        thread::spawn(|| {
508            let mut context = Context::new_with_input(serde_json::json!({}));
509            let id = CACHED_INTERNED_STRING_ID.load();
510            context.write_interned_utf8_str(id).unwrap();
511        })
512        .join()
513        .unwrap();
514    }
515
516    #[test]
517    fn test_array_len_with_null_ptr() {
518        Context::new_with_input(serde_json::json!({}));
519        let value = Value {
520            nan_box: NanBox::array(0, NanBox::MAX_VALUE_LENGTH),
521        };
522        let len = value.array_len();
523        assert_eq!(len, None);
524    }
525
526    #[test]
527    fn test_array_len_with_non_length_eligible_nan_box() {
528        Context::new_with_input(serde_json::json!({}));
529        let value = Value {
530            nan_box: NanBox::null(),
531        };
532        let len = value.array_len();
533        assert_eq!(len, None);
534    }
535
536    #[test]
537    fn test_obj_len_with_null_ptr() {
538        Context::new_with_input(serde_json::json!({}));
539        let value = Value {
540            nan_box: NanBox::obj(0, NanBox::MAX_VALUE_LENGTH),
541        };
542        let len = value.obj_len();
543        assert_eq!(len, None);
544    }
545
546    #[test]
547    fn test_obj_len_with_non_length_eligible_nan_box() {
548        Context::new_with_input(serde_json::json!({}));
549        let value = Value {
550            nan_box: NanBox::null(),
551        };
552        let len = value.obj_len();
553        assert_eq!(len, None);
554    }
555}