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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00d50e19cf07ec17b1726b49439094e14edcd7735c69ac8ac4c949a5debe963c
4
- data.tar.gz: 4741cee1c95978801525dbf639890188d824a44f055d24939d43e8fcb8ce796a
3
+ metadata.gz: 26bd5103440c1de1bd93ea671635a87e8f25d39fc56fef5025752799e6731edd
4
+ data.tar.gz: 92157e6b2962fadcca66aa24b9baf234bf04e093f0c016cd51d8e7542a8da0b1
5
5
  SHA512:
6
- metadata.gz: a7690b6f2e084ac20fda5f9307a9dd0d4fc3a2760e6f8aa19c62998e57367b795d32cb4743fdb2adcd70732bebe79e9099953208ccd28a11fdbf1fc7aeb3bdb3
7
- data.tar.gz: 6692c79285a2f86cbeb87445082e28d3f415ea909c37115f0d29b5ac0cf5e079d284d1085108cb222c22093eac272bcdfb9739b5feb54f8668532c0ef0ceda8e
6
+ metadata.gz: f7f8c267d373d1e0f5ffa730956fc5e2d0bdc43f9143c61e3866596efae583d6e9403c2cf4ead9972af4cd16ee6d6097c5fa01d8974017de6f4a8c571316b7a9
7
+ data.tar.gz: 2bebe7b17a804216f59846866d374b4d2b2439ad77aee6ab1e4d51766dbeba6053b896f2906c0feda378d9cbbaa64b695897351113aa3ee3e157cdec20586d4a
data/Cargo.lock CHANGED
@@ -55,7 +55,7 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
55
55
 
56
56
  [[package]]
57
57
  name = "app_bridge"
58
- version = "4.0.0"
58
+ version = "4.1.0"
59
59
  dependencies = [
60
60
  "base64",
61
61
  "httpmock",
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.
@@ -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.0.0"
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! impl_conversions {
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
- // 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
-
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
- // ActionContext: canonical → version
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::{Error, ExceptionClass, Ruby};
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 {
@@ -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, 3))?;
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) => send_request(&self.client, &request)
110
- .map(Into::into)
111
- .map_err(Into::into),
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 = builder.body(request.body.clone());
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
  }
@@ -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, Copy)]
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
- pub fn new(action_id: String, connection: Value, serialized_input: String) -> Result<Self, Error> {
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 inner = ActionContext {
23
- action_id,
24
- connection: wrapped_connection.clone().into(),
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
+ }
@@ -4,5 +4,5 @@
4
4
  # ext/app_bridge/Cargo.toml file to keep them in sync.
5
5
 
6
6
  module AppBridge
7
- VERSION = "4.0.0"
7
+ VERSION = "4.1.0"
8
8
  end
data/lib/app_bridge.rb CHANGED
@@ -11,7 +11,6 @@ module AppBridge
11
11
  class TooManyEventsError < Error; end
12
12
  class StoreTooLargeError < Error; end
13
13
  class ActionResponseTooLargeError < Error; end
14
- class FileUploadError < Error; end
15
14
  class InternalError < Error; end
16
15
 
17
16
  class << self
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.0.0
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