app_bridge 2.0.0 → 2.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.
data/Rakefile CHANGED
@@ -3,8 +3,6 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
5
 
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
6
  require "rubocop/rake_task"
9
7
 
10
8
  RuboCop::RakeTask.new
@@ -22,4 +20,9 @@ end
22
20
  # Load all project specific rake tasks
23
21
  Dir.glob(File.expand_path("tasks/**/*.rake", __dir__)).each { |file| load file }
24
22
 
25
- task default: %i[fixtures compile spec rubocop]
23
+ # Set test mode environment variable for specs
24
+ RSpec::Core::RakeTask.new(:spec) do |_t|
25
+ ENV["APP_BRIDGE_TEST_MODE"] = "1"
26
+ end
27
+
28
+ task default: %i[fixtures compile rust:test spec rubocop]
@@ -1,6 +1,8 @@
1
1
  [package]
2
2
  name = "app_bridge"
3
- version = "1.0.0"
3
+ # When updating the version, please also update the version in the
4
+ # lib/app_bridge/version.rb file to keep them in sync.
5
+ version = "2.1.0"
4
6
  edition = "2021"
5
7
  authors = ["Alexander Ross <ross@standout.se>"]
6
8
  publish = false
@@ -10,6 +12,9 @@ crate-type = ["cdylib"]
10
12
 
11
13
  [dependencies]
12
14
  magnus = { version = "0.7.1" }
13
- wasmtime = "29.0.1"
14
- wasmtime-wasi = "29.0.1"
15
+ wasmtime = "32.0.0"
16
+ wasmtime-wasi = "32.0.0"
15
17
  reqwest = { version = "0.12", features = ["blocking", "json", "native-tls-vendored"] }
18
+
19
+ [dev-dependencies]
20
+ httpmock = "0.7.0"
@@ -4,7 +4,7 @@ use reqwest::blocking::Client;
4
4
  use std::collections::HashMap;
5
5
  use std::sync::{Arc, Mutex};
6
6
  use wasmtime::component::ResourceTable;
7
- use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};
7
+ use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView, IoView};
8
8
 
