app_bridge 3.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,338 @@
1
- use std::result::Result::Ok;
2
1
  use std::collections::HashMap;
3
- use wasmtime::component::{bindgen, Component, Linker};
2
+ use std::result::Result::Ok;
3
+ use wasmtime::component::{Component, Linker};
4
4
  use wasmtime::{Engine, Result, Store};
5
- use wasmtime_wasi::WasiCtxBuilder;
6
-
7
- bindgen!({
8
- path: "./wit",
9
- world: "bridge",
10
- });
5
+ use wasmtime_wasi::p2::WasiCtxBuilder;
11
6
 
12
7
  use crate::app_state::AppState;
8
+ use crate::types::{
9
+ ActionContext, ActionResponse, AppError, Connection, ErrorCode, TriggerContext,
10
+ ReferenceObject, TriggerEvent, TriggerResponse,
11
+ };
12
+
13
+ // ============================================================================
14
+ // WIT version modules
15
+ //
16
+ // To add a new version:
17
+ // 1. Add a new pub mod vN { bindgen!(...) }
18
+ // 2. Add impl_conversions!(vN) below
19
+ // 3. Add variant to BridgeWrapper enum
20
+ // 4. Add to build_linker() and app() functions
21
+ // 5. Add arm to each bridge_method! in BridgeWrapper impl
22
+ // ============================================================================
23
+
24
+ pub mod v3 {
25
+ wasmtime::component::bindgen!({
26
+ path: "./wit/v3",
27
+ world: "bridge",
28
+ });
29
+ }
30
+
31
+ pub mod v4 {
32
+ wasmtime::component::bindgen!({
33
+ path: "./wit/v4",
34
+ world: "bridge",
35
+ });
36
+ }
37
+
38
+ pub mod v4_1 {
39
+ wasmtime::component::bindgen!({
40
+ path: "./wit/v4_1",
41
+ world: "bridge",
42
+ });
43
+ }
44
+
45
+ // ============================================================================
46
+ // Version conversion macro - generates From impls for a version module
47
+ // ============================================================================
48
+
49
+ macro_rules! impl_error_code_conversion {
50
+ ($v:ident, $($extra_arms:tt)*) => {
51
+ impl From<$v::standout::app::types::ErrorCode> for ErrorCode {
52
+ fn from(c: $v::standout::app::types::ErrorCode) -> Self {
53
+ use $v::standout::app::types::ErrorCode as V;
54
+ match c {
55
+ V::Unauthenticated => Self::Unauthenticated,
56
+ V::Forbidden => Self::Forbidden,
57
+ V::Misconfigured => Self::Misconfigured,
58
+ V::Unsupported => Self::Unsupported,
59
+ V::RateLimit => Self::RateLimit,
60
+ V::Timeout => Self::Timeout,
61
+ V::Unavailable => Self::Unavailable,
62
+ V::InternalError => Self::InternalError,
63
+ V::MalformedResponse => Self::MalformedResponse,
64
+ V::Other => Self::Other,
65
+ $($extra_arms)*
66
+ V::CompleteWorkflow => Self::CompleteWorkflow,
67
+ V::CompleteParent => Self::CompleteParent,
68
+ }
69
+ }
70
+ }
71
+ };
72
+ }
73
+
74
+ macro_rules! impl_conversions {
75
+ ($v:ident) => {
76
+ // TriggerEvent: version → canonical
77
+ impl From<$v::standout::app::types::TriggerEvent> for TriggerEvent {
78
+ fn from(e: $v::standout::app::types::TriggerEvent) -> Self {
79
+ Self { id: e.id, serialized_data: e.serialized_data }
80
+ }
81
+ }
82
+
83
+ // TriggerResponse: version → canonical
84
+ impl From<$v::standout::app::types::TriggerResponse> for TriggerResponse {
85
+ fn from(r: $v::standout::app::types::TriggerResponse) -> Self {
86
+ Self {
87
+ store: r.store,
88
+ events: r.events.into_iter().map(Into::into).collect(),
89
+ }
90
+ }
91
+ }
92
+
93
+ // ActionResponse: version → canonical
94
+ impl From<$v::standout::app::types::ActionResponse> for ActionResponse {
95
+ fn from(r: $v::standout::app::types::ActionResponse) -> Self {
96
+ Self { serialized_output: r.serialized_output }
97
+ }
98
+ }
99
+
100
+ // Connection: canonical → version (for passing to components)
101
+ impl From<&Connection> for $v::standout::app::types::Connection {
102
+ fn from(c: &Connection) -> Self {
103
+ Self {
104
+ id: c.id.clone(),
105
+ name: c.name.clone(),
106
+ serialized_data: c.serialized_data.clone(),
107
+ }
108
+ }
109
+ }
110
+
111
+ // TriggerContext: canonical → version
112
+ impl From<&TriggerContext> for $v::standout::app::types::TriggerContext {
113
+ fn from(c: &TriggerContext) -> Self {
114
+ Self {
115
+ trigger_id: c.trigger_id.clone(),
116
+ connection: (&c.connection).into(),
117
+ store: c.store.clone(),
118
+ serialized_input: c.serialized_input.clone(),
119
+ }
120
+ }
121
+ }
122
+
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) => {
141
+ impl From<&ActionContext> for $v::standout::app::types::ActionContext {
142
+ fn from(c: &ActionContext) -> Self {
143
+ Self {
144
+ action_id: c.action_id.clone(),
145
+ connection: (&c.connection).into(),
146
+ serialized_input: c.serialized_input.clone(),
147
+ }
148
+ }
149
+ }
150
+ };
151
+ }
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
+
186
+ // Generate conversions for all supported versions
187
+ impl_error_code_conversion!(v3,);
188
+ impl_conversions!(v3);
189
+ impl_app_error_conversion!(v3);
190
+ impl_action_context_conversion_basic!(v3);
191
+ impl_error_code_conversion!(v4,);
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);
202
+
203
+ // ============================================================================
204
+ // BridgeWrapper - unified interface for all component versions
205
+ //
206
+ // To add vN: add variant BridgeWrapper::VN(vN::Bridge)
207
+ // ============================================================================
208
+
209
+ pub enum BridgeWrapper {
210
+ V3(v3::Bridge),
211
+ V4(v4::Bridge),
212
+ V4_1(v4_1::Bridge),
213
+ }
214
+
215
+ impl BridgeWrapper {
216
+ /// Returns the WIT version this component was built against
217
+ pub fn wit_version(&self) -> &'static str {
218
+ match self {
219
+ BridgeWrapper::V3(_) => "3.0.0",
220
+ BridgeWrapper::V4(_) => "4.0.0",
221
+ BridgeWrapper::V4_1(_) => "4.1.0",
222
+ }
223
+ }
224
+ }
225
+
226
+ /// Macro to implement a bridge method that works across all versions.
227
+ /// Each version's result is converted to canonical types.
228
+ macro_rules! bridge_method {
229
+ // Simple no-arg method (e.g., trigger_ids, action_ids)
230
+ (fn $name:ident() -> Result<$ok_type:ty> via $interface:ident . $method:ident) => {
231
+ pub fn $name(&self, store: &mut Store<AppState>) -> Result<std::result::Result<$ok_type, AppError>> {
232
+ match self {
233
+ BridgeWrapper::V3(b) => {
234
+ let r = b.$interface().$method(store)?;
235
+ Ok(r.map_err(Into::into))
236
+ }
237
+ BridgeWrapper::V4(b) => {
238
+ let r = b.$interface().$method(store)?;
239
+ Ok(r.map_err(Into::into))
240
+ }
241
+ BridgeWrapper::V4_1(b) => {
242
+ let r = b.$interface().$method(store)?;
243
+ Ok(r.map_err(Into::into))
244
+ }
245
+ }
246
+ }
247
+ };
248
+ // Method with TriggerContext
249
+ (fn $name:ident(&TriggerContext) -> Result<$ok_type:ty> via $interface:ident . $method:ident) => {
250
+ pub fn $name(&self, store: &mut Store<AppState>, ctx: &TriggerContext) -> Result<std::result::Result<$ok_type, AppError>> {
251
+ match self {
252
+ BridgeWrapper::V3(b) => {
253
+ let r = b.$interface().$method(store, &ctx.into())?;
254
+ Ok(r.map(Into::into).map_err(Into::into))
255
+ }
256
+ BridgeWrapper::V4(b) => {
257
+ let r = b.$interface().$method(store, &ctx.into())?;
258
+ Ok(r.map(Into::into).map_err(Into::into))
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
+ }
264
+ }
265
+ }
266
+ };
267
+ // Method with ActionContext
268
+ (fn $name:ident(&ActionContext) -> Result<$ok_type:ty> via $interface:ident . $method:ident) => {
269
+ pub fn $name(&self, store: &mut Store<AppState>, ctx: &ActionContext) -> Result<std::result::Result<$ok_type, AppError>> {
270
+ match self {
271
+ BridgeWrapper::V3(b) => {
272
+ let r = b.$interface().$method(store, &ctx.into())?;
273
+ Ok(r.map(Into::into).map_err(Into::into))
274
+ }
275
+ BridgeWrapper::V4(b) => {
276
+ let r = b.$interface().$method(store, &ctx.into())?;
277
+ Ok(r.map(Into::into).map_err(Into::into))
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
+ }
283
+ }
284
+ }
285
+ };
286
+ }
287
+
288
+ impl BridgeWrapper {
289
+ // Trigger methods
290
+ bridge_method!(fn call_trigger_ids() -> Result<Vec<String>> via standout_app_triggers . call_trigger_ids);
291
+ bridge_method!(fn call_trigger_input_schema(&TriggerContext) -> Result<String> via standout_app_triggers . call_input_schema);
292
+ bridge_method!(fn call_trigger_output_schema(&TriggerContext) -> Result<String> via standout_app_triggers . call_output_schema);
293
+ bridge_method!(fn call_fetch_events(&TriggerContext) -> Result<TriggerResponse> via standout_app_triggers . call_fetch_events);
294
+
295
+ // Action methods
296
+ bridge_method!(fn call_action_ids() -> Result<Vec<String>> via standout_app_actions . call_action_ids);
297
+ bridge_method!(fn call_action_input_schema(&ActionContext) -> Result<String> via standout_app_actions . call_input_schema);
298
+ bridge_method!(fn call_action_output_schema(&ActionContext) -> Result<String> via standout_app_actions . call_output_schema);
299
+ bridge_method!(fn call_execute(&ActionContext) -> Result<ActionResponse> via standout_app_actions . call_execute);
300
+ }
301
+
302
+ // ============================================================================
303
+ // Builder functions
304
+ // ============================================================================
13
305
 
