diff --git a/build.rs b/build.rs index b8a7c775..b7cc054c 100644 --- a/build.rs +++ b/build.rs @@ -3,4 +3,5 @@ fn main() { println!("cargo:rerun-if-changed=providers/javy_quickjs_provider_v2.wasm"); println!("cargo:rerun-if-changed=providers/javy_quickjs_provider_v3.wasm"); println!("cargo:rerun-if-changed=providers/shopify_functions_javy_v1.wasm"); + println!("cargo:rerun-if-changed=providers/shopify_function_v0.0.1.wasm"); } diff --git a/providers/shopify_function_v0.0.1.wasm b/providers/shopify_function_v0.0.1.wasm new file mode 100755 index 00000000..3c4b9c2f Binary files /dev/null and b/providers/shopify_function_v0.0.1.wasm differ diff --git a/src/engine.rs b/src/engine.rs index f7f68602..cfc8f093 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -20,6 +20,16 @@ pub struct ProfileOpts { #[folder = "providers/"] struct StandardProviders; +const WASM_API_PROVIDER_PREFIX: &str = "shopify_function_v"; + +fn uses_wasm_api_provider(module: &Module) -> bool { + module + .imports() + .any(|i| { + i.module().starts_with(WASM_API_PROVIDER_PREFIX) + }) +} + fn import_modules( module: &Module, engine: &Engine, @@ -28,6 +38,7 @@ fn import_modules( ) { let imported_modules: HashSet = module.imports().map(|i| i.module().to_string()).collect(); + imported_modules.iter().for_each(|module_name| { let imported_module_bytes = StandardProviders::get(&format!("{module_name}.wasm")); @@ -52,6 +63,7 @@ pub struct FunctionRunParams<'a> { pub export: &'a str, pub profile_opts: Option<&'a ProfileOpts>, pub scale_factor: f64, + pub use_msgpack: bool, } const STARTING_FUEL: u64 = u64::MAX; @@ -114,6 +126,7 @@ pub fn run(params: FunctionRunParams) -> Result { export, profile_opts, scale_factor, + use_msgpack, } = params; let engine = Engine::new( @@ -126,7 +139,23 @@ pub fn run(params: FunctionRunParams) -> Result { let module = Module::from_file(&engine, &function_path) .map_err(|e| anyhow!("Couldn't load the Function {:?}: {}", &function_path, e))?; - let input_stream = wasi_common::pipe::ReadPipe::new(Cursor::new(input.clone())); + let uses_wasm_api = if use_msgpack { + true + } else { + uses_wasm_api_provider(&module) + }; + + let processed_input = if use_msgpack { + let json = serde_json::from_slice::(&input) + .map_err(|e| anyhow!("Invalid input JSON for function: {}", e))?; + + rmp_serde::to_vec(&json) + .map_err(|e| anyhow!("Couldn't convert JSON to MessagePack: {}", e))? + } else { + input.clone() + }; + + let input_stream = wasi_common::pipe::ReadPipe::new(Cursor::new(processed_input)); let output_stream = wasi_common::pipe::WritePipe::new_in_memory(); let error_stream = wasi_common::pipe::WritePipe::new_in_memory(); @@ -203,24 +232,37 @@ pub fn run(params: FunctionRunParams) -> Result { .expect("Output stream reference still exists") .into_inner(); - let output: FunctionOutput = match serde_json::from_slice(&raw_output) { - Ok(json_output) => JsonOutput(json_output), - Err(error) => InvalidJsonOutput(InvalidOutput { - stdout: std::str::from_utf8(&raw_output) - .map_err(|e| anyhow!("Couldn't print Function Output: {}", e)) - .unwrap() - .to_owned(), - error: error.to_string(), - }), + let output: FunctionOutput = if uses_wasm_api { + if raw_output.is_empty() { + JsonOutput(serde_json::Value::Null) + } else { + try_parse_as_msgpack(&raw_output) + } + } else { + match serde_json::from_slice(&raw_output) { + Ok(json_output) => JsonOutput(json_output), + Err(error) => { + match std::str::from_utf8(&raw_output) { + Ok(text) if !text.is_empty() => JsonOutput(serde_json::Value::String(text.to_owned())), + _ => InvalidJsonOutput(InvalidOutput { + stdout: String::from_utf8_lossy(&raw_output).into_owned(), + error: error.to_string(), + }) + } + } + } }; let name = function_path.file_name().unwrap().to_str().unwrap(); let size = function_path.metadata()?.len() / 1024; - let parsed_input = - String::from_utf8(input).map_err(|e| anyhow!("Couldn't parse input: {}", e))?; - - let function_run_input = serde_json::from_str(&parsed_input)?; + let function_run_input = if uses_wasm_api { + serde_json::from_slice(&input)? + } else { + String::from_utf8(input) + .map_err(|e| anyhow!("Couldn't parse input: {}", e)) + .and_then(|s| serde_json::from_str(&s).map_err(Into::into))? + }; let function_run_result = FunctionRunResult { name: name.to_string(), @@ -238,6 +280,36 @@ pub fn run(params: FunctionRunParams) -> Result { Ok(function_run_result) } +fn try_parse_as_msgpack(raw_output: &[u8]) -> FunctionOutput { + if !raw_output.is_empty() { + let first_char = raw_output[0] as char; + if first_char == '{' || first_char == '[' { + if let Ok(json_output) = serde_json::from_slice(raw_output) { + return JsonOutput(json_output); + } + } + } + + match rmp_serde::from_slice::(raw_output) { + Ok(json_output) => { + if let Some(n) = json_output.as_u64() { + if n < 127 && raw_output.len() > 1 && raw_output[0] as u64 == n { + return JsonOutput(serde_json::Value::String( + String::from_utf8_lossy(raw_output).into_owned() + )); + } + } + JsonOutput(json_output) + }, + Err(msgpack_error) => { + InvalidJsonOutput(InvalidOutput { + stdout: String::from_utf8_lossy(raw_output).into_owned(), + error: msgpack_error.to_string(), + }) + } + } +} + #[cfg(test)] mod tests { use colored::Colorize; @@ -383,4 +455,29 @@ mod tests { file_path.metadata().unwrap().len() / 1024 ); } + + #[test] + fn test_wasm_api_provider_detection() { + let module_path = Path::new("tests/fixtures/build/js_function.wasm"); + let module = Module::from_file(&Engine::default(), module_path).unwrap(); + assert!(!uses_wasm_api_provider(&module)); + + let wasm_api_module_path = Path::new("tests/fixtures/build/wasm_api_function.merged.wasm"); + let wasm_api_module = Module::from_file(&Engine::default(), wasm_api_module_path).unwrap(); + assert!(uses_wasm_api_provider(&wasm_api_module)); + } + + #[test] + fn test_wasm_api_function() { + let input = include_bytes!("../tests/fixtures/input/wasm_api_function_input.json").to_vec(); + let function_run_result = run(FunctionRunParams { + function_path: Path::new("tests/fixtures/build/wasm_api_function.merged.wasm").to_path_buf(), + input, + export: DEFAULT_EXPORT, + use_msgpack: true, + ..Default::default() + }); + + assert!(function_run_result.is_ok()); + } } diff --git a/src/main.rs b/src/main.rs index 383b2284..6012afe2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -184,6 +184,7 @@ fn main() -> Result<()> { export: opts.export.as_ref(), profile_opts: profile_opts.as_ref(), scale_factor, + use_msgpack: opts.codec == Codec::JsonToMessagepack, })?; if opts.json { diff --git a/tests/fixtures/build/wasm_api_function.merged.wasm b/tests/fixtures/build/wasm_api_function.merged.wasm new file mode 100644 index 00000000..560d41c3 Binary files /dev/null and b/tests/fixtures/build/wasm_api_function.merged.wasm differ diff --git a/tests/fixtures/input/wasm_api_function_input.json b/tests/fixtures/input/wasm_api_function_input.json new file mode 100644 index 00000000..5e4177f4 --- /dev/null +++ b/tests/fixtures/input/wasm_api_function_input.json @@ -0,0 +1,24 @@ +{ + "cart": { + "lines": [ + { + "merchandise": { + "id": "gid://shopify/ProductVariant/1", + "title": "Sample Product" + }, + "quantity": 2, + "cost": { + "totalAmount": { + "amount": "24.99", + "currencyCode": "USD" + } + } + } + ] + }, + "discounts": { + "metafield": { + "value": "{\"percentage\":20,\"applicable_items\":[\"gid://shopify/ProductVariant/1\"]}" + } + } +} \ No newline at end of file