app_bridge 4.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/Cargo.lock +1 -1
- data/README.md +61 -0
- data/ext/app_bridge/Cargo.toml +1 -1
- data/ext/app_bridge/src/app_state.rs +4 -1
- data/ext/app_bridge/src/component.rs +105 -14
- data/ext/app_bridge/src/error_mapping.rs +17 -1
- data/ext/app_bridge/src/file_ops.rs +2 -1
- data/ext/app_bridge/src/lib.rs +16 -2
- data/ext/app_bridge/src/request_builder.rs +82 -9
- data/ext/app_bridge/src/types.rs +18 -1
- data/ext/app_bridge/src/wrappers/action_context.rs +69 -8
- data/ext/app_bridge/wit/v4_1/world.wit +343 -0
- data/lib/app_bridge/version.rb +1 -1
- data/lib/app_bridge.rb +0 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26bd5103440c1de1bd93ea671635a87e8f25d39fc56fef5025752799e6731edd
|
|
4
|
+
data.tar.gz: 92157e6b2962fadcca66aa24b9baf234bf04e093f0c016cd51d8e7542a8da0b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7f8c267d373d1e0f5ffa730956fc5e2d0bdc43f9143c61e3866596efae583d6e9403c2cf4ead9972af4cd16ee6d6097c5fa01d8974017de6f4a8c571316b7a9
|
|
7
|
+
data.tar.gz: 2bebe7b17a804216f59846866d374b4d2b2439ad77aee6ab1e4d51766dbeba6053b896f2906c0feda378d9cbbaa64b695897351113aa3ee3e157cdec20586d4a
|
data/Cargo.lock
CHANGED
data/README.md
CHANGED
|
@@ -103,6 +103,67 @@ AppBridge.file_uploader = ->(file_data) {
|
|
|
103
103
|
|
|
104
104
|
The gem automatically replaces file data with the return value (in this example blob IDs) before returning the action response.
|
|
105
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
|
+
|
|
106
167
|
## Backward Compatibility
|
|
107
168
|
|
|
108
169
|
The gem supports **multi-version WIT interfaces**, allowing connectors built against older WIT versions to continue working when the gem is updated.
|
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 = "4.
|
|
5
|
+
version = "4.1.0"
|
|
6
6
|
edition = "2021"
|
|
7
7
|
authors = ["Alexander Ross <ross@standout.se>"]
|
|
8
8
|
publish = false
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use crate::component::{v3, v4};
|
|
1
|
+
use crate::component::{v3, v4, v4_1};
|
|
2
2
|
use crate::component::v4::standout::app::http::Request;
|
|
3
3
|
use reqwest::blocking::Client;
|
|
4
4
|
use std::collections::HashMap;
|
|
@@ -11,6 +11,7 @@ pub struct AppState {
|
|
|
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,6 +23,7 @@ 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
|
}
|
|
@@ -53,6 +55,7 @@ macro_rules! impl_host_for_version {
|
|
|
53
55
|
// Apply to both versions
|
|
54
56
|
impl_host_for_version!(v3);
|
|
55
57
|
impl_host_for_version!(v4);
|
|
58
|
+
impl_host_for_version!(v4_1);
|
|
56
59
|
|
|
57
60
|
// ============================================================================
|
|
58
61
|
// WASI implementations
|
|
@@ -7,7 +7,7 @@ use wasmtime_wasi::p2::WasiCtxBuilder;
|
|
|
7
7
|
use crate::app_state::AppState;
|
|
8
8
|
use crate::types::{
|
|
9
9
|
ActionContext, ActionResponse, AppError, Connection, ErrorCode, TriggerContext,
|
|
10
|
-
TriggerEvent, TriggerResponse,
|
|
10
|
+
ReferenceObject, TriggerEvent, TriggerResponse,
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// ============================================================================
|
|
@@ -35,13 +35,19 @@ pub mod v4 {
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
pub mod v4_1 {
|
|
39
|
+
wasmtime::component::bindgen!({
|
|
40
|
+
path: "./wit/v4_1",
|
|
41
|
+
world: "bridge",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
// ============================================================================
|
|
39
46
|
// Version conversion macro - generates From impls for a version module
|
|
40
47
|
// ============================================================================
|
|
41
48
|
|
|
42
|
-
macro_rules!
|
|
43
|
-
($v:ident) => {
|
|
44
|
-
// ErrorCode: version → canonical
|
|
49
|
+
macro_rules! impl_error_code_conversion {
|
|
50
|
+
($v:ident, $($extra_arms:tt)*) => {
|
|
45
51
|
impl From<$v::standout::app::types::ErrorCode> for ErrorCode {
|
|
46
52
|
fn from(c: $v::standout::app::types::ErrorCode) -> Self {
|
|
47
53
|
use $v::standout::app::types::ErrorCode as V;
|
|
@@ -56,19 +62,17 @@ macro_rules! impl_conversions {
|
|
|
56
62
|
V::InternalError => Self::InternalError,
|
|
57
63
|
V::MalformedResponse => Self::MalformedResponse,
|
|
58
64
|
V::Other => Self::Other,
|
|
65
|
+
$($extra_arms)*
|
|
59
66
|
V::CompleteWorkflow => Self::CompleteWorkflow,
|
|
60
67
|
V::CompleteParent => Self::CompleteParent,
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
fn from(e: $v::standout::app::types::AppError) -> Self {
|
|
68
|
-
Self { code: e.code.into(), message: e.message }
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
74
|
+
macro_rules! impl_conversions {
|
|
75
|
+
($v:ident) => {
|
|
72
76
|
// TriggerEvent: version → canonical
|
|
73
77
|
impl From<$v::standout::app::types::TriggerEvent> for TriggerEvent {
|
|
74
78
|
fn from(e: $v::standout::app::types::TriggerEvent) -> Self {
|
|
@@ -116,7 +120,24 @@ macro_rules! impl_conversions {
|
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
macro_rules! impl_app_error_conversion {
|
|
127
|
+
($v:ident) => {
|
|
128
|
+
impl From<$v::standout::app::types::AppError> for AppError {
|
|
129
|
+
fn from(e: $v::standout::app::types::AppError) -> Self {
|
|
130
|
+
Self {
|
|
131
|
+
code: e.code.into(),
|
|
132
|
+
message: e.message,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
macro_rules! impl_action_context_conversion_basic {
|
|
140
|
+
($v:ident) => {
|
|
120
141
|
impl From<&ActionContext> for $v::standout::app::types::ActionContext {
|
|
121
142
|
fn from(c: &ActionContext) -> Self {
|
|
122
143
|
Self {
|
|
@@ -129,9 +150,55 @@ macro_rules! impl_conversions {
|
|
|
129
150
|
};
|
|
130
151
|
}
|
|
131
152
|
|
|
153
|
+
macro_rules! impl_action_context_conversion_retry {
|
|
154
|
+
($v:ident) => {
|
|
155
|
+
impl From<&ActionContext> for $v::standout::app::types::ActionContext {
|
|
156
|
+
fn from(c: &ActionContext) -> Self {
|
|
157
|
+
Self {
|
|
158
|
+
action_id: c.action_id.clone(),
|
|
159
|
+
connection: (&c.connection).into(),
|
|
160
|
+
serialized_input: c.serialized_input.clone(),
|
|
161
|
+
reference_object: c.reference_object.as_ref().map(Into::into),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
impl From<v4_1::standout::app::types::ReferenceObject> for ReferenceObject {
|
|
169
|
+
fn from(r: v4_1::standout::app::types::ReferenceObject) -> Self {
|
|
170
|
+
Self {
|
|
171
|
+
reference: r.reference,
|
|
172
|
+
status: r.status,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
impl From<&ReferenceObject> for v4_1::standout::app::types::ReferenceObject {
|
|
178
|
+
fn from(r: &ReferenceObject) -> Self {
|
|
179
|
+
Self {
|
|
180
|
+
reference: r.reference.clone(),
|
|
181
|
+
status: r.status.clone(),
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
132
186
|
// Generate conversions for all supported versions
|
|
187
|
+
impl_error_code_conversion!(v3,);
|
|
133
188
|
impl_conversions!(v3);
|
|
189
|
+
impl_app_error_conversion!(v3);
|
|
190
|
+
impl_action_context_conversion_basic!(v3);
|
|
191
|
+
impl_error_code_conversion!(v4,);
|
|
134
192
|
impl_conversions!(v4);
|
|
193
|
+
impl_app_error_conversion!(v4);
|
|
194
|
+
impl_action_context_conversion_basic!(v4);
|
|
195
|
+
impl_error_code_conversion!(
|
|
196
|
+
v4_1,
|
|
197
|
+
V::RetryWithReference(r) => Self::RetryWithReference(r.into()),
|
|
198
|
+
);
|
|
199
|
+
impl_conversions!(v4_1);
|
|
200
|
+
impl_app_error_conversion!(v4_1);
|
|
201
|
+
impl_action_context_conversion_retry!(v4_1);
|
|
135
202
|
|
|
136
203
|
// ============================================================================
|
|
137
204
|
// BridgeWrapper - unified interface for all component versions
|
|
@@ -142,6 +209,7 @@ impl_conversions!(v4);
|
|
|
142
209
|
pub enum BridgeWrapper {
|
|
143
210
|
V3(v3::Bridge),
|
|
144
211
|
V4(v4::Bridge),
|
|
212
|
+
V4_1(v4_1::Bridge),
|
|
145
213
|
}
|
|
146
214
|
|
|
147
215
|
impl BridgeWrapper {
|
|
@@ -150,6 +218,7 @@ impl BridgeWrapper {
|
|
|
150
218
|
match self {
|
|
151
219
|
BridgeWrapper::V3(_) => "3.0.0",
|
|
152
220
|
BridgeWrapper::V4(_) => "4.0.0",
|
|
221
|
+
BridgeWrapper::V4_1(_) => "4.1.0",
|
|
153
222
|
}
|
|
154
223
|
}
|
|
155
224
|
}
|
|
@@ -169,6 +238,10 @@ macro_rules! bridge_method {
|
|
|
169
238
|
let r = b.$interface().$method(store)?;
|
|
170
239
|
Ok(r.map_err(Into::into))
|
|
171
240
|
}
|
|
241
|
+
BridgeWrapper::V4_1(b) => {
|
|
242
|
+
let r = b.$interface().$method(store)?;
|
|
243
|
+
Ok(r.map_err(Into::into))
|
|
244
|
+
}
|
|
172
245
|
}
|
|
173
246
|
}
|
|
174
247
|
};
|
|
@@ -184,6 +257,10 @@ macro_rules! bridge_method {
|
|
|
184
257
|
let r = b.$interface().$method(store, &ctx.into())?;
|
|
185
258
|
Ok(r.map(Into::into).map_err(Into::into))
|
|
186
259
|
}
|
|
260
|
+
BridgeWrapper::V4_1(b) => {
|
|
261
|
+
let r = b.$interface().$method(store, &ctx.into())?;
|
|
262
|
+
Ok(r.map(Into::into).map_err(Into::into))
|
|
263
|
+
}
|
|
187
264
|
}
|
|
188
265
|
}
|
|
189
266
|
};
|
|
@@ -199,6 +276,10 @@ macro_rules! bridge_method {
|
|
|
199
276
|
let r = b.$interface().$method(store, &ctx.into())?;
|
|
200
277
|
Ok(r.map(Into::into).map_err(Into::into))
|
|
201
278
|
}
|
|
279
|
+
BridgeWrapper::V4_1(b) => {
|
|
280
|
+
let r = b.$interface().$method(store, &ctx.into())?;
|
|
281
|
+
Ok(r.map(Into::into).map_err(Into::into))
|
|
282
|
+
}
|
|
202
283
|
}
|
|
203
284
|
}
|
|
204
285
|
};
|
|
@@ -242,6 +323,11 @@ pub fn build_linker(engine: &Engine) -> Result<Linker<AppState>> {
|
|
|
242
323
|
v4::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
|
|
243
324
|
v4::standout::app::file::add_to_linker(&mut linker, |s| s)?;
|
|
244
325
|
|
|
326
|
+
// v4.1: http + environment + file
|
|
327
|
+
v4_1::standout::app::http::add_to_linker(&mut linker, |s| s)?;
|
|
328
|
+
v4_1::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
|
|
329
|
+
v4_1::standout::app::file::add_to_linker(&mut linker, |s| s)?;
|
|
330
|
+
|
|
245
331
|
// Add new versions here:
|
|
246
332
|
// v5::standout::app::http::add_to_linker(&mut linker, |s| s)?;
|
|
247
333
|
// v5::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
|
|
@@ -274,7 +360,12 @@ pub fn app(
|
|
|
274
360
|
let component = Component::from_file(&engine, &file_path)?;
|
|
275
361
|
|
|
276
362
|
// Try versions newest-first. When adding vN, insert at the top.
|
|
277
|
-
// v4 (current - has file interface)
|
|
363
|
+
// v4.1 (current - has file interface)
|
|
364
|
+
if let Ok(instance) = v4_1::Bridge::instantiate(&mut *store, &component, &linker) {
|
|
365
|
+
return Ok(BridgeWrapper::V4_1(instance));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// v4
|
|
278
369
|
if let Ok(instance) = v4::Bridge::instantiate(&mut *store, &component, &linker) {
|
|
279
370
|
return Ok(BridgeWrapper::V4(instance));
|
|
280
371
|
}
|
|
@@ -285,6 +376,6 @@ pub fn app(
|
|
|
285
376
|
}
|
|
286
377
|
|
|
287
378
|
Err(wasmtime::Error::msg(
|
|
288
|
-
"Failed to instantiate component: no compatible WIT version found (tried v4, v3)",
|
|
379
|
+
"Failed to instantiate component: no compatible WIT version found (tried v4.1, v4, v3)",
|
|
289
380
|
))
|
|
290
381
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use crate::types::{AppError, ErrorCode};
|
|
2
|
-
use magnus::
|
|
2
|
+
use magnus::prelude::*;
|
|
3
|
+
use magnus::{Error, ExceptionClass, RObject, Ruby};
|
|
3
4
|
|
|
4
5
|
impl From<ErrorCode> for ExceptionClass {
|
|
5
6
|
fn from(value: ErrorCode) -> Self {
|
|
@@ -19,6 +20,7 @@ impl From<ErrorCode> for ExceptionClass {
|
|
|
19
20
|
ErrorCode::InternalError => get_class("AppBridge::InternalError"),
|
|
20
21
|
ErrorCode::MalformedResponse => get_class("AppBridge::MalformedResponseError"),
|
|
21
22
|
ErrorCode::Other => get_class("AppBridge::OtherError"),
|
|
23
|
+
ErrorCode::RetryWithReference(_) => get_class("AppBridge::RetryWithReferenceError"),
|
|
22
24
|
ErrorCode::CompleteWorkflow => get_class("AppBridge::CompleteWorkflowException"),
|
|
23
25
|
ErrorCode::CompleteParent => get_class("AppBridge::CompleteParentException"),
|
|
24
26
|
}
|
|
@@ -27,6 +29,20 @@ impl From<ErrorCode> for ExceptionClass {
|
|
|
27
29
|
|
|
28
30
|
impl From<AppError> for Error {
|
|
29
31
|
fn from(value: AppError) -> Self {
|
|
32
|
+
if let ErrorCode::RetryWithReference(retry) = value.code.clone() {
|
|
33
|
+
let class: ExceptionClass = value.code.into();
|
|
34
|
+
let message = value.message;
|
|
35
|
+
if let Ok(exception) = class.new_instance((message.as_str(),)) {
|
|
36
|
+
if let Ok(exception_value) = RObject::try_convert(exception.as_value()) {
|
|
37
|
+
let _ = exception_value.ivar_set("@reference", retry.reference);
|
|
38
|
+
let _ = exception_value.ivar_set("@status", retry.status);
|
|
39
|
+
}
|
|
40
|
+
return Error::from(exception);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Error::new(class, message);
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
Error::new(value.code.into(), value.message)
|
|
31
47
|
}
|
|
32
48
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
use crate::app_state::AppState;
|
|
2
|
-
use crate::component::v4;
|
|
2
|
+
use crate::component::{v4, v4_1};
|
|
3
3
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
|
4
4
|
|
|
5
5
|
/// Detects the type of input string
|
|
@@ -256,6 +256,7 @@ macro_rules! impl_file_host {
|
|
|
256
256
|
// Note: v3 doesn't have the file interface, so no impl needed
|
|
257
257
|
// When adding v5, add: impl_file_host!(v5);
|
|
258
258
|
impl_file_host!(v4);
|
|
259
|
+
impl_file_host!(v4_1);
|
|
259
260
|
|
|
260
261
|
#[cfg(test)]
|
|
261
262
|
mod tests {
|
data/ext/app_bridge/src/lib.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use magnus::{function, method, prelude::*, Error, Ruby};
|
|
1
|
+
use magnus::{function, method, prelude::*, Error, RObject, Ruby, Value};
|
|
2
2
|
mod app_state;
|
|
3
3
|
mod component;
|
|
4
4
|
mod error_mapping;
|
|
@@ -15,6 +15,16 @@ use wrappers::action_context::RActionContext;
|
|
|
15
15
|
use wrappers::action_response::RActionResponse;
|
|
16
16
|
use wrappers::app::MutRApp;
|
|
17
17
|
|
|
18
|
+
fn retry_reference(exception: Value) -> Result<Value, Error> {
|
|
19
|
+
let exception = RObject::try_convert(exception)?;
|
|
20
|
+
exception.ivar_get("@reference")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn retry_status(exception: Value) -> Result<Value, Error> {
|
|
24
|
+
let exception = RObject::try_convert(exception)?;
|
|
25
|
+
exception.ivar_get("@status")
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
#[magnus::init]
|
|
19
29
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
20
30
|
let module = ruby.define_module("AppBridge")?;
|
|
@@ -30,6 +40,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
30
40
|
module.define_error("InternalError", error)?;
|
|
31
41
|
module.define_error("MalformedResponseError", error)?;
|
|
32
42
|
module.define_error("OtherError", error)?;
|
|
43
|
+
let retry_error = module.define_error("RetryWithReferenceError", error)?;
|
|
44
|
+
retry_error.define_method("reference", method!(retry_reference, 0))?;
|
|
45
|
+
retry_error.define_method("status", method!(retry_status, 0))?;
|
|
33
46
|
module.define_error("CompleteWorkflowException", error)?;
|
|
34
47
|
module.define_error("CompleteParentException", error)?;
|
|
35
48
|
|
|
@@ -62,10 +75,11 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
62
75
|
|
|
63
76
|
// Define the Action classes
|
|
64
77
|
let action_context_class = module.define_class("ActionContext", ruby.class_object())?;
|
|
65
|
-
action_context_class.define_singleton_method("new", function!(RActionContext::new,
|
|
78
|
+
action_context_class.define_singleton_method("new", function!(RActionContext::new, -1))?;
|
|
66
79
|
action_context_class.define_method("action_id", method!(RActionContext::action_id, 0))?;
|
|
67
80
|
action_context_class.define_method("connection", method!(RActionContext::connection, 0))?;
|
|
68
81
|
action_context_class.define_method("serialized_input", method!(RActionContext::serialized_input, 0))?;
|
|
82
|
+
action_context_class.define_method("reference_object", method!(RActionContext::reference_object, 0))?;
|
|
69
83
|
|
|
70
84
|
let action_response_class = module.define_class("ActionResponse", ruby.class_object())?;
|
|
71
85
|
action_response_class.define_singleton_method("new", function!(RActionResponse::new, 1))?;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
use crate::app_state::AppState;
|
|
2
|
-
use crate::component::{v3, v4};
|
|
2
|
+
use crate::component::{v3, v4, v4_1};
|
|
3
3
|
use crate::component::v4::standout::app::http::{Method, Request, RequestError, Response};
|
|
4
4
|
use reqwest::Method as ReqwestMethod;
|
|
5
5
|
use std::result::Result::Ok;
|
|
@@ -14,7 +14,7 @@ use wasmtime::component::Resource;
|
|
|
14
14
|
// ============================================================================
|
|
15
15
|
|
|
16
16
|
macro_rules! impl_host_request_builder {
|
|
17
|
-
($v:ident) => {
|
|
17
|
+
($v:ident, $has_body_bytes:ident) => {
|
|
18
18
|
impl $v::standout::app::http::HostRequestBuilder for AppState {
|
|
19
19
|
fn new(&mut self) -> Resource<$v::standout::app::http::RequestBuilder> {
|
|
20
20
|
let id = self.next_request_id;
|
|
@@ -34,6 +34,9 @@ macro_rules! impl_host_request_builder {
|
|
|
34
34
|
let new_id = self.next_request_id;
|
|
35
35
|
self.next_request_id += 1;
|
|
36
36
|
self.request_list.insert(new_id, request);
|
|
37
|
+
if let Some(bytes) = self.request_body_bytes.get(&id).cloned() {
|
|
38
|
+
self.request_body_bytes.insert(new_id, bytes);
|
|
39
|
+
}
|
|
37
40
|
Resource::new_own(new_id)
|
|
38
41
|
} else {
|
|
39
42
|
Resource::new_own(id)
|
|
@@ -51,6 +54,9 @@ macro_rules! impl_host_request_builder {
|
|
|
51
54
|
let new_id = self.next_request_id;
|
|
52
55
|
self.next_request_id += 1;
|
|
53
56
|
self.request_list.insert(new_id, request);
|
|
57
|
+
if let Some(bytes) = self.request_body_bytes.get(&id).cloned() {
|
|
58
|
+
self.request_body_bytes.insert(new_id, bytes);
|
|
59
|
+
}
|
|
54
60
|
Resource::new_own(new_id)
|
|
55
61
|
} else {
|
|
56
62
|
Resource::new_own(id)
|
|
@@ -69,6 +75,9 @@ macro_rules! impl_host_request_builder {
|
|
|
69
75
|
let new_id = self.next_request_id;
|
|
70
76
|
self.next_request_id += 1;
|
|
71
77
|
self.request_list.insert(new_id, request);
|
|
78
|
+
if let Some(bytes) = self.request_body_bytes.get(&id).cloned() {
|
|
79
|
+
self.request_body_bytes.insert(new_id, bytes);
|
|
80
|
+
}
|
|
72
81
|
Resource::new_own(new_id)
|
|
73
82
|
}
|
|
74
83
|
|
|
@@ -83,6 +92,9 @@ macro_rules! impl_host_request_builder {
|
|
|
83
92
|
let new_id = self.next_request_id;
|
|
84
93
|
self.next_request_id += 1;
|
|
85
94
|
self.request_list.insert(new_id, request);
|
|
95
|
+
if let Some(bytes) = self.request_body_bytes.get(&id).cloned() {
|
|
96
|
+
self.request_body_bytes.insert(new_id, bytes);
|
|
97
|
+
}
|
|
86
98
|
Resource::new_own(new_id)
|
|
87
99
|
}
|
|
88
100
|
|
|
@@ -97,6 +109,7 @@ macro_rules! impl_host_request_builder {
|
|
|
97
109
|
let new_id = self.next_request_id;
|
|
98
110
|
self.next_request_id += 1;
|
|
99
111
|
self.request_list.insert(new_id, request);
|
|
112
|
+
self.request_body_bytes.remove(&new_id);
|
|
100
113
|
Resource::new_own(new_id)
|
|
101
114
|
}
|
|
102
115
|
|
|
@@ -106,9 +119,12 @@ macro_rules! impl_host_request_builder {
|
|
|
106
119
|
) -> Result<$v::standout::app::http::Response, $v::standout::app::http::RequestError> {
|
|
107
120
|
let id = self_.rep();
|
|
108
121
|
match self.request_list.get(&id).cloned() {
|
|
109
|
-
Some(request) =>
|
|
110
|
-
.map(
|
|
111
|
-
.
|
|
122
|
+
Some(request) => {
|
|
123
|
+
let body_bytes = self.request_body_bytes.get(&id).map(|b| b.as_slice());
|
|
124
|
+
send_request(&self.client, &request, body_bytes)
|
|
125
|
+
.map(Into::into)
|
|
126
|
+
.map_err(Into::into)
|
|
127
|
+
}
|
|
112
128
|
None => Err($v::standout::app::http::RequestError::Other(
|
|
113
129
|
"Request not found".to_string(),
|
|
114
130
|
)),
|
|
@@ -120,6 +136,7 @@ macro_rules! impl_host_request_builder {
|
|
|
120
136
|
rep: Resource<$v::standout::app::http::RequestBuilder>,
|
|
121
137
|
) -> wasmtime::Result<()> {
|
|
122
138
|
self.request_list.remove(&rep.rep());
|
|
139
|
+
self.request_body_bytes.remove(&rep.rep());
|
|
123
140
|
Ok(())
|
|
124
141
|
}
|
|
125
142
|
|
|
@@ -133,10 +150,33 @@ macro_rules! impl_host_request_builder {
|
|
|
133
150
|
.unwrap_or_default()
|
|
134
151
|
.into()
|
|
135
152
|
}
|
|
153
|
+
|
|
154
|
+
impl_host_request_builder_body_bytes!($v, $has_body_bytes);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
macro_rules! impl_host_request_builder_body_bytes {
|
|
160
|
+
($v:ident, yes) => {
|
|
161
|
+
fn body_bytes(
|
|
162
|
+
&mut self,
|
|
163
|
+
self_: Resource<$v::standout::app::http::RequestBuilder>,
|
|
164
|
+
body: Vec<u8>,
|
|
165
|
+
) -> Resource<$v::standout::app::http::RequestBuilder> {
|
|
166
|
+
let id = self_.rep();
|
|
167
|
+
let mut request = self.request_list.get(&id).cloned().unwrap_or_default();
|
|
168
|
+
request.body.clear();
|
|
169
|
+
let new_id = self.next_request_id;
|
|
170
|
+
self.next_request_id += 1;
|
|
171
|
+
self.request_list.insert(new_id, request);
|
|
172
|
+
self.request_body_bytes.insert(new_id, body);
|
|
173
|
+
Resource::new_own(new_id)
|
|
136
174
|
}
|
|
137
175
|
};
|
|
176
|
+
($v:ident, no) => {};
|
|
138
177
|
}
|
|
139
178
|
|
|
179
|
+
|
|
140
180
|
// ============================================================================
|
|
141
181
|
// Macro to implement HTTP type conversions for a version
|
|
142
182
|
// ============================================================================
|
|
@@ -210,11 +250,13 @@ macro_rules! impl_http_type_conversions {
|
|
|
210
250
|
// impl_http_type_conversions!(v5);
|
|
211
251
|
// ============================================================================
|
|
212
252
|
|
|
213
|
-
impl_host_request_builder!(v3);
|
|
214
|
-
impl_host_request_builder!(v4);
|
|
253
|
+
impl_host_request_builder!(v3, no);
|
|
254
|
+
impl_host_request_builder!(v4, no);
|
|
255
|
+
impl_host_request_builder!(v4_1, yes);
|
|
215
256
|
|
|
216
257
|
impl_http_type_conversions!(v3);
|
|
217
258
|
// Note: v4 doesn't need conversions since we use v4 types as the canonical internal types
|
|
259
|
+
impl_http_type_conversions!(v4_1);
|
|
218
260
|
|
|
219
261
|
// ============================================================================
|
|
220
262
|
// Shared request sending logic
|
|
@@ -223,6 +265,7 @@ impl_http_type_conversions!(v3);
|
|
|
223
265
|
fn send_request(
|
|
224
266
|
client: &std::sync::Arc<std::sync::Mutex<reqwest::blocking::Client>>,
|
|
225
267
|
request: &Request,
|
|
268
|
+
body_bytes: Option<&[u8]>,
|
|
226
269
|
) -> Result<Response, RequestError> {
|
|
227
270
|
let client = client.lock().unwrap();
|
|
228
271
|
let mut builder = client.request(request.method.clone().into(), &request.url);
|
|
@@ -230,7 +273,11 @@ fn send_request(
|
|
|
230
273
|
for (key, value) in &request.headers {
|
|
231
274
|
builder = builder.header(key, value);
|
|
232
275
|
}
|
|
233
|
-
builder =
|
|
276
|
+
builder = if let Some(bytes) = body_bytes {
|
|
277
|
+
builder.body(bytes.to_vec())
|
|
278
|
+
} else {
|
|
279
|
+
builder.body(request.body.clone())
|
|
280
|
+
};
|
|
234
281
|
|
|
235
282
|
match builder.send() {
|
|
236
283
|
Ok(resp) => {
|
|
@@ -321,7 +368,7 @@ impl std::fmt::Display for Method {
|
|
|
321
368
|
#[cfg(test)]
|
|
322
369
|
mod tests {
|
|
323
370
|
use super::*;
|
|
324
|
-
use httpmock::{Method::GET, MockServer};
|
|
371
|
+
use httpmock::{Method::GET, Method::POST, MockServer};
|
|
325
372
|
|
|
326
373
|
#[test]
|
|
327
374
|
fn sends_request_with_default_user_agent() {
|
|
@@ -349,4 +396,30 @@ mod tests {
|
|
|
349
396
|
assert_eq!(response.status, 200);
|
|
350
397
|
mock.assert();
|
|
351
398
|
}
|
|
399
|
+
|
|
400
|
+
#[test]
|
|
401
|
+
fn sends_binary_body_when_bytes_present() {
|
|
402
|
+
use v4_1::standout::app::http::HostRequestBuilder;
|
|
403
|
+
|
|
404
|
+
let server = MockServer::start();
|
|
405
|
+
let mock = server.mock(|when, then| {
|
|
406
|
+
when.method(POST)
|
|
407
|
+
.path("/upload")
|
|
408
|
+
.body("raw-bytes");
|
|
409
|
+
then.status(200);
|
|
410
|
+
});
|
|
411
|
+
let url = format!("{}/upload", server.base_url());
|
|
412
|
+
|
|
413
|
+
let mut app_state = AppState::default();
|
|
414
|
+
let builder = app_state.new();
|
|
415
|
+
let builder = app_state.method(builder, v4_1::standout::app::http::Method::Post);
|
|
416
|
+
let builder = app_state.url(builder, url);
|
|
417
|
+
let builder = app_state.body(builder, "string-body".to_string());
|
|
418
|
+
let builder = app_state.body_bytes(builder, b"raw-bytes".to_vec());
|
|
419
|
+
|
|
420
|
+
let response = app_state.send(builder).expect("Request failed");
|
|
421
|
+
|
|
422
|
+
assert_eq!(response.status, 200);
|
|
423
|
+
mock.assert();
|
|
424
|
+
}
|
|
352
425
|
}
|
data/ext/app_bridge/src/types.rs
CHANGED
|
@@ -14,6 +14,15 @@ pub struct AppError {
|
|
|
14
14
|
pub message: String,
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
impl AppError {
|
|
18
|
+
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
|
|
19
|
+
Self {
|
|
20
|
+
code,
|
|
21
|
+
message: message.into(),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
impl fmt::Display for AppError {
|
|
18
27
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
19
28
|
write!(f, "{:?}: {}", self.code, self.message)
|
|
@@ -22,7 +31,7 @@ impl fmt::Display for AppError {
|
|
|
22
31
|
|
|
23
32
|
impl std::error::Error for AppError {}
|
|
24
33
|
|
|
25
|
-
#[derive(Debug, Clone
|
|
34
|
+
#[derive(Debug, Clone)]
|
|
26
35
|
pub enum ErrorCode {
|
|
27
36
|
Unauthenticated,
|
|
28
37
|
Forbidden,
|
|
@@ -34,10 +43,17 @@ pub enum ErrorCode {
|
|
|
34
43
|
InternalError,
|
|
35
44
|
MalformedResponse,
|
|
36
45
|
Other,
|
|
46
|
+
RetryWithReference(ReferenceObject),
|
|
37
47
|
CompleteWorkflow,
|
|
38
48
|
CompleteParent,
|
|
39
49
|
}
|
|
40
50
|
|
|
51
|
+
#[derive(Debug, Clone)]
|
|
52
|
+
pub struct ReferenceObject {
|
|
53
|
+
pub reference: String,
|
|
54
|
+
pub status: String,
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
#[derive(Debug, Clone)]
|
|
42
58
|
pub struct Connection {
|
|
43
59
|
pub id: String,
|
|
@@ -58,6 +74,7 @@ pub struct ActionContext {
|
|
|
58
74
|
pub action_id: String,
|
|
59
75
|
pub connection: Connection,
|
|
60
76
|
pub serialized_input: String,
|
|
77
|
+
pub reference_object: Option<ReferenceObject>,
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
#[derive(Debug, Clone)]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
use magnus::{prelude::*, Error, TryConvert, Value};
|
|
2
|
-
use crate::types::ActionContext;
|
|
1
|
+
use magnus::{prelude::*, scan_args::scan_args, Error, RHash, Ruby, Symbol, TryConvert, Value};
|
|
2
|
+
use crate::types::{ActionContext, ReferenceObject};
|
|
3
3
|
use super::connection::RConnection;
|
|
4
4
|
|
|
5
5
|
#[magnus::wrap(class = "AppBridge::ActionContext")]
|
|
@@ -9,7 +9,29 @@ pub struct RActionContext {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
impl RActionContext {
|
|
12
|
-
|
|
12
|
+
fn parse_reference_object(value: Value) -> Result<ReferenceObject, Error> {
|
|
13
|
+
let reference: String = fetch_hash_string(value, "reference")?;
|
|
14
|
+
let status: String = fetch_hash_string(value, "status")?;
|
|
15
|
+
Ok(ReferenceObject {
|
|
16
|
+
reference,
|
|
17
|
+
status,
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn new(args: &[Value]) -> Result<Self, Error> {
|
|
22
|
+
let args = scan_args::<(String, Value, String), (Option<Value>,), (), (), (), ()>(args)?;
|
|
23
|
+
let (action_id, connection, serialized_input) = args.required;
|
|
24
|
+
let (retry,) = args.optional;
|
|
25
|
+
|
|
26
|
+
Self::build(action_id, connection, serialized_input, retry)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn build(
|
|
30
|
+
action_id: String,
|
|
31
|
+
connection: Value,
|
|
32
|
+
serialized_input: String,
|
|
33
|
+
retry: Option<Value>,
|
|
34
|
+
) -> Result<Self, Error> {
|
|
13
35
|
if connection.is_nil() {
|
|
14
36
|
return Err(Error::new(magnus::exception::runtime_error(), "Connection is required"));
|
|
15
37
|
}
|
|
@@ -19,14 +41,18 @@ impl RActionContext {
|
|
|
19
41
|
Err(_) => return Err(Error::new(magnus::exception::runtime_error(), "Connection is required")),
|
|
20
42
|
};
|
|
21
43
|
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
serialized_input,
|
|
44
|
+
let reference_object = match retry {
|
|
45
|
+
Some(value) if !value.is_nil() => Some(Self::parse_reference_object(value)?),
|
|
46
|
+
_ => None,
|
|
26
47
|
};
|
|
27
48
|
|
|
28
49
|
Ok(Self {
|
|
29
|
-
inner
|
|
50
|
+
inner: ActionContext {
|
|
51
|
+
action_id,
|
|
52
|
+
connection: wrapped_connection.clone().into(),
|
|
53
|
+
serialized_input,
|
|
54
|
+
reference_object,
|
|
55
|
+
},
|
|
30
56
|
wrapped_connection: Some(wrapped_connection),
|
|
31
57
|
})
|
|
32
58
|
}
|
|
@@ -42,6 +68,18 @@ impl RActionContext {
|
|
|
42
68
|
pub fn serialized_input(&self) -> String {
|
|
43
69
|
self.inner.serialized_input.clone()
|
|
44
70
|
}
|
|
71
|
+
|
|
72
|
+
pub fn reference_object(&self) -> Result<Value, Error> {
|
|
73
|
+
let ruby = Ruby::get().unwrap();
|
|
74
|
+
if let Some(reference_object) = &self.inner.reference_object {
|
|
75
|
+
let hash: RHash = ruby.hash_new();
|
|
76
|
+
hash.aset("reference", reference_object.reference.clone())?;
|
|
77
|
+
hash.aset("status", reference_object.status.clone())?;
|
|
78
|
+
Ok(hash.as_value())
|
|
79
|
+
} else {
|
|
80
|
+
Ok(ruby.qnil().as_value())
|
|
81
|
+
}
|
|
82
|
+
}
|
|
45
83
|
}
|
|
46
84
|
|
|
47
85
|
impl TryConvert for RActionContext {
|
|
@@ -49,6 +87,16 @@ impl TryConvert for RActionContext {
|
|
|
49
87
|
let connection_val: Value = val.funcall("connection", ())?;
|
|
50
88
|
let serialized_input: String = val.funcall("serialized_input", ())?;
|
|
51
89
|
let action_id: String = val.funcall("action_id", ())?;
|
|
90
|
+
let reference_object = if val.funcall("respond_to?", ("reference_object",))? {
|
|
91
|
+
let reference_object_val: Value = val.funcall("reference_object", ())?;
|
|
92
|
+
if reference_object_val.is_nil() {
|
|
93
|
+
None
|
|
94
|
+
} else {
|
|
95
|
+
Some(Self::parse_reference_object(reference_object_val)?)
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
None
|
|
99
|
+
};
|
|
52
100
|
|
|
53
101
|
if connection_val.is_nil() {
|
|
54
102
|
return Err(Error::new(magnus::exception::runtime_error(), "Connection is required"));
|
|
@@ -63,6 +111,7 @@ impl TryConvert for RActionContext {
|
|
|
63
111
|
action_id,
|
|
64
112
|
connection: wrapped_connection.clone().inner,
|
|
65
113
|
serialized_input,
|
|
114
|
+
reference_object,
|
|
66
115
|
};
|
|
67
116
|
|
|
68
117
|
Ok(Self {
|
|
@@ -72,6 +121,18 @@ impl TryConvert for RActionContext {
|
|
|
72
121
|
}
|
|
73
122
|
}
|
|
74
123
|
|
|
124
|
+
fn fetch_hash_string(hash: Value, key: &str) -> Result<String, Error> {
|
|
125
|
+
let value: Value = hash.funcall("[]", (key,))?;
|
|
126
|
+
let value = if value.is_nil() {
|
|
127
|
+
let symbol = Symbol::new(key);
|
|
128
|
+
hash.funcall("[]", (symbol,))?
|
|
129
|
+
} else {
|
|
130
|
+
value
|
|
131
|
+
};
|
|
132
|
+
TryConvert::try_convert(value)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
75
136
|
impl From<RActionContext> for ActionContext {
|
|
76
137
|
fn from(raction_context: RActionContext) -> Self {
|
|
77
138
|
raction_context.inner
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
package standout:app@4.1.0;
|
|
2
|
+
|
|
3
|
+
interface types {
|
|
4
|
+
// The trigger-store is a string that is used to store data between trigger
|
|
5
|
+
// invocations. It is unique per trigger instance and is persisted between
|
|
6
|
+
// invocations.
|
|
7
|
+
//
|
|
8
|
+
// You can store any string here. We suggest that you use a serialized
|
|
9
|
+
// JSON object or similar since that will give you some flexibility if you
|
|
10
|
+
// need to add more data to the store.
|
|
11
|
+
type trigger-store = string;
|
|
12
|
+
|
|
13
|
+
record connection {
|
|
14
|
+
id: string,
|
|
15
|
+
name: string,
|
|
16
|
+
// The connection data is a JSON object serialized into a string. The JSON root
|
|
17
|
+
// will always be an object.
|
|
18
|
+
serialized-data: string,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
record trigger-context {
|
|
22
|
+
// Trigger ID is a unique identifier for the trigger that is requested to be
|
|
23
|
+
// invoked.
|
|
24
|
+
trigger-id: string,
|
|
25
|
+
|
|
26
|
+
// The connection that the trigger is invoked for.
|
|
27
|
+
// Connection is required for all trigger operations.
|
|
28
|
+
connection: connection,
|
|
29
|
+
|
|
30
|
+
// The store will contain the data that was stored in the trigger store the
|
|
31
|
+
// last time the trigger was invoked.
|
|
32
|
+
store: trigger-store,
|
|
33
|
+
|
|
34
|
+
// The input data for the trigger, serialized as a JSON object string.
|
|
35
|
+
// This contains the input data from the trigger configuration form.
|
|
36
|
+
serialized-input: string,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
record action-context {
|
|
40
|
+
// Action ID is a unique identifier for the action that is requested to be
|
|
41
|
+
// invoked.
|
|
42
|
+
action-id: string,
|
|
43
|
+
|
|
44
|
+
// The connection that the action is invoked for.
|
|
45
|
+
// Connection is required for all action operations.
|
|
46
|
+
connection: connection,
|
|
47
|
+
|
|
48
|
+
// The input data for the action, serialized as a JSON object string.
|
|
49
|
+
// This contains the data passed from the previous step in the workflow.
|
|
50
|
+
serialized-input: string,
|
|
51
|
+
|
|
52
|
+
// Optional reference information when the platform retries an action.
|
|
53
|
+
reference-object: option<reference-object>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
record trigger-response {
|
|
57
|
+
// The trigger events, each event will be used to spawn a new workflow
|
|
58
|
+
// execution in Standouts integration platform.
|
|
59
|
+
events: list<trigger-event>,
|
|
60
|
+
|
|
61
|
+
// The updated store will be stored and used the next time the trigger is
|
|
62
|
+
// invoked.
|
|
63
|
+
store: trigger-store,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
record action-response {
|
|
67
|
+
// The output data from the action, serialized as a JSON object string.
|
|
68
|
+
// This contains the data that will be passed to the next step in the workflow.
|
|
69
|
+
// The data must be a valid JSON object (not an array or primitive).
|
|
70
|
+
serialized-output: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
record trigger-event {
|
|
74
|
+
// The ID of the trigger event
|
|
75
|
+
//
|
|
76
|
+
// If the connection used for the given instance of the trigger is the same,
|
|
77
|
+
// as seen before. Then the event will be ignored.
|
|
78
|
+
//
|
|
79
|
+
// A scheduler could therefore use a timestamp as the ID, to ensure that
|
|
80
|
+
// the event is only triggered once per given time.
|
|
81
|
+
//
|
|
82
|
+
// A trigger that acts on created orders in a e-commerce system could use
|
|
83
|
+
// the order ID as the ID, to ensure that the event is only triggered once
|
|
84
|
+
// per order.
|
|
85
|
+
//
|
|
86
|
+
// A trigger that acts on updated orders in a e-commerce system could use
|
|
87
|
+
// the order ID in combination with an updated at timestamp as the ID, to
|
|
88
|
+
// ensure that the event is only triggered once per order update.
|
|
89
|
+
id: string,
|
|
90
|
+
|
|
91
|
+
// Serialized data must be a JSON object serialized into a string
|
|
92
|
+
// Note that it is important that the root is an object, not an array,
|
|
93
|
+
// or another primitive type.
|
|
94
|
+
serialized-data: string,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Retry reference payload returned with error-code.retry-with-reference.
|
|
98
|
+
record reference-object {
|
|
99
|
+
/// Reference ID provided for retrying this request later.
|
|
100
|
+
reference: string,
|
|
101
|
+
|
|
102
|
+
/// Status describing the retry state.
|
|
103
|
+
status: string,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
record app-error {
|
|
107
|
+
/// The error code identifying the type of failure.
|
|
108
|
+
code: error-code,
|
|
109
|
+
|
|
110
|
+
/// A human-readable message describing the error in more detail.
|
|
111
|
+
message: string,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// An enumeration of error codes that can be returned by a trigger implementation.
|
|
115
|
+
/// These codes help the platform and plugin developers distinguish between different types of failures.
|
|
116
|
+
variant error-code {
|
|
117
|
+
/// Authentication failed. Typically due to an invalid or expired API key or token.
|
|
118
|
+
unauthenticated,
|
|
119
|
+
|
|
120
|
+
/// Authorization failed. The connection is valid but does not have the necessary permissions.
|
|
121
|
+
forbidden,
|
|
122
|
+
|
|
123
|
+
/// The trigger is misconfigured. For example, a required setting is missing or invalid.
|
|
124
|
+
misconfigured,
|
|
125
|
+
|
|
126
|
+
/// The target system does not support a required feature or endpoint.
|
|
127
|
+
unsupported,
|
|
128
|
+
|
|
129
|
+
/// The target system is rate-limiting requests. Try again later.
|
|
130
|
+
rate-limit,
|
|
131
|
+
|
|
132
|
+
/// The request timed out. The target system did not respond in time.
|
|
133
|
+
timeout,
|
|
134
|
+
|
|
135
|
+
/// The target system is currently unavailable or unreachable.
|
|
136
|
+
unavailable,
|
|
137
|
+
|
|
138
|
+
/// An unexpected internal error occurred in the plugin.
|
|
139
|
+
internal-error,
|
|
140
|
+
|
|
141
|
+
/// The response from the external system could not be parsed or was in an invalid format.
|
|
142
|
+
malformed-response,
|
|
143
|
+
|
|
144
|
+
/// A catch-all for all other types of errors. Should include a descriptive message.
|
|
145
|
+
other,
|
|
146
|
+
|
|
147
|
+
/// Retry the request using a reference identifier.
|
|
148
|
+
retry-with-reference(reference-object),
|
|
149
|
+
|
|
150
|
+
/// Complete the current workflow execution.
|
|
151
|
+
complete-workflow,
|
|
152
|
+
|
|
153
|
+
/// Complete the parent step execution.
|
|
154
|
+
complete-parent,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
interface triggers {
|
|
160
|
+
use types.{trigger-context, trigger-event, trigger-response, app-error};
|
|
161
|
+
|
|
162
|
+
trigger-ids: func() -> result<list<string>, app-error>;
|
|
163
|
+
|
|
164
|
+
// Get the input schema for a specific trigger
|
|
165
|
+
// Returns a JSON Schema Draft 2020-12 schema as a string
|
|
166
|
+
// The schema may vary based on the connection in the context
|
|
167
|
+
// The trigger-id is extracted from the context
|
|
168
|
+
input-schema: func(context: trigger-context) -> result<string, app-error>;
|
|
169
|
+
|
|
170
|
+
// Get the output schema for a specific trigger
|
|
171
|
+
// Returns a JSON Schema Draft 2020-12 schema as a string
|
|
172
|
+
// The schema may vary based on the connection in the context
|
|
173
|
+
// The trigger-id is extracted from the context
|
|
174
|
+
output-schema: func(context: trigger-context) -> result<string, app-error>;
|
|
175
|
+
|
|
176
|
+
// Fetch events
|
|
177
|
+
//
|
|
178
|
+
// There are some limitations to the function:
|
|
179
|
+
// - It must return a `trigger-response` within 30 seconds
|
|
180
|
+
// - It must return less than or equal to 100 `trigger-response.events`
|
|
181
|
+
// - It must not return more than 64 kB of data in the `trigger-response.store`
|
|
182
|
+
//
|
|
183
|
+
// If you need to fetch more events, you can return up to 100 events and then
|
|
184
|
+
// store the data needed for you to remember where you left off in the store.
|
|
185
|
+
// The next time the trigger is invoked, you can use the store to continue
|
|
186
|
+
// where you left off.
|
|
187
|
+
//
|
|
188
|
+
// If you do not pass the limitations the return value will be ignored. We
|
|
189
|
+
// will not handle any events and we persist the store that was returned in
|
|
190
|
+
// the response.
|
|
191
|
+
//
|
|
192
|
+
// That also means that you should implement your fetch event function in a
|
|
193
|
+
// way that it can be called multiple times using the same context and return
|
|
194
|
+
// the same events. That will ensure that the user that is building an
|
|
195
|
+
// integration with your trigger will not miss any events if your system is
|
|
196
|
+
// down for a short period of time.
|
|
197
|
+
fetch-events: func(context: trigger-context) -> result<trigger-response, app-error>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface actions {
|
|
201
|
+
use types.{action-context, action-response, app-error};
|
|
202
|
+
|
|
203
|
+
action-ids: func() -> result<list<string>, app-error>;
|
|
204
|
+
|
|
205
|
+
// Get the input schema for a specific action
|
|
206
|
+
// Returns a JSON Schema Draft 2020-12 schema as a string
|
|
207
|
+
// The schema may vary based on the connection in the context
|
|
208
|
+
// The action-id is extracted from the context
|
|
209
|
+
input-schema: func(context: action-context) -> result<string, app-error>;
|
|
210
|
+
|
|
211
|
+
// Get the output schema for a specific action
|
|
212
|
+
// Returns a JSON Schema Draft 2020-12 schema as a string
|
|
213
|
+
// The schema may vary based on the connection in the context
|
|
214
|
+
// The action-id is extracted from the context
|
|
215
|
+
output-schema: func(context: action-context) -> result<string, app-error>;
|
|
216
|
+
|
|
217
|
+
// Execute an action
|
|
218
|
+
//
|
|
219
|
+
// There are some limitations to the function:
|
|
220
|
+
// - It must return an `action-response` within 30 seconds
|
|
221
|
+
// - The serialized-output must be a valid JSON object serialized as a string
|
|
222
|
+
//
|
|
223
|
+
// Actions can perform various operations such as:
|
|
224
|
+
// - Making HTTP requests to external APIs
|
|
225
|
+
// - Processing and transforming data
|
|
226
|
+
// - Storing data for future use
|
|
227
|
+
// - Triggering other systems or workflows
|
|
228
|
+
//
|
|
229
|
+
// The action receives input data from the previous step and can return
|
|
230
|
+
// serialized output data to be passed to the next step in the workflow.
|
|
231
|
+
execute: func(context: action-context) -> result<action-response, app-error>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface environment {
|
|
235
|
+
// Get all environment variables
|
|
236
|
+
env-vars: func() -> list<tuple<string, string>>;
|
|
237
|
+
// Get a specific environment variable by name
|
|
238
|
+
env-var: func(name: string) -> option<string>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
interface http {
|
|
242
|
+
record response {
|
|
243
|
+
status: u16,
|
|
244
|
+
headers: headers,
|
|
245
|
+
body: string,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
record request {
|
|
249
|
+
method: method,
|
|
250
|
+
url: string,
|
|
251
|
+
headers: headers,
|
|
252
|
+
body: string,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
variant request-error {
|
|
256
|
+
other(string)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
type headers = list<tuple<string, string>>;
|
|
260
|
+
|
|
261
|
+
resource request-builder {
|
|
262
|
+
constructor();
|
|
263
|
+
|
|
264
|
+
method: func(method: method) -> request-builder;
|
|
265
|
+
url: func(url: string) -> request-builder;
|
|
266
|
+
|
|
267
|
+
// Add a header to the request
|
|
268
|
+
header: func(key: string, value: string) -> request-builder;
|
|
269
|
+
headers: func(headers: list<tuple<string, string>>) -> request-builder;
|
|
270
|
+
|
|
271
|
+
// Add a body to the request
|
|
272
|
+
body: func(body: string) -> request-builder;
|
|
273
|
+
// Add a binary body to the request
|
|
274
|
+
body-bytes: func(body: list<u8>) -> request-builder;
|
|
275
|
+
|
|
276
|
+
object: func() -> request;
|
|
277
|
+
|
|
278
|
+
// Send the request
|
|
279
|
+
send: func() -> result<response, request-error>;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
variant method {
|
|
283
|
+
get,
|
|
284
|
+
post,
|
|
285
|
+
put,
|
|
286
|
+
delete,
|
|
287
|
+
patch,
|
|
288
|
+
options,
|
|
289
|
+
head,
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
interface file {
|
|
294
|
+
// HTTP headers for file requests (same as http interface)
|
|
295
|
+
type headers = list<tuple<string, string>>;
|
|
296
|
+
|
|
297
|
+
// Normalized file data
|
|
298
|
+
record file-data {
|
|
299
|
+
// Base64-encoded file content
|
|
300
|
+
base64: string,
|
|
301
|
+
// MIME type (e.g., "application/pdf")
|
|
302
|
+
content-type: string,
|
|
303
|
+
// Filename
|
|
304
|
+
filename: string,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
variant file-error {
|
|
308
|
+
// Failed to fetch file from URL
|
|
309
|
+
fetch-failed(string),
|
|
310
|
+
// Invalid input format (not a valid URL, data URI, or base64)
|
|
311
|
+
invalid-input(string),
|
|
312
|
+
// Request timed out
|
|
313
|
+
timeout(string),
|
|
314
|
+
// Any other error
|
|
315
|
+
other(string),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Normalize any file source to FileData
|
|
319
|
+
//
|
|
320
|
+
// The source is automatically detected:
|
|
321
|
+
// - URL: "https://example.com/file.pdf" - fetched with optional headers
|
|
322
|
+
// - Data URI: "data:application/pdf;base64,JVBERi0..." - parsed and extracted
|
|
323
|
+
// - Base64: Any other string is treated as raw base64 - decoded to detect type
|
|
324
|
+
//
|
|
325
|
+
// Parameters:
|
|
326
|
+
// - source: URL, data URI, or base64-encoded content
|
|
327
|
+
// - headers: Optional HTTP headers for URL requests (e.g., Authorization)
|
|
328
|
+
// - filename: Optional filename override (auto-detected if not provided)
|
|
329
|
+
//
|
|
330
|
+
// Returns file-data which will be processed by the platform:
|
|
331
|
+
// 1. Fields with format: "file-output" in the output schema are identified
|
|
332
|
+
// 2. File data is uploaded using the configured file_uploader
|
|
333
|
+
// 3. The file-data is replaced with the blob ID in the response
|
|
334
|
+
normalize: func(source: string, headers: option<headers>, filename: option<string>) -> result<file-data, file-error>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
world bridge {
|
|
338
|
+
import http;
|
|
339
|
+
import environment;
|
|
340
|
+
import file;
|
|
341
|
+
export triggers;
|
|
342
|
+
export actions;
|
|
343
|
+
}
|
data/lib/app_bridge/version.rb
CHANGED
data/lib/app_bridge.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: app_bridge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Ross
|
|
@@ -61,6 +61,7 @@ files:
|
|
|
61
61
|
- ext/app_bridge/src/wrappers/trigger_response.rs
|
|
62
62
|
- ext/app_bridge/wit/v3/world.wit
|
|
63
63
|
- ext/app_bridge/wit/v4/world.wit
|
|
64
|
+
- ext/app_bridge/wit/v4_1/world.wit
|
|
64
65
|
- lib/app_bridge.rb
|
|
65
66
|
- lib/app_bridge/app.rb
|
|
66
67
|
- lib/app_bridge/file_processor.rb
|