14
306
  pub fn build_engine() -> Engine {
15
307
  Engine::default()
16
308
  }
17
309
 
18
310
  pub fn build_linker(engine: &Engine) -> Result<Linker<AppState>> {
19
- let mut linker = Linker::<AppState>::new(&engine);
20
- wasmtime_wasi::add_to_linker_sync(&mut linker)?;
21
- standout::app::http::add_to_linker(&mut linker, |s| s)?;
22
- standout::app::environment::add_to_linker(&mut linker, |s| s)?;
311
+ let mut linker = Linker::<AppState>::new(engine);
312
+
313
+ // WASI support (shared by all versions)
314
+ wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
315
+
316
+ // ---- Version-specific interfaces ----
317
+ // v3: http + environment
318
+ v3::standout::app::http::add_to_linker(&mut linker, |s| s)?;
319
+ v3::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
320
+
321
+ // v4: http + environment + file
322
+ v4::standout::app::http::add_to_linker(&mut linker, |s| s)?;
323
+ v4::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
324
+ v4::standout::app::file::add_to_linker(&mut linker, |s| s)?;
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
+
331
+ // Add new versions here:
332
+ // v5::standout::app::http::add_to_linker(&mut linker, |s| s)?;
333
+ // v5::standout::app::environment::add_to_linker(&mut linker, |s| s)?;
334
+ // v5::standout::app::file::add_to_linker(&mut linker, |s| s)?;
335
+ // v5::standout::app::new_feature::add_to_linker(&mut linker, |s| s)?;
23
336
 
24
337
  Ok(linker)
25
338
  }
