app_bridge 3.0.0 → 4.1.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 +274 -2
- data/ext/app_bridge/Cargo.toml +8 -4
- data/ext/app_bridge/src/app_state.rs +32 -13
- data/ext/app_bridge/src/component.rs +346 -45
- data/ext/app_bridge/src/error_mapping.rs +40 -24
- data/ext/app_bridge/src/file_ops.rs +326 -0
- data/ext/app_bridge/src/lib.rs +21 -3
- data/ext/app_bridge/src/request_builder.rs +343 -152
- data/ext/app_bridge/src/types.rs +95 -0
- data/ext/app_bridge/src/wrappers/action_context.rs +70 -9
- 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/ext/app_bridge/wit/v4_1/world.wit +343 -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 +25 -0
- data/tasks/fixtures.rake +16 -2
- metadata +9 -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,279 @@ 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
|
+
### Multipart Form Data
|
|
107
|
+
|
|
108
|
+
For `multipart/form-data`, you must build the body manually and set the `Content-Type` header with a boundary. In `standout:app@4.1.0` you can send raw bytes via `body-bytes`; earlier versions only support a string body.
|
|
109
|
+
|
|
110
|
+
#### In your WASM connector (Rust):
|
|
111
|
+
|
|
112
|
+
```rust
|
|
113
|
+
let boundary = "----app-bridge-boundary";
|
|
114
|
+
|
|
115
|
+
let mut body = Vec::new();
|
|
116
|
+
body.extend_from_slice(format!(
|
|
117
|
+
"--{b}\r\n\
|
|
118
|
+
Content-Disposition: form-data; name=\"metadata\"\r\n\r\n\
|
|
119
|
+
{metadata}\r\n\
|
|
120
|
+
--{b}\r\n\
|
|
121
|
+
Content-Disposition: form-data; name=\"file\"; filename=\"invoice.pdf\"\r\n\
|
|
122
|
+
Content-Type: application/pdf\r\n\r\n",
|
|
123
|
+
b = boundary,
|
|
124
|
+
metadata = metadata_json,
|
|
125
|
+
).as_bytes());
|
|
126
|
+
body.extend_from_slice(&file_bytes);
|
|
127
|
+
body.extend_from_slice(format!("\r\n--{b}--\r\n", b = boundary).as_bytes());
|
|
128
|
+
|
|
129
|
+
let response = RequestBuilder::new()
|
|
130
|
+
.method(Method::Post)
|
|
131
|
+
.url(upload_url)
|
|
132
|
+
.header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
|
|
133
|
+
.body_bytes(body)
|
|
134
|
+
.send()?;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If you are targeting `standout:app@4.0.0` or earlier, you can only send a string body. That means multipart uploads must be base64-encoded and the receiving API must support `Content-Transfer-Encoding: base64`.
|
|
138
|
+
|
|
139
|
+
## Retry With Reference
|
|
140
|
+
|
|
141
|
+
`standout:app@4.1.0` introduces a structured retry error for actions. Connectors can return a retry error with a reference and status, and the platform can pass that back on subsequent retries via `action-context.reference-object`.
|
|
142
|
+
|
|
143
|
+
### In your WASM connector (Rust)
|
|
144
|
+
|
|
145
|
+
```rust
|
|
146
|
+
return Err(AppError {
|
|
147
|
+
code: ErrorCode::RetryWithReference(ReferenceObject {
|
|
148
|
+
reference: request_id.to_string(),
|
|
149
|
+
status: status.to_string(),
|
|
150
|
+
}),
|
|
151
|
+
message: "Background request still processing".to_string(),
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### In the Ruby platform
|
|
156
|
+
|
|
157
|
+
When the connector returns `retry-with-reference`, the bridge raises `AppBridge::RetryWithReferenceError`. You can access the structured fields:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
rescue AppBridge::RetryWithReferenceError => e
|
|
161
|
+
e.reference # => "ref-123"
|
|
162
|
+
e.status # => "queued"
|
|
163
|
+
e.message # => "Background request still processing after 1 attempts."
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Backward Compatibility
|
|
168
|
+
|
|
169
|
+
The gem supports **multi-version WIT interfaces**, allowing connectors built against older WIT versions to continue working when the gem is updated.
|
|
170
|
+
|
|
171
|
+
### How it works
|
|
172
|
+
|
|
173
|
+
When loading a WASM component, the gem automatically detects which WIT version it was built against:
|
|
174
|
+
|
|
175
|
+
1. **V4 components** (current, `standout:app@4.0.0`): Full feature support including the `file` interface
|
|
176
|
+
2. **V3 components** (`standout:app@3.0.0`): Legacy support without file interface
|
|
177
|
+
|
|
178
|
+
### Adding support for new WIT versions
|
|
179
|
+
|
|
180
|
+
When adding a new WIT version (e.g., v5), follow these steps:
|
|
181
|
+
|
|
182
|
+
#### 1. Create the WIT file
|
|
183
|
+
|
|
184
|
+
Copy the latest version and modify:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
cp -r ext/app_bridge/wit/v4 ext/app_bridge/wit/v5
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Edit `ext/app_bridge/wit/v5/world.wit`:
|
|
191
|
+
- Update the package version: `package standout:app@5.0.0;`
|
|
192
|
+
- Add new interfaces or modify existing ones
|
|
193
|
+
|
|
194
|
+
#### 2. Add the bindgen module
|
|
195
|
+
|
|
196
|
+
In `ext/app_bridge/src/component.rs`, add after the existing modules:
|
|
197
|
+
|
|
198
|
+
```rust
|
|
199
|
+
pub mod v5 {
|
|
200
|
+
wasmtime::component::bindgen!({
|
|
201
|
+
path: "./wit/v5",
|
|
202
|
+
world: "bridge",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### 3. Generate type conversions
|
|
208
|
+
|
|
209
|
+
Add the conversion macro call:
|
|
210
|
+
|
|
211
|
+
```rust
|
|
212
|
+
impl_conversions!(v5);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### 4. Add BridgeWrapper variant
|
|
216
|
+
|
|
217
|
+
Update the enum:
|
|
218
|
+
|
|
219
|
+
```rust
|
|
220
|
+
pub enum BridgeWrapper {
|
|
221
|
+
V3(v3::Bridge),
|
|
222
|
+
V4(v4::Bridge),
|
|
223
|
+
V5(v5::Bridge), // <-- add this
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Add an arm to each `bridge_method!` macro expansion. In the macro definitions, add:
|
|
228
|
+
|
|
229
|
+
```rust
|
|
230
|
+
BridgeWrapper::V5(b) => {
|
|
231
|
+
let r = b.$interface().$method(store, ...)?;
|
|
232
|
+
Ok(r.map(Into::into).map_err(Into::into))
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### 5. Register interfaces in the linker
|
|
237
|
+
|
|
238
|
+
In `build_linker()`:
|
|
239
|
+
|
|
240
|
+
```rust
|
|
241
|
+
// v5: http + environment + file + new_feature
|
|
242
|
+
v5::standout::app::http::add_to_linker(&mut linker, |s| s)?;
|
|
243
|
+
v5::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
|
|
244
|
+
v5::standout::app::file::add_to_linker(&mut linker, |s| s)?;
|
|
245
|
+
v5::standout::app::new_feature::add_to_linker(&mut linker, |s| s)?; // if applicable
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### 6. Update the instantiation chain
|
|
249
|
+
|
|
250
|
+
In `app()`, add v5 at the top (newest first):
|
|
251
|
+
|
|
252
|
+
```rust
|
|
253
|
+
// v5 (newest)
|
|
254
|
+
if let Ok(instance) = v5::Bridge::instantiate(&mut *store, &component, &linker) {
|
|
255
|
+
return Ok(BridgeWrapper::V5(instance));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// v4
|
|
259
|
+
if let Ok(instance) = v4::Bridge::instantiate(&mut *store, &component, &linker) {
|
|
260
|
+
return Ok(BridgeWrapper::V4(instance));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// v3 (oldest)
|
|
264
|
+
// ...
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### 7. Register Host implementations
|
|
268
|
+
|
|
269
|
+
In `app_state.rs`:
|
|
270
|
+
|
|
271
|
+
```rust
|
|
272
|
+
impl_host_for_version!(v5);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
In `request_builder.rs`:
|
|
276
|
+
|
|
277
|
+
```rust
|
|
278
|
+
impl_host_request_builder!(v5);
|
|
279
|
+
impl_http_type_conversions!(v5);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### 8. If the version has the file interface
|
|
283
|
+
|
|
284
|
+
In `file_ops.rs` (only if v5 includes the `file` interface):
|
|
285
|
+
|
|
286
|
+
```rust
|
|
287
|
+
impl_file_host!(v5);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### 9. If adding new host functions
|
|
291
|
+
|
|
292
|
+
If the new version introduces entirely new interfaces (not just `file`), implement the `Host` trait:
|
|
293
|
+
|
|
294
|
+
```rust
|
|
295
|
+
impl v5::standout::app::new_feature::Host for AppState {
|
|
296
|
+
fn some_function(&mut self, ...) -> ... {
|
|
297
|
+
// implementation
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Benefits
|
|
303
|
+
|
|
304
|
+
- **No forced rebuilds**: Existing connectors continue to work after gem updates
|
|
305
|
+
- **Gradual migration**: Update connectors to new WIT versions at your own pace
|
|
306
|
+
- **Type safety**: Each version's types are converted to the latest version internally
|
|
35
307
|
|
|
36
308
|
## Development
|
|
37
309
|
|
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.1.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,16 +1,17 @@
|
|
|
1
|
-
use crate::component::
|
|
2
|
-
use crate::component::standout::app::http::Request;
|
|
1
|
+
use crate::component::{v3, v4, v4_1};
|
|
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,
|
|
11
11
|
table: ResourceTable,
|
|
12
12
|
pub client: Arc<Mutex<Client>>,
|
|
13
13
|
pub request_list: HashMap<u32, Request>,
|
|
14
|
+
pub request_body_bytes: HashMap<u32, Vec<u8>>,
|
|
14
15
|
pub next_request_id: u32,
|
|
15
16
|
pub environment_variables: HashMap<String, String>,
|
|
16
17
|
}
|
|
@@ -22,26 +23,44 @@ impl AppState {
|
|
|
22
23
|
table: ResourceTable::new(),
|
|
23
24
|
client: Arc::new(Mutex::new(Client::new())),
|
|
24
25
|
request_list: HashMap::new(),
|
|
26
|
+
request_body_bytes: HashMap::new(),
|
|
25
27
|
next_request_id: 0,
|
|
26
28
|
environment_variables: env_vars.unwrap_or_default(),
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Macro to implement identical Host traits for multiple WIT versions
|
|
35
|
+
// ============================================================================
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/// Implements http::Host and environment::Host for a given WIT version module.
|
|
38
|
+
/// These implementations are identical across versions.
|
|
39
|
+
macro_rules! impl_host_for_version {
|
|
40
|
+
($version:ident) => {
|
|
41
|
+
impl $version::standout::app::http::Host for AppState {}
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
impl $version::standout::app::environment::Host for AppState {
|
|
44
|
+
fn env_vars(&mut self) -> Vec<(String, String)> {
|
|
45
|
+
self.environment_variables.clone().into_iter().collect()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn env_var(&mut self, name: String) -> Option<String> {
|
|
49
|
+
self.environment_variables.get(&name).cloned()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
// Apply to both versions
|
|
56
|
+
impl_host_for_version!(v3);
|
|
57
|
+
impl_host_for_version!(v4);
|
|
58
|
+
impl_host_for_version!(v4_1);
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// WASI implementations
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
45
64
|
impl IoView for AppState {
|
|
46
65
|
fn table(&mut self) -> &mut ResourceTable {
|
|
47
66
|
&mut self.table
|