Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Binary file added providers/shopify_function_v0.0.1.wasm
Binary file not shown.
125 changes: 111 additions & 14 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
module: &Module,
engine: &Engine,
Expand All @@ -28,6 +38,7 @@ fn import_modules<T>(
) {
let imported_modules: HashSet<String> =
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"));

Expand All @@ -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;
Expand Down Expand Up @@ -114,6 +126,7 @@ pub fn run(params: FunctionRunParams) -> Result<FunctionRunResult> {
export,
profile_opts,
scale_factor,
use_msgpack,
} = params;

let engine = Engine::new(
Expand All @@ -126,7 +139,23 @@ pub fn run(params: FunctionRunParams) -> Result<FunctionRunResult> {
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::<serde_json::Value>(&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();

Expand Down Expand Up @@ -203,24 +232,37 @@ pub fn run(params: FunctionRunParams) -> Result<FunctionRunResult> {
.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(),
Expand All @@ -238,6 +280,36 @@ pub fn run(params: FunctionRunParams) -> Result<FunctionRunResult> {
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::<serde_json::Value>(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;
Expand Down Expand Up @@ -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());
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Binary file not shown.
24 changes: 24 additions & 0 deletions tests/fixtures/input/wasm_api_function_input.json
Original file line number Diff line number Diff line change
@@ -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\"]}"
}
}
}
Loading