@@ -27,54 +340,42 @@ pub fn build_linker(engine: &Engine) -> Result<Linker<AppState>> {
27
340
  pub fn build_store(engine: &Engine, env_vars: Option<HashMap<String, String>>) -> Store<AppState> {
28
341
  let mut builder = WasiCtxBuilder::new();
29
342
 
30
- // Add environment variables to WASI context if provided
31
343
  if let Some(env_vars) = &env_vars {
32
344
  for (key, value) in env_vars {
33
345
  builder.env(key, value);
34
346
  }
35
347
  }
36
348
 
37
- let ctx = builder.build();
38
-
39
- // Create AppState with or without environment variables
40
- let app_state = AppState::new(ctx, env_vars);
41
-
42
- Store::new(&engine, app_state)
349
+ Store::new(engine, AppState::new(builder.build(), env_vars))
43
350
  }
44
351
 
352
+ /// Try to instantiate a WASM component.
353
+ /// Attempts versions from newest to oldest until one succeeds.
45
354
  pub fn app(
46
355
  file_path: String,
47
356
  engine: Engine,
48
357
  store: &mut Store<AppState>,
49
358
  linker: Linker<AppState>,
50
- ) -> Result<Bridge> {
51
- // Load the application component from the file system.
52
- let component = Component::from_file(&engine, file_path)?;
53
-
54
- // Try to instantiate the component - if it fails due to missing interface,
55
- // we'll catch that error and return a specific message
56
- match Bridge::instantiate(store, &component, &linker) {
57
- Ok(instance) => Ok(instance),
58
- Err(e) => {
59
- if e.to_string().contains("no exported instance") {
60
- Err(wasmtime::Error::msg(
61
- "Incompatible WASM file version"
62
- ))
63
- } else {
64
- Err(e)
65
- }
66
- }
359
+ ) -> Result<BridgeWrapper> {
360
+ let component = Component::from_file(&engine, &file_path)?;
361
+
362
+ // Try versions newest-first. When adding vN, insert at the top.
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));
67
366
  }
