shopify_function_wasm_api/
lib.rs1#![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 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 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 fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize);
68
69 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 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 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 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 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#[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
197thread_local! {
199 static INTERNED_STRING_CACHE: RefCell<HashMap::<&'static str, InternedStringId>> = RefCell::new(HashMap::new());
200}
201
202pub struct CachedInternedStringId {
204 value: &'static str,
205}
206
207impl CachedInternedStringId {
208 pub const fn new(value: &'static str) -> Self {
210 Self { value }
211 }
212
213 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#[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 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 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 pub fn is_null(&self) -> bool {
263 matches!(self.nan_box.try_decode(), Ok(ValueRef::Null))
264 }
265
266 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 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 pub fn is_obj(&self) -> bool {
293 matches!(self.nan_box.try_decode(), Ok(ValueRef::Object { .. }))
294 }
295
296 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 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 pub fn is_array(&self) -> bool {
317 matches!(self.nan_box.try_decode(), Ok(ValueRef::Array { .. }))
318 }
319
320 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 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 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 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 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
387pub struct Context;
391
392#[derive(Debug)]
394#[non_exhaustive]
395pub enum ContextError {
396 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 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 #[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 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 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
462pub 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 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 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 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}