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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.tool-versions +1 -1
- data/Cargo.lock +292 -751
- data/Cargo.toml +1 -1
- data/README.md +213 -2
- data/ext/app_bridge/Cargo.toml +8 -4
- data/ext/app_bridge/src/app_state.rs +29 -13
- data/ext/app_bridge/src/component.rs +256 -46
- data/ext/app_bridge/src/error_mapping.rs +24 -24
- data/ext/app_bridge/src/file_ops.rs +325 -0
- data/ext/app_bridge/src/lib.rs +5 -1
- data/ext/app_bridge/src/request_builder.rs +270 -152
- data/ext/app_bridge/src/types.rs +78 -0
- data/ext/app_bridge/src/wrappers/action_context.rs +3 -3
- data/ext/app_bridge/src/wrappers/action_response.rs +7 -3
- data/ext/app_bridge/src/wrappers/app.rs +112 -148
- data/ext/app_bridge/src/wrappers/connection.rs +4 -4
- data/ext/app_bridge/src/wrappers/trigger_context.rs +7 -7
- data/ext/app_bridge/src/wrappers/trigger_event.rs +4 -4
- data/ext/app_bridge/src/wrappers/trigger_response.rs +29 -28
- data/ext/app_bridge/wit/{world.wit → v3/world.wit} +3 -0
- data/ext/app_bridge/wit/v4/world.wit +328 -0
- data/lib/app_bridge/app.rb +21 -2
- data/lib/app_bridge/file_processor.rb +131 -0
- data/lib/app_bridge/version.rb +1 -1
- data/lib/app_bridge.rb +26 -0
- data/tasks/fixtures.rake +16 -2
- metadata +8 -4
data/Cargo.toml
CHANGED
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
|
-
|
|
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
|
|
data/ext/app_bridge/Cargo.toml
CHANGED
|
@@ -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 = "
|
|
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 = "
|
|
16
|
-
wasmtime-wasi = "
|
|
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.
|
|
24
|
+
httpmock = "0.8.2"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
use crate::component::
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Macro to implement identical Host traits for multiple WIT versions
|
|
33
|
+
// ============================================================================
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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<
|
|
51
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
}
|