68
- }
69
367
 
70
- impl Default for Bridge {
71
- fn default() -> Self {
72
- let file_path = "spec/fixtures/components/example_app.wasm";
73
- let engine = Engine::default();
74
- let mut store = build_store(&engine, None);
75
- let linker = build_linker(&engine).unwrap();
76
- let instant = app(file_path.to_string(), engine, &mut store, linker);
368
+ // v4
369
+ if let Ok(instance) = v4::Bridge::instantiate(&mut *store, &component, &linker) {
370
+ return Ok(BridgeWrapper::V4(instance));
371
+ }
77
372
 
78
- instant.unwrap()
373
+ // v3 (legacy - no file interface)
374
+ if let Ok(instance) = v3::Bridge::instantiate(&mut *store, &component, &linker) {
375
+ return Ok(BridgeWrapper::V3(instance));
79
376
  }
377
+
378
+ Err(wasmtime::Error::msg(
379
+ "Failed to instantiate component: no compatible WIT version found (tried v4.1, v4, v3)",
380
+ ))
80
381
  }
@@ -1,32 +1,48 @@
1
- use crate::component::standout::app::types::{AppError, ErrorCode};
2
- use magnus::{self, Error, Ruby, ExceptionClass};
1
+ use crate::types::{AppError, ErrorCode};
2
+ use magnus::prelude::*;
3
+ use magnus::{Error, ExceptionClass, RObject, Ruby};
3
4
 
4
5
  impl From<ErrorCode> for ExceptionClass {
5
- fn from(value: ErrorCode) -> Self {
6
- fn get_class(name: &str) -> ExceptionClass {
7
- let ruby = Ruby::get().unwrap();
8
- ruby.eval::<ExceptionClass>(name).unwrap()
9
- }
6
+ fn from(value: ErrorCode) -> Self {
7
+ fn get_class(name: &str) -> ExceptionClass {
8
+ let ruby = Ruby::get().unwrap();
9
+ ruby.eval::<ExceptionClass>(name).unwrap()
10
+ }
10
11
 
11
- match value {
12
- ErrorCode::Unauthenticated => get_class("AppBridge::UnauthenticatedError"),
13
- ErrorCode::Forbidden => get_class("AppBridge::ForbiddenError"),
14
- ErrorCode::Misconfigured => get_class("AppBridge::MisconfiguredError"),
15
- ErrorCode::Unsupported => get_class("AppBridge::UnsupportedError"),
16
- ErrorCode::RateLimit => get_class("AppBridge::RateLimitError"),
17
- ErrorCode::Timeout => get_class("AppBridge::TimeoutError"),
18
- ErrorCode::Unavailable => get_class("AppBridge::UnavailableError"),
19
- ErrorCode::InternalError => get_class("AppBridge::InternalError"),
20
- ErrorCode::MalformedResponse => get_class("AppBridge::MalformedResponseError"),
21
- ErrorCode::Other => get_class("AppBridge::OtherError"),
22
- ErrorCode::CompleteWorkflow => get_class("AppBridge::CompleteWorkflowException"),
23
- ErrorCode::CompleteParent => get_class("AppBridge::CompleteParentException"),
12
+ match value {
13
+ ErrorCode::Unauthenticated => get_class("AppBridge::UnauthenticatedError"),
14
+ ErrorCode::Forbidden => get_class("AppBridge::ForbiddenError"),
15
+ ErrorCode::Misconfigured => get_class("AppBridge::MisconfiguredError"),
16
+ ErrorCode::Unsupported => get_class("AppBridge::UnsupportedError"),
17
+ ErrorCode::RateLimit => get_class("AppBridge::RateLimitError"),
18
+ ErrorCode::Timeout => get_class("AppBridge::TimeoutError"),
19
+ ErrorCode::Unavailable => get_class("AppBridge::UnavailableError"),
20
+ ErrorCode::InternalError => get_class("AppBridge::InternalError"),
21
+ ErrorCode::MalformedResponse => get_class("AppBridge::MalformedResponseError"),
22
+ ErrorCode::Other => get_class("AppBridge::OtherError"),
23
+ ErrorCode::RetryWithReference(_) => get_class("AppBridge::RetryWithReferenceError"),
24
+ ErrorCode::CompleteWorkflow => get_class("AppBridge::CompleteWorkflowException"),
25
+ ErrorCode::CompleteParent => get_class("AppBridge::CompleteParentException"),
26
+ }
24
27
  }
25
- }
26
28
  }
27
29
 
28
30
  impl From<AppError> for Error {
29
- fn from(value: AppError) -> Self {
30
- Error::new(value.code.into(), value.message)
31
- }
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
+
46
+ Error::new(value.code.into(), value.message)
47
+ }
32
48
  }