#![warn(missing_docs)]
use shopify_function_wasm_api_core::read::{ErrorCode, NanBox, Val, ValueRef};
use std::{cell::RefCell, collections::HashMap};
pub mod log;
pub mod read;
pub mod write;
pub use read::Deserialize;
pub use write::Serialize;
#[cfg(target_family = "wasm")]
#[link(wasm_import_module = "shopify_function_v2")]
extern "C" {
fn shopify_function_input_get() -> Val;
fn shopify_function_input_get_val_len(scope: Val) -> usize;
fn shopify_function_input_read_utf8_str(src: usize, out: *mut u8, len: usize);
fn shopify_function_input_get_obj_prop(scope: Val, ptr: *const u8, len: usize) -> Val;
fn shopify_function_input_get_interned_obj_prop(
scope: Val,
interned_string_id: shopify_function_wasm_api_core::InternedStringId,
) -> Val;
fn shopify_function_input_get_at_index(scope: Val, index: usize) -> Val;
fn shopify_function_input_get_obj_key_at_index(scope: Val, index: usize) -> Val;
fn shopify_function_output_new_bool(bool: u32) -> usize;
fn shopify_function_output_new_null() -> usize;
fn shopify_function_output_new_i32(int: i32) -> usize;
fn shopify_function_output_new_f64(float: f64) -> usize;
fn shopify_function_output_new_utf8_str(ptr: *const u8, len: usize) -> usize;
fn shopify_function_output_new_interned_utf8_str(
id: shopify_function_wasm_api_core::InternedStringId,
) -> usize;
fn shopify_function_output_new_object(len: usize) -> usize;
fn shopify_function_output_finish_object() -> usize;
fn shopify_function_output_new_array(len: usize) -> usize;
fn shopify_function_output_finish_array() -> usize;
fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize);
fn shopify_function_intern_utf8_str(ptr: *const u8, len: usize) -> usize;
}
#[cfg(not(target_family = "wasm"))]
mod provider_fallback {
use super::Val;
use shopify_function_wasm_api_core::write::WriteResult;
pub(crate) unsafe fn shopify_function_input_get() -> Val {
shopify_function_provider::read::shopify_function_input_get()
}
pub(crate) unsafe fn shopify_function_input_get_val_len(scope: Val) -> usize {
shopify_function_provider::read::shopify_function_input_get_val_len(scope)
}
pub(crate) unsafe fn shopify_function_input_read_utf8_str(
src: usize,
out: *mut u8,
len: usize,
) {
let src = shopify_function_provider::read::shopify_function_input_get_utf8_str_addr(src);
std::ptr::copy(src as _, out, len);
}
pub(crate) unsafe fn shopify_function_input_get_obj_prop(
scope: Val,
ptr: *const u8,
len: usize,
) -> Val {
shopify_function_provider::read::shopify_function_input_get_obj_prop(scope, ptr as _, len)
}
pub(crate) unsafe fn shopify_function_input_get_interned_obj_prop(
scope: Val,
interned_string_id: shopify_function_wasm_api_core::InternedStringId,
) -> Val {
shopify_function_provider::read::shopify_function_input_get_interned_obj_prop(
scope,
interned_string_id,
)
}
pub(crate) unsafe fn shopify_function_input_get_at_index(scope: Val, index: usize) -> Val {
shopify_function_provider::read::shopify_function_input_get_at_index(scope, index)
}
pub(crate) unsafe fn shopify_function_input_get_obj_key_at_index(
scope: Val,
index: usize,
) -> Val {
shopify_function_provider::read::shopify_function_input_get_obj_key_at_index(scope, index)
}
pub(crate) unsafe fn shopify_function_output_new_bool(bool: u32) -> usize {
shopify_function_provider::write::shopify_function_output_new_bool(bool) as usize
}
pub(crate) unsafe fn shopify_function_output_new_null() -> usize {
shopify_function_provider::write::shopify_function_output_new_null() as usize
}
pub(crate) unsafe fn shopify_function_output_new_i32(int: i32) -> usize {
shopify_function_provider::write::shopify_function_output_new_i32(int) as usize
}
pub(crate) unsafe fn shopify_function_output_new_f64(float: f64) -> usize {
shopify_function_provider::write::shopify_function_output_new_f64(float) as usize
}
pub(crate) unsafe fn shopify_function_output_new_utf8_str(ptr: *const u8, len: usize) -> usize {
let result = shopify_function_provider::write::shopify_function_output_new_utf8_str(len);
let write_result = (result >> usize::BITS) as usize;
let dst = result as usize;
if write_result == WriteResult::Ok as usize {
std::ptr::copy(ptr as _, dst as _, len);
}
write_result
}
pub(crate) unsafe fn shopify_function_output_new_interned_utf8_str(
id: shopify_function_wasm_api_core::InternedStringId,
) -> usize {
shopify_function_provider::write::shopify_function_output_new_interned_utf8_str(id) as usize
}
pub(crate) unsafe fn shopify_function_output_new_object(len: usize) -> usize {
shopify_function_provider::write::shopify_function_output_new_object(len) as usize
}
pub(crate) unsafe fn shopify_function_output_finish_object() -> usize {
shopify_function_provider::write::shopify_function_output_finish_object() as usize
}
pub(crate) unsafe fn shopify_function_output_new_array(len: usize) -> usize {
shopify_function_provider::write::shopify_function_output_new_array(len) as usize
}
pub(crate) unsafe fn shopify_function_output_finish_array() -> usize {
shopify_function_provider::write::shopify_function_output_finish_array() as usize
}
pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) {
let addr = shopify_function_provider::log::shopify_function_log_new_utf8_str(len)
as *const [usize; 5];
let array = *addr;
let source_offset = array[0];
let dst_offset1 = array[1];
let len1 = array[2];
let dst_offset2 = array[3];
let len2 = array[4];
std::ptr::copy(ptr.add(source_offset) as _, dst_offset1 as _, len1);
std::ptr::copy(ptr.add(source_offset).add(len1), dst_offset2 as _, len2);
}
pub(crate) unsafe fn shopify_function_intern_utf8_str(ptr: *const u8, len: usize) -> usize {
let result = shopify_function_provider::shopify_function_intern_utf8_str(len);
let id = (result >> usize::BITS) as usize;
let dst = result as usize;
std::ptr::copy(ptr as _, dst as _, len);
id
}
}
#[cfg(not(target_family = "wasm"))]
use provider_fallback::*;
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct InternedStringId(shopify_function_wasm_api_core::InternedStringId);
impl InternedStringId {
fn as_usize(&self) -> usize {
self.0
}
}
thread_local! {
static INTERNED_STRING_CACHE: RefCell<HashMap::<&'static str, InternedStringId>> = RefCell::new(HashMap::new());
}
pub struct CachedInternedStringId {
value: &'static str,
}
impl CachedInternedStringId {
pub const fn new(value: &'static str) -> Self {
Self { value }
}
pub fn load(&self) -> InternedStringId {
INTERNED_STRING_CACHE.with_borrow_mut(|cache| {
*cache.entry(self.value).or_insert_with(|| {
InternedStringId(unsafe {
shopify_function_intern_utf8_str(self.value.as_ptr(), self.value.len())
})
})
})
}
}
#[derive(Copy, Clone)]
pub struct Value {
nan_box: NanBox,
}
impl Value {
fn new_child(&self, nan_box: NanBox) -> Self {
Self { nan_box }
}
pub fn intern_utf8_str(&self, s: &str) -> InternedStringId {
let len = s.len();
let ptr = s.as_ptr();
let id = unsafe { shopify_function_intern_utf8_str(ptr, len) };
InternedStringId(id)
}
pub fn as_bool(&self) -> Option<bool> {
match self.nan_box.try_decode() {
Ok(ValueRef::Bool(b)) => Some(b),
_ => None,
}
}
pub fn is_null(&self) -> bool {
matches!(self.nan_box.try_decode(), Ok(ValueRef::Null))
}
pub fn as_number(&self) -> Option<f64> {
match self.nan_box.try_decode() {
Ok(ValueRef::Number(n)) => Some(n),
_ => None,
}
}
pub fn as_string(&self) -> Option<String> {
match self.nan_box.try_decode() {
Ok(ValueRef::String { ptr, len }) => {
let len = if len == NanBox::MAX_VALUE_LENGTH {
unsafe { shopify_function_input_get_val_len(self.nan_box.to_bits()) }
} else {
len
};
let mut buf = vec![0; len];
unsafe { shopify_function_input_read_utf8_str(ptr as _, buf.as_mut_ptr(), len) };
Some(unsafe { String::from_utf8_unchecked(buf) })
}
_ => None,
}
}
pub fn is_obj(&self) -> bool {
matches!(self.nan_box.try_decode(), Ok(ValueRef::Object { .. }))
}
pub fn get_obj_prop(&self, prop: &str) -> Self {
let scope = unsafe {
shopify_function_input_get_obj_prop(self.nan_box.to_bits(), prop.as_ptr(), prop.len())
};
self.new_child(NanBox::from_bits(scope))
}
pub fn get_interned_obj_prop(&self, interned_string_id: InternedStringId) -> Self {
let scope = unsafe {
shopify_function_input_get_interned_obj_prop(
self.nan_box.to_bits(),
interned_string_id.as_usize(),
)
};
self.new_child(NanBox::from_bits(scope))
}
pub fn is_array(&self) -> bool {
matches!(self.nan_box.try_decode(), Ok(ValueRef::Array { .. }))
}
pub fn array_len(&self) -> Option<usize> {
match self.nan_box.try_decode() {
Ok(ValueRef::Array { len, .. }) => {
let len = if len == NanBox::MAX_VALUE_LENGTH {
unsafe { shopify_function_input_get_val_len(self.nan_box.to_bits()) }
} else {
len
};
if len == usize::MAX {
None
} else {
Some(len)
}
}
_ => None,
}
}
pub fn obj_len(&self) -> Option<usize> {
match self.nan_box.try_decode() {
Ok(ValueRef::Object { len, .. }) => {
let len = if len == NanBox::MAX_VALUE_LENGTH {
unsafe { shopify_function_input_get_val_len(self.nan_box.to_bits()) }
} else {
len
};
if len == usize::MAX {
None
} else {
Some(len)
}
}
_ => None,
}
}
pub fn get_at_index(&self, index: usize) -> Self {
let scope = unsafe { shopify_function_input_get_at_index(self.nan_box.to_bits(), index) };
self.new_child(NanBox::from_bits(scope))
}
pub fn get_obj_key_at_index(&self, index: usize) -> Option<String> {
match self.nan_box.try_decode() {
Ok(ValueRef::Object { .. }) => {
let scope = unsafe {
shopify_function_input_get_obj_key_at_index(self.nan_box.to_bits(), index)
};
let value = self.new_child(NanBox::from_bits(scope));
value.as_string()
}
_ => None,
}
}
pub fn as_error(&self) -> Option<ErrorCode> {
match self.nan_box.try_decode() {
Ok(ValueRef::Error(e)) => Some(e),
_ => None,
}
}
}
pub struct Context;
#[derive(Debug)]
#[non_exhaustive]
pub enum ContextError {
NullPointer,
}
impl std::error::Error for ContextError {}
impl std::fmt::Display for ContextError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContextError::NullPointer => write!(f, "Null pointer encountered"),
}
}
}
impl Context {
pub fn new() -> Self {
#[cfg(not(target_family = "wasm"))]
panic!("Cannot run in non-WASM environment; use `new_with_input` instead");
#[cfg(target_family = "wasm")]
{
Self
}
}
#[cfg(not(target_family = "wasm"))]
pub fn new_with_input(input: serde_json::Value) -> Self {
let bytes = rmp_serde::to_vec(&input).unwrap();
shopify_function_provider::initialize_from_msgpack_bytes(bytes);
Self
}
pub fn input_get(&self) -> Result<Value, ContextError> {
let val = unsafe { shopify_function_input_get() };
Ok(Value {
nan_box: NanBox::from_bits(val),
})
}
pub fn intern_utf8_str(&self, s: &str) -> InternedStringId {
let len = s.len();
let ptr = s.as_ptr();
let id = unsafe { shopify_function_intern_utf8_str(ptr, len) };
InternedStringId(id)
}
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
pub fn init_panic_handler() {
#[cfg(target_family = "wasm")]
std::panic::set_hook(Box::new(|info| {
let message = format!("{info}\n");
log::log_utf8_str(&message);
}));
}
#[cfg(test)]
mod tests {
use std::thread;
use super::*;
static CACHED_INTERNED_STRING_ID: CachedInternedStringId = CachedInternedStringId::new("test");
#[test]
fn test_interned_string_id_cache() {
let mut context = Context::new_with_input(serde_json::json!({}));
let id = CACHED_INTERNED_STRING_ID.load();
let id2 = CACHED_INTERNED_STRING_ID.load();
context.write_interned_utf8_str(id).unwrap();
assert_eq!(id, id2);
let mut context = Context::new_with_input(serde_json::json!({}));
context.write_interned_utf8_str(id).unwrap();
}
#[test]
fn test_interned_string_id_in_another_test() {
let mut context = Context::new_with_input(serde_json::json!({}));
let id = CACHED_INTERNED_STRING_ID.load();
context.write_interned_utf8_str(id).unwrap();
}
#[test]
fn test_interned_string_in_new_thread() {
let mut context = Context::new_with_input(serde_json::json!({}));
let id = CACHED_INTERNED_STRING_ID.load();
context.write_interned_utf8_str(id).unwrap();
thread::spawn(|| {
let mut context = Context::new_with_input(serde_json::json!({}));
let id = CACHED_INTERNED_STRING_ID.load();
context.write_interned_utf8_str(id).unwrap();
})
.join()
.unwrap();
}
#[test]
fn test_array_len_with_null_ptr() {
Context::new_with_input(serde_json::json!({}));
let value = Value {
nan_box: NanBox::array(0, NanBox::MAX_VALUE_LENGTH),
};
let len = value.array_len();
assert_eq!(len, None);
}
#[test]
fn test_array_len_with_non_length_eligible_nan_box() {
Context::new_with_input(serde_json::json!({}));
let value = Value {
nan_box: NanBox::null(),
};
let len = value.array_len();
assert_eq!(len, None);
}
#[test]
fn test_obj_len_with_null_ptr() {
Context::new_with_input(serde_json::json!({}));
let value = Value {
nan_box: NanBox::obj(0, NanBox::MAX_VALUE_LENGTH),
};
let len = value.obj_len();
assert_eq!(len, None);
}
#[test]
fn test_obj_len_with_non_length_eligible_nan_box() {
Context::new_with_input(serde_json::json!({}));
let value = Value {
nan_box: NanBox::null(),
};
let len = value.obj_len();
assert_eq!(len, None);
}
}