app_bridge is a Ruby gem designed to facilitate communication with WebAssembly components that implement the WIT specification standout:app. This gem is developed for use in Standout's products.
Add the following line to your Gemfile:
gem 'app_bridge'Then, install the gem by running:
bundle installTo use this gem, you need a WebAssembly component that adheres to the specification defined in ext/app_bridge/wit/v4/world.wit (or an older supported version like v3).
You can check out the example components in spec/fixtures/components to see how such a component should be structured.
Once you have a WebAssembly component, you can use the gem as follows:
require 'app_bridge'
app = AppBridge::App.new('path/to/your/component.wasm')
app.triggers # => ['trigger1', 'trigger2']The gem provides a file.normalize function for handling files in connectors. It automatically detects the input format (URL, data URI, or base64) and returns normalized file data.
import { normalize } from 'standout:app/[email protected]';
// Normalize any file source - input type is auto-detected
const fileData = normalize(
input.fileUrl, // URL, data URI, or base64 string
[["Authorization", token]], // Optional headers for URL requests
"invoice.pdf" // Optional filename override
);
// Returns: { base64, contentType, filename }
// Include in your action output
return {
serializedOutput: JSON.stringify({
document: fileData // Will be replaced with blob ID by platform
})
};use crate::standout::app::file::normalize;
let file_data = normalize(
&input.file_url,
Some(&[("Authorization".to_string(), token)]),
Some("invoice.pdf"),
)?;
// file_data contains { base64, content_type, filename }Mark file fields with format: "file-output" so the platform knows to process them:
{
"properties": {
"document": {
"type": "object",
"format": "file-output"
}
}
}Configure the file uploader in your Rails app:
AppBridge.file_uploader = ->(file_data) {
blob = ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(Base64.decode64(file_data['base64'])),
filename: file_data['filename'],
content_type: file_data['content_type']
)
blob.signed_id
}The gem automatically replaces file data with the return value (in this example blob IDs) before returning the action response.
The gem supports multi-version WIT interfaces, allowing connectors built against older WIT versions to continue working when the gem is updated.
When loading a WASM component, the gem automatically detects which WIT version it was built against:
- V4 components (current,
standout:[email protected]): Full feature support including thefileinterface - V3 components (
standout:[email protected]): Legacy support without file interface
When adding a new WIT version (e.g., v5), follow these steps:
Copy the latest version and modify:
cp -r ext/app_bridge/wit/v4 ext/app_bridge/wit/v5Edit ext/app_bridge/wit/v5/world.wit:
- Update the package version:
package standout:[email protected]; - Add new interfaces or modify existing ones
In ext/app_bridge/src/component.rs, add after the existing modules:
pub mod v5 {
wasmtime::component::bindgen!({
path: "./wit/v5",
world: "bridge",
});
}Add the conversion macro call:
impl_conversions!(v5);Update the enum:
pub enum BridgeWrapper {
V3(v3::Bridge),
V4(v4::Bridge),
V5(v5::Bridge), // <-- add this
}Add an arm to each bridge_method! macro expansion. In the macro definitions, add:
BridgeWrapper::V5(b) => {
let r = b.$interface().$method(store, ...)?;
Ok(r.map(Into::into).map_err(Into::into))
}In build_linker():
// v5: http + environment + file + new_feature
v5::standout::app::http::add_to_linker(&mut linker, |s| s)?;
v5::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
v5::standout::app::file::add_to_linker(&mut linker, |s| s)?;
v5::standout::app::new_feature::add_to_linker(&mut linker, |s| s)?; // if applicableIn app(), add v5 at the top (newest first):
// v5 (newest)
if let Ok(instance) = v5::Bridge::instantiate(&mut *store, &component, &linker) {
return Ok(BridgeWrapper::V5(instance));
}
// v4
if let Ok(instance) = v4::Bridge::instantiate(&mut *store, &component, &linker) {
return Ok(BridgeWrapper::V4(instance));
}
// v3 (oldest)
// ...In app_state.rs:
impl_host_for_version!(v5);In request_builder.rs:
impl_host_request_builder!(v5);
impl_http_type_conversions!(v5);In file_ops.rs (only if v5 includes the file interface):
impl_file_host!(v5);If the new version introduces entirely new interfaces (not just file), implement the Host trait:
impl v5::standout::app::new_feature::Host for AppState {
fn some_function(&mut self, ...) -> ... {
// implementation
}
}- No forced rebuilds: Existing connectors continue to work after gem updates
- Gradual migration: Update connectors to new WIT versions at your own pace
- Type safety: Each version's types are converted to the latest version internally
To contribute or modify this gem, ensure you have the following dependencies installed:
- Ruby 3.3.0 (or later)
- Rust 1.84.0 (or later)
Run the following command to setup and install additional dependencies:
bin/setupThen, to compile example applications, run tests, and perform syntax checks, execute:
rake- Interactive Console: Run
bin/consoleto interactively test the code. - Full Test Suite & Linting: Run
raketo compile, execute tests, and perform syntax checks. - Run Tests Only: Execute
rake specto run only the test suite. - Linting: Run
rake rubocopto check code style and formatting. - Compile Example Applications: Use
rake fixturesto build the example apps.
To install this gem locally for testing purposes, run:
bundle exec rake installTo release a new version of the gem, update the version number in lib/app_bridge/version.rb and in ext/app_bridge/Cargo.toml. They should be the same.
Then push the changes to the repository and create a new release on GitHub. The gem will be automatically built and published to RubyGems.