app_bridge 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Cargo.toml CHANGED
@@ -5,4 +5,4 @@
5
5
  [workspace]
6
6
  members = ["./ext/app_bridge"]
7
7
  resolver = "2"
8
- exclude = ["./spec/fixtures/components/rust_app"]
8
+ exclude = ["./spec/fixtures/components/rust_app", "./spec/fixtures/components/rust_app_v3"]
data/README.md CHANGED
@@ -18,7 +18,7 @@ bundle install
18
18
 
19
19
  ## Usage
20
20
 
21
- To use this gem, you need a WebAssembly component that adheres to the specification defined in `ext/app_bridge/wit/world.wit`.
21
+ To 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`).
22
22
 
23
23
  You can check out the example components in `spec/fixtures/components` to see how such a component should be structured.
24
24
 
@@ -31,7 +31,218 @@ app = AppBridge::App.new('path/to/your/component.wasm')
31
31
  app.triggers # => ['trigger1', 'trigger2']
32
32
  ```
33
33
 
34
- More documentation and features will be added as the gem evolves.
34
+ ### File Handling
35
+
36
+ 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.
37
+
38
+ #### In your WASM connector (JavaScript):
39
+
40
+ ```javascript
41
+ import { normalize } from 'standout:app/file@4.0.0';
42
+
43
+ // Normalize any file source - input type is auto-detected
44
+ const fileData = normalize(
45
+ input.fileUrl, // URL, data URI, or base64 string
46
+ [["Authorization", token]], // Optional headers for URL requests
47
+ "invoice.pdf" // Optional filename override
48
+ );
49
+
50
+ // Returns: { base64, contentType, filename }
51
+
52
+ // Include in your action output
53
+ return {
54
+ serializedOutput: JSON.stringify({
55
+ document: fileData // Will be replaced with blob ID by platform
56
+ })
57
+ };
58
+ ```
59
+
60
+ #### In your WASM connector (Rust):
61
+
62
+ ```rust
63
+ use crate::standout::app::file::normalize;
64
+
65
+ let file_data = normalize(
66
+ &input.file_url,
67
+ Some(&[("Authorization".to_string(), token)]),
68
+ Some("invoice.pdf"),
69
+ )?;
70
+
71
+ // file_data contains { base64, content_type, filename }
72
+ ```
73
+
74
+ #### Output schema:
75
+
76
+ Mark file fields with `format: "file-output"` so the platform knows to process them:
77
+
78
+ ```json
79
+ {
80
+ "properties": {
81
+ "document": {
82
+ "type": "object",
83
+ "format": "file-output"
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ #### Platform configuration:
90
+
91
+ Configure the file uploader in your Rails app:
92
+
93
+ ```ruby
94
+ AppBridge.file_uploader = ->(file_data) {
95
+ blob = ActiveStorage::Blob.create_and_upload!(
96
+ io: StringIO.new(Base64.decode64(file_data['base64'])),
97
+ filename: file_data['filename'],
98
+ content_type: file_data['content_type']
99
+ )
100
+ blob.signed_id
101
+ }
102
+ ```
103
+
104
+ The gem automatically replaces file data with the return value (in this example blob IDs) before returning the action response.
105
+
106
+ ## Backward Compatibility
107
+
108
+ The gem supports **multi-version WIT interfaces**, allowing connectors built against older WIT versions to continue working when the gem is updated.
109
+
110
+ ### How it works
111
+
112
+ When loading a WASM component, the gem automatically detects which WIT version it was built against:
113
+
114
+ 1. **V4 components** (current, `standout:app@4.0.0`): Full feature support including the `file` interface
115
+ 2. **V3 components** (`standout:app@3.0.0`): Legacy support without file interface
116
+
117
+ ### Adding support for new WIT versions
118
+
119
+ When adding a new WIT version (e.g., v5), follow these steps:
120
+
121
+ #### 1. Create the WIT file
122
+
123
+ Copy the latest version and modify:
124
+
125
+ ```bash
126
+ cp -r ext/app_bridge/wit/v4 ext/app_bridge/wit/v5
127
+ ```
128
+
129
+ Edit `ext/app_bridge/wit/v5/world.wit`:
130
+ - Update the package version: `package standout:app@5.0.0;`
131
+ - Add new interfaces or modify existing ones
132
+
133
+ #### 2. Add the bindgen module
134
+
135
+ In `ext/app_bridge/src/component.rs`, add after the existing modules:
136
+
137
+ ```rust
138
+ pub mod v5 {
139
+ wasmtime::component::bindgen!({
140
+ path: "./wit/v5",
141
+ world: "bridge",
142
+ });
143
+ }
144
+ ```
145
+
146
+ #### 3. Generate type conversions
147
+
148
+ Add the conversion macro call:
149
+
150
+ ```rust
151
+ impl_conversions!(v5);
152
+ ```
153
+
154
+ #### 4. Add BridgeWrapper variant
155
+
156
+ Update the enum:
157
+
158
+ ```rust
159
+ pub enum BridgeWrapper {
160
+ V3(v3::Bridge),
161
+ V4(v4::Bridge),
162
+ V5(v5::Bridge), // <-- add this
163
+ }
164
+ ```
165
+
166
+ Add an arm to each `bridge_method!` macro expansion. In the macro definitions, add:
167
+
168
+ ```rust
169
+ BridgeWrapper::V5(b) => {
170
+ let r = b.$interface().$method(store, ...)?;
171
+ Ok(r.map(Into::into).map_err(Into::into))
172
+ }
173
+ ```
174
+
175
+ #### 5. Register interfaces in the linker
176
+
177
+ In `build_linker()`:
178
+
179
+ ```rust
180
+ // v5: http + environment + file + new_feature
181
+ v5::standout::app::http::add_to_linker(&mut linker, |s| s)?;
182
+ v5::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
183
+ v5::standout::app::file::add_to_linker(&mut linker, |s| s)?;
184
+ v5::standout::app::new_feature::add_to_linker(&mut linker, |s| s)?; // if applicable
185
+ ```
186
+
187
+ #### 6. Update the instantiation chain
188
+
189
+ In `app()`, add v5 at the top (newest first):
190
+
191
+ ```rust
192
+ // v5 (newest)
193
+ if let Ok(instance) = v5::Bridge::instantiate(&mut *store, &component, &linker) {
194
+ return Ok(BridgeWrapper::V5(instance));
195
+ }
196
+
197
+ // v4
198
+ if let Ok(instance) = v4::Bridge::instantiate(&mut *store, &component, &linker) {
199
+ return Ok(BridgeWrapper::V4(instance));
200
+ }
201
+
202
+ // v3 (oldest)
203
+ // ...
204
+ ```
205
+
206
+ #### 7. Register Host implementations
207
+
208
+ In `app_state.rs`:
209
+
210
+ ```rust
211
+ impl_host_for_version!(v5);
212
+ ```
213
+
214
+ In `request_builder.rs`:
215
+
216
+ ```rust
217
+ impl_host_request_builder!(v5);
218
+ impl_http_type_conversions!(v5);
219
+ ```
220
+
221
+ #### 8. If the version has the file interface
222
+
223
+ In `file_ops.rs` (only if v5 includes the `file` interface):
224
+
225
+ ```rust
226
+ impl_file_host!(v5);
227
+ ```
228
+
229
+ #### 9. If adding new host functions
230
+
231
+ If the new version introduces entirely new interfaces (not just `file`), implement the `Host` trait:
232
+
233
+ ```rust
234
+ impl v5::standout::app::new_feature::Host for AppState {
235
+ fn some_function(&mut self, ...) -> ... {
236
+ // implementation
237
+ }
238
+ }
239
+ ```
240
+
241
+ ### Benefits
242
+
243
+ - **No forced rebuilds**: Existing connectors continue to work after gem updates
244
+ - **Gradual migration**: Update connectors to new WIT versions at your own pace
245
+ - **Type safety**: Each version's types are converted to the latest version internally
35
246
 
36
247
  ## Development
37
248
 
@@ -2,7 +2,7 @@
2
2
  name = "app_bridge"
3
3
  # When updating the version, please also update the version in the
4
4
  # lib/app_bridge/version.rb file to keep them in sync.
5
- version = "3.0.0"
5
+ version = "4.0.0"
6
6
  edition = "2021"
7
7
  authors = ["Alexander Ross <ross@standout.se>"]
8
8
  publish = false
@@ -12,9 +12,13 @@ crate-type = ["cdylib"]
12
12
 
13
13
  [dependencies]
14
14
  magnus = { version = "0.7.1" }
15
- wasmtime = "32.0.0"
16
- wasmtime-wasi = "32.0.0"
15
+ wasmtime = "33.0.2"
16
+ wasmtime-wasi = "33.0.2"
17
17
  reqwest = { version = "0.12", features = ["blocking", "json", "native-tls-vendored"] }
18
+ base64 = "0.22"
19
+ infer = "0.16"
20
+ serde = { version = "1.0", features = ["derive"] }
21
+ serde_json = "1.0"
18
22
 
19
23
  [dev-dependencies]
20
- httpmock = "0.7.0"
24
+ httpmock = "0.8.2"
@@ -1,10 +1,10 @@
1
- use crate::component::standout;
2
- use crate::component::standout::app::http::Request;
1
+ use crate::component::{v3, v4};
2
+ use crate::component::v4::standout::app::http::Request;
3
3
  use reqwest::blocking::Client;
4
4
  use std::collections::HashMap;
5
5
  use std::sync::{Arc, Mutex};
6
6
  use wasmtime::component::ResourceTable;
7
- use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView, IoView};
7
+ use wasmtime_wasi::p2::{WasiCtx, WasiCtxBuilder, WasiView, IoView};
8
8
 
9
9
  pub struct AppState {
10
10
  ctx: WasiCtx,
@@ -28,20 +28,36 @@ impl AppState {
28
28
  }
29
29
  }
30
30
 
31
- impl standout::app::http::Host for AppState {
32
- // Impl http host methods here
33
- }
31
+ // ============================================================================
32
+ // Macro to implement identical Host traits for multiple WIT versions
33
+ // ============================================================================
34
34
 
35
- impl standout::app::environment::Host for AppState {
36
- fn env_vars(&mut self) -> Vec<(String, String)> {
37
- self.environment_variables.clone().into_iter().collect()
38
- }
35
+ /// Implements http::Host and environment::Host for a given WIT version module.
36
+ /// These implementations are identical across versions.
37
+ macro_rules! impl_host_for_version {
38
+ ($version:ident) => {
39
+ impl $version::standout::app::http::Host for AppState {}
39
40
 
40
- fn env_var(&mut self, name: String) -> Option<String> {
41
- self.environment_variables.get(&name).cloned()
42
- }
41
+ impl $version::standout::app::environment::Host for AppState {
42
+ fn env_vars(&mut self) -> Vec<(String, String)> {
43
+ self.environment_variables.clone().into_iter().collect()
44
+ }
45
+
46
+ fn env_var(&mut self, name: String) -> Option<String> {
47
+ self.environment_variables.get(&name).cloned()
48
+ }
49
+ }
50
+ };
43
51
  }
44
52
 
53
+ // Apply to both versions
54
+ impl_host_for_version!(v3);
55
+ impl_host_for_version!(v4);
56
+
57
+ // ============================================================================
58
+ // WASI implementations
59
+ // ============================================================================
60
+
45
61
  impl IoView for AppState {
46
62
  fn table(&mut self) -> &mut ResourceTable {
47
63
  &mut self.table
@@ -1,25 +1,252 @@
1
- use std::result::Result::Ok;
2
1
  use std::collections::HashMap;
3
- use wasmtime::component::{bindgen, Component, Linker};
2
+ use std::result::Result::Ok;
3
+ use wasmtime::component::{Component, Linker};
4
4
  use wasmtime::{Engine, Result, Store};
5
- use wasmtime_wasi::WasiCtxBuilder;
6
-
7
- bindgen!({
8
- path: "./wit",
9
- world: "bridge",
10
- });
5
+ use wasmtime_wasi::p2::WasiCtxBuilder;
11
6
 
12
7
  use crate::app_state::AppState;
8
+ use crate::types::{
9
+ ActionContext, ActionResponse, AppError, Connection, ErrorCode, TriggerContext,
10
+ TriggerEvent, TriggerResponse,
11
+ };
12
+
13
+ // ============================================================================
14
+ // WIT version modules
15
+ //
16
+ // To add a new version:
17
+ // 1. Add a new pub mod vN { bindgen!(...) }
18
+ // 2. Add impl_conversions!(vN) below
19
+ // 3. Add variant to BridgeWrapper enum
20
+ // 4. Add to build_linker() and app() functions
21
+ // 5. Add arm to each bridge_method! in BridgeWrapper impl
22
+ // ============================================================================
23
+
24
+ pub mod v3 {
25
+ wasmtime::component::bindgen!({
26
+ path: "./wit/v3",
27
+ world: "bridge",
28
+ });
29
+ }
30
+
31
+ pub mod v4 {
32
+ wasmtime::component::bindgen!({
33
+ path: "./wit/v4",
34
+ world: "bridge",
35
+ });
36
+ }
37
+
38
+ // ============================================================================
39
+ // Version conversion macro - generates From impls for a version module
40
+ // ============================================================================
41
+
42
+ macro_rules! impl_conversions {
43
+ ($v:ident) => {
44
+ // ErrorCode: version → canonical
45
+ impl From<$v::standout::app::types::ErrorCode> for ErrorCode {
46
+ fn from(c: $v::standout::app::types::ErrorCode) -> Self {
47
+ use $v::standout::app::types::ErrorCode as V;
48
+ match c {
49
+ V::Unauthenticated => Self::Unauthenticated,
50
+ V::Forbidden => Self::Forbidden,
51
+ V::Misconfigured => Self::Misconfigured,
52
+ V::Unsupported => Self::Unsupported,
53
+ V::RateLimit => Self::RateLimit,
54
+ V::Timeout => Self::Timeout,
55
+ V::Unavailable => Self::Unavailable,
56
+ V::InternalError => Self::InternalError,
57
+ V::MalformedResponse => Self::MalformedResponse,
58
+ V::Other => Self::Other,
59
+ V::CompleteWorkflow => Self::CompleteWorkflow,
60
+ V::CompleteParent => Self::CompleteParent,
61
+ }
62
+ }
63
+ }
64
+
65
+ // AppError: version → canonical
66
+ impl From<$v::standout::app::types::AppError> for AppError {
67
+ fn from(e: $v::standout::app::types::AppError) -> Self {
68
+ Self { code: e.code.into(), message: e.message }
69
+ }
70
+ }
71
+
72
+ // TriggerEvent: version → canonical
73
+ impl From<$v::standout::app::types::TriggerEvent> for TriggerEvent {
74
+ fn from(e: $v::standout::app::types::TriggerEvent) -> Self {
75
+ Self { id: e.id, serialized_data: e.serialized_data }
76
+ }
77
+ }
78
+
79
+ // TriggerResponse: version → canonical
80
+ impl From<$v::standout::app::types::TriggerResponse> for TriggerResponse {
81
+ fn from(r: $v::standout::app::types::TriggerResponse) -> Self {
82
+ Self {
83
+ store: r.store,
84
+ events: r.events.into_iter().map(Into::into).collect(),
85
+ }
86
+ }
87
+ }
88
+
89
+ // ActionResponse: version → canonical
90
+ impl From<$v::standout::app::types::ActionResponse> for ActionResponse {
91
+ fn from(r: $v::standout::app::types::ActionResponse) -> Self {
92
+ Self { serialized_output: r.serialized_output }
93
+ }
94
+ }
95
+
96
+ // Connection: canonical → version (for passing to components)
97
+ impl From<&Connection> for $v::standout::app::types::Connection {
98
+ fn from(c: &Connection) -> Self {
99
+ Self {
100
+ id: c.id.clone(),
101
+ name: c.name.clone(),
102
+ serialized_data: c.serialized_data.clone(),
103
+ }
104
+ }
105
+ }
106
+
107
+ // TriggerContext: canonical → version
108
+ impl From<&TriggerContext> for $v::standout::app::types::TriggerContext {
109
+ fn from(c: &TriggerContext) -> Self {
110
+ Self {
111
+ trigger_id: c.trigger_id.clone(),
112
+ connection: (&c.connection).into(),
113
+ store: c.store.clone(),
114
+ serialized_input: c.serialized_input.clone(),
115
+ }
116
+ }
117
+ }
118
+
119
+ // ActionContext: canonical → version
120
+ impl From<&ActionContext> for $v::standout::app::types::ActionContext {
121
+ fn from(c: &ActionContext) -> Self {
122
+ Self {
123
+ action_id: c.action_id.clone(),
124
+ connection: (&c.connection).into(),
125
+ serialized_input: c.serialized_input.clone(),
126
+ }
127
+ }
128
+ }
129
+ };
130
+ }
131
+
132
+ // Generate conversions for all supported versions
133
+ impl_conversions!(v3);
134
+ impl_conversions!(v4);
135
+
136
+ // ============================================================================
137
+ // BridgeWrapper - unified interface for all component versions
138
+ //
139
+ // To add vN: add variant BridgeWrapper::VN(vN::Bridge)
140
+ // ============================================================================
141
+
142
+ pub enum BridgeWrapper {
143
+ V3(v3::Bridge),
144
+ V4(v4::Bridge),
145
+ }
146
+
147
+ impl BridgeWrapper {
148
+ /// Returns the WIT version this component was built against
149
+ pub fn wit_version(&self) -> &'static str {
150
+ match self {
151
+ BridgeWrapper::V3(_) => "3.0.0",
152
+ BridgeWrapper::V4(_) => "4.0.0",
153
+ }
154
+ }
155
+ }
156
+
157
+ /// Macro to implement a bridge method that works across all versions.
158
+ /// Each version's result is converted to canonical types.
159
+ macro_rules! bridge_method {
160
+ // Simple no-arg method (e.g., trigger_ids, action_ids)
161
+ (fn $name:ident() -> Result<$ok_type:ty> via $interface:ident . $method:ident) => {
162
+ pub fn $name(&self, store: &mut Store<AppState>) -> Result<std::result::Result<$ok_type, AppError>> {
163
+ match self {
164
+ BridgeWrapper::V3(b) => {
165
+ let r = b.$interface().$method(store)?;
166
+ Ok(r.map_err(Into::into))
167
+ }
168
+ BridgeWrapper::V4(b) => {
169
+ let r = b.$interface().$method(store)?;
170
+ Ok(r.map_err(Into::into))
171
+ }
172
+ }
173
+ }
174
+ };
175
+ // Method with TriggerContext
176
+ (fn $name:ident(&TriggerContext) -> Result<$ok_type:ty> via $interface:ident . $method:ident) => {
177
+ pub fn $name(&self, store: &mut Store<AppState>, ctx: &TriggerContext) -> Result<std::result::Result<$ok_type, AppError>> {
178
+ match self {
179
+ BridgeWrapper::V3(b) => {
180
+ let r = b.$interface().$method(store, &ctx.into())?;
181
+ Ok(r.map(Into::into).map_err(Into::into))
182
+ }
183
+ BridgeWrapper::V4(b) => {
184
+ let r = b.$interface().$method(store, &ctx.into())?;
185
+ Ok(r.map(Into::into).map_err(Into::into))
186
+ }
187
+ }
188
+ }
189
+ };
190
+ // Method with ActionContext
191
+ (fn $name:ident(&ActionContext) -> Result<$ok_type:ty> via $interface:ident . $method:ident) => {
192
+ pub fn $name(&self, store: &mut Store<AppState>, ctx: &ActionContext) -> Result<std::result::Result<$ok_type, AppError>> {
193
+ match self {
194
+ BridgeWrapper::V3(b) => {
195
+ let r = b.$interface().$method(store, &ctx.into())?;
196
+ Ok(r.map(Into::into).map_err(Into::into))
197
+ }
198
+ BridgeWrapper::V4(b) => {
199
+ let r = b.$interface().$method(store, &ctx.into())?;
200
+ Ok(r.map(Into::into).map_err(Into::into))
201
+ }
202
+ }
203
+ }
204
+ };
205
+ }
206
+
207
+ impl BridgeWrapper {
208
+ // Trigger methods
209
+ bridge_method!(fn call_trigger_ids() -> Result<Vec<String>> via standout_app_triggers . call_trigger_ids);
210
+ bridge_method!(fn call_trigger_input_schema(&TriggerContext) -> Result<String> via standout_app_triggers . call_input_schema);
211
+ bridge_method!(fn call_trigger_output_schema(&TriggerContext) -> Result<String> via standout_app_triggers . call_output_schema);
212
+ bridge_method!(fn call_fetch_events(&TriggerContext) -> Result<TriggerResponse> via standout_app_triggers . call_fetch_events);
213
+
214
+ // Action methods
215
+ bridge_method!(fn call_action_ids() -> Result<Vec<String>> via standout_app_actions . call_action_ids);
216
+ bridge_method!(fn call_action_input_schema(&ActionContext) -> Result<String> via standout_app_actions . call_input_schema);
217
+ bridge_method!(fn call_action_output_schema(&ActionContext) -> Result<String> via standout_app_actions . call_output_schema);
218
+ bridge_method!(fn call_execute(&ActionContext) -> Result<ActionResponse> via standout_app_actions . call_execute);
219
+ }
220
+
221
+ // ============================================================================
222
+ // Builder functions
223
+ // ============================================================================
13
224
 
14
225
  pub fn build_engine() -> Engine {
15
226
  Engine::default()
16
227
  }
17
228
 
18
229
  pub fn build_linker(engine: &Engine) -> Result<Linker<AppState>> {
19
- let mut linker = Linker::<AppState>::new(&engine);
20
- wasmtime_wasi::add_to_linker_sync(&mut linker)?;
21
- standout::app::http::add_to_linker(&mut linker, |s| s)?;
22
- standout::app::environment::add_to_linker(&mut linker, |s| s)?;
230
+ let mut linker = Linker::<AppState>::new(engine);
231
+
232
+ // WASI support (shared by all versions)
233
+ wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
234
+
235
+ // ---- Version-specific interfaces ----
236
+ // v3: http + environment
237
+ v3::standout::app::http::add_to_linker(&mut linker, |s| s)?;
238
+ v3::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
239
+
240
+ // v4: http + environment + file
241
+ v4::standout::app::http::add_to_linker(&mut linker, |s| s)?;
242
+ v4::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
243
+ v4::standout::app::file::add_to_linker(&mut linker, |s| s)?;
244
+
245
+ // Add new versions here:
246
+ // v5::standout::app::http::add_to_linker(&mut linker, |s| s)?;
247
+ // v5::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
248
+ // v5::standout::app::file::add_to_linker(&mut linker, |s| s)?;
249
+ // v5::standout::app::new_feature::add_to_linker(&mut linker, |s| s)?;
23
250
 
24
251
  Ok(linker)
25
252
  }
@@ -27,54 +254,37 @@ pub fn build_linker(engine: &Engine) -> Result<Linker<AppState>> {
27
254
  pub fn build_store(engine: &Engine, env_vars: Option<HashMap<String, String>>) -> Store<AppState> {
28
255
  let mut builder = WasiCtxBuilder::new();
29
256
 
30
- // Add environment variables to WASI context if provided
31
257
  if let Some(env_vars) = &env_vars {
32
258
  for (key, value) in env_vars {
33
259
  builder.env(key, value);
34
260
  }
35
261
  }
36
262
 
37
- let ctx = builder.build();
38
-
39
- // Create AppState with or without environment variables
40
- let app_state = AppState::new(ctx, env_vars);
41
-
42
- Store::new(&engine, app_state)
263
+ Store::new(engine, AppState::new(builder.build(), env_vars))
43
264
  }
44
265
 
266
+ /// Try to instantiate a WASM component.
267
+ /// Attempts versions from newest to oldest until one succeeds.
45
268
  pub fn app(
46
269
  file_path: String,
47
270
  engine: Engine,
48
271
  store: &mut Store<AppState>,
49
272
  linker: Linker<AppState>,
50
- ) -> Result<Bridge> {
51
- // Load the application component from the file system.
52
- let component = Component::from_file(&engine, file_path)?;
53
-
54
- // Try to instantiate the component - if it fails due to missing interface,
55
- // we'll catch that error and return a specific message
56
- match Bridge::instantiate(store, &component, &linker) {
57
- Ok(instance) => Ok(instance),
58
- Err(e) => {
59
- if e.to_string().contains("no exported instance") {
60
- Err(wasmtime::Error::msg(
61
- "Incompatible WASM file version"
62
- ))
63
- } else {
64
- Err(e)
65
- }
66
- }
67
- }
68
- }
273
+ ) -> Result<BridgeWrapper> {
274
+ let component = Component::from_file(&engine, &file_path)?;
69
275
 
70
- impl Default for Bridge {
71
- fn default() -> Self {
72
- let file_path = "spec/fixtures/components/example_app.wasm";
73
- let engine = Engine::default();
74
- let mut store = build_store(&engine, None);
75
- let linker = build_linker(&engine).unwrap();
76
- let instant = app(file_path.to_string(), engine, &mut store, linker);
276
+ // Try versions newest-first. When adding vN, insert at the top.
277
+ // v4 (current - has file interface)
278
+ if let Ok(instance) = v4::Bridge::instantiate(&mut *store, &component, &linker) {
279
+ return Ok(BridgeWrapper::V4(instance));
280
+ }
77
281
 
78
- instant.unwrap()
282
+ // v3 (legacy - no file interface)
283
+ if let Ok(instance) = v3::Bridge::instantiate(&mut *store, &component, &linker) {
284
+ return Ok(BridgeWrapper::V3(instance));
79
285
  }
286
+
287
+ Err(wasmtime::Error::msg(
288
+ "Failed to instantiate component: no compatible WIT version found (tried v4, v3)",
289
+ ))
80
290
  }