9
9
  pub struct AppState {
10
10
  ctx: WasiCtx,
@@ -12,16 +12,18 @@ pub struct AppState {
12
12
  pub client: Arc<Mutex<Client>>,
13
13
  pub request_list: HashMap<u32, Request>,
14
14
  pub next_request_id: u32,
15
+ pub environment_variables: HashMap<String, String>,
15
16
  }
16
17
 
17
18
  impl AppState {
18
- pub fn new(ctx: WasiCtx) -> Self {
19
+ pub fn new(ctx: WasiCtx, env_vars: Option<HashMap<String, String>>) -> Self {
19
20
  Self {
20
21
  ctx,
21
22
  table: ResourceTable::new(),
22
23
  client: Arc::new(Mutex::new(Client::new())),
23
24
  request_list: HashMap::new(),
24
25
  next_request_id: 0,
26
+ environment_variables: env_vars.unwrap_or_default(),
25
27
  }
26
28
  }
27
29
  }
@@ -30,17 +32,30 @@ impl standout::app::http::Host for AppState {
30
32
  // Impl http host methods here
31
33
  }
32
34
 
33
- impl WasiView for AppState {
34
- fn ctx(&mut self) -> &mut WasiCtx {
35
- &mut self.ctx
35
+ impl standout::app::environment::Host for AppState {
36
+ fn env_vars(&mut self) -> Vec<(String, String)> {
37
+ self.environment_variables.clone().into_iter().collect()
36
38
  }
39
+
40
+ fn env_var(&mut self, name: String) -> Option<String> {
41
+ self.environment_variables.get(&name).cloned()
42
+ }
43
+ }
44
+
45
+ impl IoView for AppState {
37
46
  fn table(&mut self) -> &mut ResourceTable {
38
47
  &mut self.table
39
48
  }
40
49
  }
41
50
 
51
+ impl WasiView for AppState {
52
+ fn ctx(&mut self) -> &mut WasiCtx {
53
+ &mut self.ctx
54
+ }
55
+ }
56
+
42
57
  impl Default for AppState {
43
58
  fn default() -> Self {
44
- Self::new(WasiCtxBuilder::new().build())
59
+ Self::new(WasiCtxBuilder::new().build(), None)
45
60
  }
46
61
  }
@@ -1,9 +1,13 @@
1
1
  use std::result::Result::Ok;
2
+ use std::collections::HashMap;
2
3
  use wasmtime::component::{bindgen, Component, Linker};
3
4
  use wasmtime::{Engine, Result, Store};
4
5
  use wasmtime_wasi::WasiCtxBuilder;
5
6
 
6
- bindgen!();
7
+ bindgen!({
8
+ path: "./wit",
9
+ world: "bridge",
10
+ });
7
11
 
8
12
  use crate::app_state::AppState;
9
13
 
@@ -15,18 +19,27 @@ pub fn build_linker(engine: &Engine) -> Result<Linker<AppState>> {
15
19
  let mut linker = Linker::<AppState>::new(&engine);
16
20
  wasmtime_wasi::add_to_linker_sync(&mut linker)?;
17
21
  standout::app::http::add_to_linker(&mut linker, |s| s)?;
22
+ standout::app::environment::add_to_linker(&mut linker, |s| s)?;
18
23
 
19
24
  Ok(linker)
20
25
  }
21
26
 
22
- pub fn build_store(engine: &Engine) -> Store<AppState> {
23
- let builder = WasiCtxBuilder::new().build();
27
+ pub fn build_store(engine: &Engine, env_vars: Option<HashMap<String, String>>) -> Store<AppState> {
28
+ let mut builder = WasiCtxBuilder::new();
24
29
 
25
- // ... configure `builder` more to add env vars, args, etc ...
30
+ // Add environment variables to WASI context if provided
31
+ if let Some(env_vars) = &env_vars {
32
+ for (key, value) in env_vars {
33
+ builder.env(key, value);
34
+ }
35
+ }
36
+
37
+ let ctx = builder.build();
26
38
 
27
- let store = Store::new(&engine, AppState::new(builder));
39
+ // Create AppState with or without environment variables
40
+ let app_state = AppState::new(ctx, env_vars);
28
41
 
29
- store
42
+ Store::new(&engine, app_state)
30
43
  }
31
44
 
32
45
  pub fn app(
@@ -37,16 +50,28 @@ pub fn app(
37
50
  ) -> Result<Bridge> {
38
51
  // Load the application component from the file system.
39
52
  let component = Component::from_file(&engine, file_path)?;
40
- let instance = Bridge::instantiate(store, &component, &linker)?;
41
53
 
42
- Ok(instance)
54
+ // Try to instantiate the component - if it fails due to missing interface,
55
+ // we'll catch that error and return a specific message
56
+ match Bridge::instantiate(store, &component, &linker) {
57
+ Ok(instance) => Ok(instance),
58
+ Err(e) => {
59
+ if e.to_string().contains("no exported instance") {
60
+ Err(wasmtime::Error::msg(
61
+ "Incompatible WASM file version"
62
+ ))
63
+ } else {
64
+ Err(e)
65
+ }
66
+ }
67
+ }
43
68
  }
44
69
 
45
70
  impl Default for Bridge {
46
71
  fn default() -> Self {
47
72
  let file_path = "spec/fixtures/components/example_app.wasm";
48
73
  let engine = Engine::default();
49
- let mut store = build_store(&engine);
74
+ let mut store = build_store(&engine, None);
50
75
  let linker = build_linker(&engine).unwrap();
51
76
  let instant = app(file_path.to_string(), engine, &mut store, linker);
52
77
 
@@ -5,10 +5,12 @@ mod request_builder;
5
5
  mod error_mapping;
6
6
 
7
7
  mod wrappers;
8
- use wrappers::account::RAccount;
8
+ use wrappers::connection::RConnection;
9
9
  use wrappers::trigger_context::RTriggerContext;
10
10
  use wrappers::trigger_event::RTriggerEvent;
11
11
  use wrappers::trigger_response::RTriggerResponse;
12
+ use wrappers::action_context::RActionContext;
13
+ use wrappers::action_response::RActionResponse;
12
14
  use wrappers::app::MutRApp;
13
15
 
14
16
  #[magnus::init]
@@ -27,12 +29,12 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
27
29
  module.define_error("MalformedResponseError", error)?;
28
30
  module.define_error("OtherError", error)?;
29
31
 
30
- // Define the Accout class
31
- let account_class = module.define_class("Account", ruby.class_object())?;
32
- account_class.define_singleton_method("new", function!(RAccount::new, 3))?;
33
- account_class.define_method("id", method!(RAccount::id, 0))?;
34
- account_class.define_method("name", method!(RAccount::name, 0))?;
35
- account_class.define_method("serialized_data", method!(RAccount::serialized_data, 0))?;
32
+ // Define the Connection class
33
+ let connection_class = module.define_class("Connection", ruby.class_object())?;
34
+ connection_class.define_singleton_method("new", function!(RConnection::new, 3))?;
35
+ connection_class.define_method("id", method!(RConnection::id, 0))?;
36
+ connection_class.define_method("name", method!(RConnection::name, 0))?;
37
+ connection_class.define_method("serialized_data", method!(RConnection::serialized_data, 0))?;
36
38
 
37
39
  let trigger_event_class = module.define_class("TriggerEvent", ruby.class_object())?;
38
40
  trigger_event_class.define_singleton_method("new", function!(RTriggerEvent::new, 2))?;
@@ -48,17 +50,35 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
48
50
  trigger_response_class.define_method("events", method!(RTriggerResponse::events, 0))?;
49
51
 
50
52
  let trigger_context_class = module.define_class("TriggerContext", ruby.class_object())?;
51
- trigger_context_class.define_singleton_method("new", function!(RTriggerContext::new, 3))?;
53
+ trigger_context_class.define_singleton_method("new", function!(RTriggerContext::new, 4))?;
52
54
  trigger_context_class.define_method("trigger_id", method!(RTriggerContext::trigger_id, 0))?;
53
- trigger_context_class.define_method("account", method!(RTriggerContext::account, 0))?;
55
+ trigger_context_class.define_method("connection", method!(RTriggerContext::connection, 0))?;
54
56
  trigger_context_class.define_method("store", method!(RTriggerContext::store, 0))?;
57
+ trigger_context_class.define_method("serialized_input", method!(RTriggerContext::serialized_input, 0))?;
58
+
59
+ // Define the Action classes
60
+ let action_context_class = module.define_class("ActionContext", ruby.class_object())?;
61
+ action_context_class.define_singleton_method("new", function!(RActionContext::new, 3))?;
62
+ action_context_class.define_method("action_id", method!(RActionContext::action_id, 0))?;
63
+ action_context_class.define_method("connection", method!(RActionContext::connection, 0))?;
64
+ action_context_class.define_method("serialized_input", method!(RActionContext::serialized_input, 0))?;
65
+
66
+ let action_response_class = module.define_class("ActionResponse", ruby.class_object())?;
67
+ action_response_class.define_singleton_method("new", function!(RActionResponse::new, 1))?;
68
+ action_response_class.define_method("serialized_output", method!(RActionResponse::serialized_output, 0))?;
55
69
 
56
70
  // Define the App class
57
71
  let app_class = module.define_class("App", ruby.class_object())?;
58
72
  app_class.define_alloc_func::<MutRApp>();
59
- app_class.define_method("initialize", method!(MutRApp::initialize, 1))?;
60
73
  app_class.define_method("trigger_ids", method!(MutRApp::trigger_ids, 0))?;
74
+ app_class.define_method("action_ids", method!(MutRApp::action_ids, 0))?;
75
+ app_class.define_method("action_input_schema", method!(MutRApp::action_input_schema, 1))?;
76
+ app_class.define_method("action_output_schema", method!(MutRApp::action_output_schema, 1))?;
77
+ app_class.define_method("trigger_input_schema", method!(MutRApp::trigger_input_schema, 1))?;
78
+ app_class.define_method("trigger_output_schema", method!(MutRApp::trigger_output_schema, 1))?;
79
+ app_class.define_private_method("_rust_initialize", method!(MutRApp::initialize, 2))?;
61
80
  app_class.define_private_method("_rust_fetch_events", method!(MutRApp::rb_fetch_events, 1))?;
81
+ app_class.define_private_method("_rust_execute_action", method!(MutRApp::rb_execute_action, 1))?;
62
82
 
63
83
  Ok(())
64
84
  }
@@ -162,11 +162,17 @@ impl From<Method> for ReqwestMethod {
162
162
 
163
163
  impl Default for Request {
164
164
  fn default() -> Self {
165
+ let version = env!("CARGO_PKG_VERSION");
166
+ let user_agent = format!("Standout-AppBridge/{version}");
167
+ let headers = vec![
168
+ ("User-Agent".to_string(), user_agent.into()),
169
+ ];
170
+
165
171
  Self {
166
172
  url: "".to_string(),
167
173
  method: Method::Get,
168
174
  body: "".to_string(),
169
- headers: Vec::new(),
175
+ headers,
170
176
  }
171
177
  }
172
178
  }
@@ -194,3 +200,35 @@ impl std::fmt::Display for Method {
194
200
  }
195
201
  }
196
202
  }
203
+
204
+
205
+ #[cfg(test)]
206
+ mod tests {
207
+ use super::*;
208
+ use httpmock::{MockServer, Method::GET};
209
+
210
+ #[test]
211
+ fn sends_request_with_default_user_agent() {
212
+ let version = env!("CARGO_PKG_VERSION");
213
+ let user_agent = format!("Standout-AppBridge/{version}");
214
+
215
+ let server = MockServer::start();
216
+ let mock = server.mock(|when, then| {
217
+ when.method(GET)
218
+ .path("/headers")
219
+ .header("User-Agent", user_agent.clone());
220
+ then.status(200);
221
+ });
222
+ let url = format!("{}/headers", server.base_url());
223
+
224
+ let mut app_state = AppState::default();
225
+ let builder = app_state.new();
226
+ let builder = app_state.method(builder, Method::Get);
227
+ let builder = app_state.url(builder, url);
228
+
229
+ let response = app_state.send(builder).expect("Request failed");
230
+
231
+ assert_eq!(response.status, 200);
232
+ mock.assert();
233
+ }
234
+ }
@@ -0,0 +1,79 @@
1
+ use magnus::{prelude::*, Error, TryConvert, Value};
2
+ use crate::component::standout::app::types::ActionContext;
3
+ use super::connection::RConnection;
4
+
5
+ #[magnus::wrap(class = "AppBridge::ActionContext")]
6
+ pub struct RActionContext {
7
+ inner: ActionContext,
8
+ wrapped_connection: Option<RConnection>,
9
+ }
10
+
11
+ impl RActionContext {
12
+ pub fn new(action_id: String, connection: Value, serialized_input: String) -> Result<Self, Error> {
13
+ if connection.is_nil() {
14
+ return Err(Error::new(magnus::exception::runtime_error(), "Connection is required"));
15
+ }
16
+
17
+ let wrapped_connection: RConnection = match TryConvert::try_convert(connection) {
18
+ Ok(conn) => conn,
19
+ Err(_) => return Err(Error::new(magnus::exception::runtime_error(), "Connection is required")),
20
+ };
21
+
22
+ let inner = ActionContext {
23
+ action_id: action_id,
24
+ connection: wrapped_connection.clone().into(),
25
+ serialized_input,
26
+ };
27
+
28
+ Ok(Self {
29
+ inner,
30
+ wrapped_connection: Some(wrapped_connection),
31
+ })
32
+ }
33
+
34
+ pub fn action_id(&self) -> String {
35
+ self.inner.action_id.clone()
36
+ }
37
+
38
+ pub fn connection(&self) -> RConnection {
39
+ self.wrapped_connection.clone().unwrap()
40
+ }
41
+
42
+ pub fn serialized_input(&self) -> String {
43
+ self.inner.serialized_input.clone()
44
+ }
45
+ }
46
+
47
+ impl TryConvert for RActionContext {
48
+ fn try_convert(val: Value) -> Result<Self, Error> {
49
+ let connection_val: Value = val.funcall("connection", ())?;
50
+ let serialized_input: String = val.funcall("serialized_input", ())?;
51
+ let action_id: String = val.funcall("action_id", ())?;
52
+
53
+ if connection_val.is_nil() {
54
+ return Err(Error::new(magnus::exception::runtime_error(), "Connection is required"));
55
+ }
56
+
57
+ let wrapped_connection: RConnection = match TryConvert::try_convert(connection_val) {
58
+ Ok(conn) => conn,
59
+ Err(_) => return Err(Error::new(magnus::exception::runtime_error(), "Connection is required")),
60
+ };
61
+
62
+ let inner = ActionContext {
63
+ action_id: action_id,
64
+ connection: wrapped_connection.clone().inner,
65
+ serialized_input,
66
+ };
67
+
68
+ Ok(Self {
69
+ inner,
70
+ wrapped_connection: Some(wrapped_connection),
71
+ })
72
+ }
73
+ }
74
+
75
+ impl From<RActionContext> for ActionContext {
76
+ fn from(raction_context: RActionContext) -> Self {
77
+ raction_context.inner
78
+ }
79
+ }
@@ -0,0 +1,32 @@
1
+ use crate::component::standout::app::types::ActionResponse;
2
+
3
+ #[magnus::wrap(class = "AppBridge::ActionResponse")]
4
+ pub struct RActionResponse {
5
+ inner: ActionResponse,
6
+ }
7
+
8
+ impl RActionResponse {
9
+ pub fn new(serialized_output: String) -> Self {
10
+ let inner = ActionResponse {
11
+ serialized_output: serialized_output,
12
+ };
13
+ Self { inner }
14
+ }
15
+
16
+ pub fn serialized_output(&self) -> String {
17
+ self.inner.serialized_output.clone()
18
+ }
19
+ }
20
+
21
+ impl From<ActionResponse> for RActionResponse {
22
+ fn from(value: ActionResponse) -> Self {
23
+ Self { inner: value }
24
+ }
25
+ }
26
+
27
+ impl From<RActionResponse> for ActionResponse {
28
+ fn from(value: RActionResponse) -> Self {
29
+ value.inner
30
+ }
31
+ }
32
+