#elm #binding #function #call

elm-rust-binding

Call Elm functions from Rust in an ergonomic way

2 unstable releases

0.2.0 Nov 19, 2024
0.1.0 Nov 18, 2024

#596 in Web programming

Download history 265/week @ 2024-11-15 51/week @ 2024-11-22 4/week @ 2024-11-29 3/week @ 2024-12-06

323 downloads per month

BSD-3-Clause

21KB
389 lines

Elm Rust Binding

This crate offers a way to conveniently call an Elm function from Rust. The main motivation here is for testing purposes:

  • You have some logic in your Elm frontend that cannot be moved to the backend (because e.g. it is called in a hot loop)
  • You have related logic in your Rust backend

Then you can call your Elm code in a Rust test with this crate to verify the two implementations are in sync. The performance this crate offers is NOT optimal - do not use this for Interop in production code.

Example

Rust side

use elm_rust_binding::{ElmRoot, Result};

#[test]
fn call_elm() -> Result<()> {
    let mut elm_root = ElmRoot::new("../frontend/src")?;
    let mut square_fn = elm_root.prepare("Math.square")?;
    let squared = square_fn.call(4)?;
    assert_eq!(squared, 16);
    Ok(())
}

Elm side

-- In /frontend/src/Math.elm
module Math exposing (square)


square : Int -> Int
square n = n * n

Note that you can call the function multiple times, which will improve performance over creating a new ElmRoot and ElmFunctionHandle. This is especially useful for fuzz/property-based testing

Implementation Details

How does this work under the hood? It essentially boils down to 6 steps:

  1. Infer the Elm input and output types based on the Rust type annotations (i32 -> Int, bool -> Bool, etc.).
  2. Generate an .elm file with a Platform.worker main function. It will accept the input type over its flags and return the output type via a port. Internally it will call the specified function by importing it.
  3. Invoke the Elm compiler on the generated file, producing a .js file.
  4. Make the produced .js file ESM-compatible.
  5. Load the module into a Deno runtime.
  6. Initialize the Elm application every time .call is invoked by passing the input argument as flags and getting the output via .subscribe.

The generated files are removed automatically and are postfixed with a UUID to prevent different invocations messing with each other. If you call .debug() on the ElmRoot, the files are not removed to help debugging issues.

Dependencies

~95MB
~2M SLoC