app_bridge 0.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/Cargo.toml ADDED
@@ -0,0 +1,8 @@
1
+ # This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
2
+ # a Rust project. Your extensions dependencies should be added to the Cargo.toml
3
+ # in the ext/ directory.
4
+
5
+ [workspace]
6
+ members = ["./ext/app_bridge"]
7
+ resolver = "2"
8
+ exclude = ["./spec/fixtures/components/example"]
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Standout App Bridge
2
+
3
+ `app_bridge` is a Ruby gem designed to facilitate communication with WebAssembly components that implement the WIT specification `standout:app`. This gem is developed for use in Standout's products.
4
+
5
+ ## Installation
6
+
7
+ Add the following line to your `Gemfile`:
8
+
9
+ ```ruby
10
+ gem 'app_bridge', github: 'standout/app_bridge'
11
+ ```
12
+
13
+ Then, install the gem by running:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ To use this gem, you need a WebAssembly component that adheres to the specification defined in `ext/app_bridge/wit/world.wit`.
22
+
23
+ You can check out the example components in `spec/fixtures/components` to see how such a component should be structured.
24
+
25
+ Once you have a WebAssembly component, you can use the gem as follows:
26
+
27
+ ```ruby
28
+ require 'app_bridge'
29
+
30
+ app = AppBridge::App.new('path/to/your/component.wasm')
31
+ app.triggers # => ['trigger1', 'trigger2']
32
+ ```
33
+
34
+ More documentation and features will be added as the gem evolves.
35
+
36
+ ## Development
37
+
38
+ To contribute or modify this gem, ensure you have the following dependencies installed:
39
+
40
+ - **Ruby 3.3.0** (or later)
41
+ - **Rust 1.84.0** (or later)
42
+
43
+ ### Setting Up the Development Environment
44
+
45
+ Run the following command to setup and install additional dependencies:
46
+
47
+ ```bash
48
+ bin/setup
49
+ ```
50
+
51
+ Then, to compile example applications, run tests, and perform syntax checks, execute:
52
+
53
+ ```bash
54
+ rake
55
+ ```
56
+
57
+ ### Useful Commands
58
+
59
+ - **Interactive Console:** Run `bin/console` to interactively test the code.
60
+ - **Full Test Suite & Linting:** Run `rake` to compile, execute tests, and perform syntax checks.
61
+ - **Run Tests Only:** Execute `rake spec` to run only the test suite.
62
+ - **Linting:** Run `rake rubocop` to check code style and formatting.
63
+ - **Compile Example Applications:** Use `rake fixtures` to build the example apps.
64
+
65
+ To install this gem locally for testing purposes, run:
66
+
67
+ ```bash
68
+ bundle exec rake install
69
+ ```
70
+
71
+ ## Release & Distribution
72
+
73
+ This gem is **not** published to RubyGems. It is intended for internal use within Standout's products only.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ require "rb_sys/extensiontask"
13
+
14
+ task build: :compile
15
+
16
+ GEMSPEC = Gem::Specification.load("app_bridge.gemspec")
17
+
18
+ RbSys::ExtensionTask.new("app_bridge", GEMSPEC) do |ext|
19
+ ext.lib_dir = "lib/app_bridge"
20
+ end
21
+
22
+ # Load all project specific rake tasks
23
+ Dir.glob(File.expand_path("tasks/**/*.rake", __dir__)).each { |file| load file }
24
+
25
+ task default: %i[fixtures compile spec rubocop]
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "app_bridge"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ authors = ["Alexander Ross <ross@standout.se>"]
6
+ publish = false
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ magnus = { version = "0.7.1" }
13
+ anyhow = "1.0.95"
14
+ wasmtime = "29.0.1"
15
+ wasmtime-wasi = "29.0.1"
16
+ wasmtime-cli = "29.0.1"
17
+ wasmtime-component-util = "29.0.1"
18
+ reqwest = { version = "0.12", features = ["blocking", "json"] }
19
+ tokio = { version = "1", features = ["full"] }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("app_bridge/app_bridge")
@@ -0,0 +1,46 @@
1
+ use crate::component::standout;
2
+ use crate::component::standout::app::http::Request;
3
+ use reqwest::blocking::Client;
4
+ use std::collections::HashMap;
5
+ use std::sync::{Arc, Mutex};
6
+ use wasmtime::component::ResourceTable;
7
+ use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};
8
+
9
+ pub struct AppState {
10
+ ctx: WasiCtx,
11
+ table: ResourceTable,
12
+ pub client: Arc<Mutex<Client>>,
13
+ pub request_list: HashMap<u32, Request>,
14
+ pub next_request_id: u32,
15
+ }
16
+
17
+ impl AppState {
18
+ pub fn new(ctx: WasiCtx) -> Self {
19
+ Self {
20
+ ctx,
21
+ table: ResourceTable::new(),
22
+ client: Arc::new(Mutex::new(Client::new())),
23
+ request_list: HashMap::new(),
24
+ next_request_id: 0,
25
+ }
26
+ }
27
+ }
28
+
29
+ impl standout::app::http::Host for AppState {
30
+ // Impl http host methods here
31
+ }
32
+
33
+ impl WasiView for AppState {
34
+ fn ctx(&mut self) -> &mut WasiCtx {
35
+ &mut self.ctx
36
+ }
37
+ fn table(&mut self) -> &mut ResourceTable {
38
+ &mut self.table
39
+ }
40
+ }
41
+
42
+ impl Default for AppState {
43
+ fn default() -> Self {
44
+ Self::new(WasiCtxBuilder::new().build())
45
+ }
46
+ }
@@ -0,0 +1,55 @@
1
+ use std::result::Result::Ok;
2
+ use wasmtime::component::{bindgen, Component, Linker};
3
+ use wasmtime::{Engine, Result, Store};
4
+ use wasmtime_wasi::WasiCtxBuilder;
5
+
6
+ bindgen!();
7
+
8
+ use crate::app_state::AppState;
9
+
10
+ pub fn build_engine() -> Engine {
11
+ Engine::default()
12
+ }
13
+
14
+ pub fn build_linker(engine: &Engine) -> Result<Linker<AppState>> {
15
+ let mut linker = Linker::<AppState>::new(&engine);
16
+ wasmtime_wasi::add_to_linker_sync(&mut linker)?;
17
+ standout::app::http::add_to_linker(&mut linker, |s| s)?;
18
+
19
+ Ok(linker)
20
+ }
21
+
22
+ pub fn build_store(engine: &Engine) -> Store<AppState> {
23
+ let builder = WasiCtxBuilder::new().build();
24
+
25
+ // ... configure `builder` more to add env vars, args, etc ...
26
+
27
+ let store = Store::new(&engine, AppState::new(builder));
28
+
29
+ store
30
+ }
31
+
32
+ pub fn app(
33
+ file_path: String,
34
+ engine: Engine,
35
+ store: &mut Store<AppState>,
36
+ linker: Linker<AppState>,
37
+ ) -> Result<Bridge> {
38
+ // Load the application component from the file system.
39
+ let component = Component::from_file(&engine, file_path)?;
40
+ let instance = Bridge::instantiate(store, &component, &linker)?;
41
+
42
+ Ok(instance)
43
+ }
44
+
45
+ impl Default for Bridge {
46
+ fn default() -> Self {
47
+ let file_path = "spec/fixtures/components/example_app.wasm";
48
+ let engine = Engine::default();
49
+ let mut store = build_store(&engine);
50
+ let linker = build_linker(&engine).unwrap();
51
+ let instant = app(file_path.to_string(), engine, &mut store, linker);
52
+
53
+ instant.unwrap()
54
+ }
55
+ }
@@ -0,0 +1,307 @@
1
+ use magnus::{function, method, prelude::*, Error, RArray, Ruby, TryConvert, Value};
2
+ use std::cell::RefCell;
3
+ use wasmtime::Store;
4
+ mod app_state;
5
+ mod component;
6
+ mod request_builder;
7
+
8
+ use app_state::AppState;
9
+ use component::standout::app::types::{Account, TriggerContext, TriggerEvent, TriggerResponse};
10
+ use component::{app, build_engine, build_linker, build_store, Bridge};
11
+
12
+ #[derive(Default)]
13
+ pub struct RApp {
14
+ component_path: String,
15
+ instance: RefCell<Option<Bridge>>,
16
+ store: RefCell<Option<Store<AppState>>>,
17
+ }
18
+
19
+ #[derive(Default)]
20
+ #[magnus::wrap(class = "AppBridge::App")]
21
+ struct MutRApp(RefCell<RApp>);
22
+
23
+ impl MutRApp {
24
+ fn initialize(&self, component_path: String) {
25
+ let mut this = self.0.borrow_mut();
26
+ let engine = build_engine();
27
+ let linker = build_linker(&engine).unwrap();
28
+ let mut store = build_store(&engine);
29
+
30
+ let app = app(component_path.clone(), engine, &mut store, linker).unwrap();
31
+
32
+ this.component_path = component_path.to_string();
33
+ *this.instance.borrow_mut() = Some(app);
34
+ *this.store.borrow_mut() = Some(store);
35
+ }
36
+
37
+ fn triggers(&self) -> Vec<String> {
38
+ let binding = self.0.borrow();
39
+
40
+ let mut instance = binding.instance.borrow_mut();
41
+ let mut store = binding.store.borrow_mut();
42
+
43
+ if let (Some(instance), Some(store)) = (&mut *instance, &mut *store) {
44
+ instance
45
+ .standout_app_triggers()
46
+ .call_get_triggers(store)
47
+ .unwrap()
48
+ } else {
49
+ vec![]
50
+ }
51
+ }
52
+
53
+ fn rb_fetch_events(&self, context: Value) -> RTriggerResponse {
54
+ let context: RTriggerContext = TryConvert::try_convert(context).unwrap();
55
+ let response = self.fetch_events(context.inner);
56
+
57
+ RTriggerResponse::from_trigger_response(response)
58
+ }
59
+
60
+ fn fetch_events(&self, context: TriggerContext) -> TriggerResponse {
61
+ let binding = self.0.borrow();
62
+
63
+ let mut instance = binding.instance.borrow_mut();
64
+ let mut store = binding.store.borrow_mut();
65
+
66
+ if let (Some(instance), Some(store)) = (&mut *instance, &mut *store) {
67
+ instance
68
+ .standout_app_triggers()
69
+ .call_fetch_events(store, &context)
70
+ .unwrap()
71
+ } else {
72
+ TriggerResponse {
73
+ store: context.store,
74
+ events: vec![],
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ #[magnus::wrap(class = "AppBridge::Account")]
81
+ struct RAccount {
82
+ inner: Account,
83
+ }
84
+
85
+ impl RAccount {
86
+ fn new(id: String, name: String, serialized_data: String) -> Self {
87
+ let inner = Account {
88
+ id: id,
89
+ name: name,
90
+ serialized_data: serialized_data,
91
+ };
92
+ Self { inner }
93
+ }
94
+
95
+ fn id(&self) -> String {
96
+ self.inner.id.clone()
97
+ }
98
+
99
+ fn name(&self) -> String {
100
+ self.inner.name.clone()
101
+ }
102
+
103
+ fn serialized_data(&self) -> String {
104
+ self.inner.serialized_data.clone()
105
+ }
106
+ }
107
+
108
+ impl Clone for RAccount {
109
+ fn clone(&self) -> Self {
110
+ Self {
111
+ inner: self.inner.clone(),
112
+ }
113
+ }
114
+ }
115
+
116
+ impl TryConvert for RAccount {
117
+ fn try_convert(val: Value) -> Result<Self, Error> {
118
+ let id: String = val.funcall("id", ())?;
119
+ let name: String = val.funcall("name", ())?;
120
+ let serialized_data: String = val.funcall("serialized_data", ())?;
121
+
122
+ let inner = Account {
123
+ id,
124
+ name,
125
+ serialized_data,
126
+ };
127
+
128
+ Ok(Self { inner })
129
+ }
130
+ }
131
+
132
+ #[magnus::wrap(class = "AppBridge::TriggerContext")]
133
+ struct RTriggerContext {
134
+ inner: TriggerContext,
135
+ wrapped_account: RAccount,
136
+ }
137
+
138
+ impl RTriggerContext {
139
+ fn new(trigger_id: String, account: Value, store: String) -> Self {
140
+ let account: RAccount = TryConvert::try_convert(account).unwrap();
141
+
142
+ let inner = TriggerContext {
143
+ trigger_id: trigger_id,
144
+ account: account.clone().inner,
145
+ store: store,
146
+ };
147
+ Self {
148
+ inner,
149
+ wrapped_account: account.clone(),
150
+ }
151
+ }
152
+
153
+ fn trigger_id(&self) -> String {
154
+ self.inner.trigger_id.clone()
155
+ }
156
+
157
+ fn account(&self) -> RAccount {
158
+ self.wrapped_account.clone()
159
+ }
160
+
161
+ fn store(&self) -> String {
162
+ self.inner.store.clone()
163
+ }
164
+ }
165
+
166
+ impl TryConvert for RTriggerContext {
167
+ fn try_convert(val: Value) -> Result<Self, Error> {
168
+ let account_val: Value = val.funcall("account", ())?;
169
+ let store: String = val.funcall("store", ())?;
170
+ let trigger_id: String = val.funcall("trigger_id", ())?;
171
+
172
+ let account: RAccount = TryConvert::try_convert(account_val).unwrap();
173
+
174
+ let inner = TriggerContext {
175
+ trigger_id: trigger_id,
176
+ account: account.clone().inner,
177
+ store: store,
178
+ };
179
+
180
+ Ok(Self {
181
+ inner,
182
+ wrapped_account: account.clone(),
183
+ })
184
+ }
185
+ }
186
+
187
+ #[magnus::wrap(class = "AppBridge::TriggerResponse")]
188
+ struct RTriggerResponse {
189
+ inner: TriggerResponse,
190
+ }
191
+
192
+ impl RTriggerResponse {
193
+ fn new(store: String, events: RArray) -> Self {
194
+ let iter = events.into_iter();
195
+ let res: Vec<RTriggerEvent> = iter
196
+ .map(&TryConvert::try_convert)
197
+ .collect::<Result<Vec<RTriggerEvent>, Error>>()
198
+ .unwrap();
199
+
200
+ let inner = TriggerResponse {
201
+ store: store,
202
+ events: res.iter().map(|e| e.inner.clone()).collect(),
203
+ };
204
+ Self { inner }
205
+ }
206
+
207
+ fn from_trigger_response(inner: TriggerResponse) -> Self {
208
+ Self { inner }
209
+ }
210
+
211
+ fn store(&self) -> String {
212
+ self.inner.store.clone()
213
+ }
214
+
215
+ fn events(&self) -> RArray {
216
+ self.inner
217
+ .events
218
+ .iter()
219
+ .map(|e| RTriggerEvent::new(e.id.clone(), e.timestamp, e.serialized_data.clone()))
220
+ .collect()
221
+ }
222
+ }
223
+
224
+ #[magnus::wrap(class = "AppBridge::TriggerEvent")]
225
+ struct RTriggerEvent {
226
+ inner: TriggerEvent,
227
+ }
228
+
229
+ impl RTriggerEvent {
230
+ fn new(id: String, timestamp: u64, serialized_data: String) -> Self {
231
+ let inner = TriggerEvent {
232
+ id: id,
233
+ timestamp: timestamp,
234
+ serialized_data: serialized_data,
235
+ };
236
+ Self { inner }
237
+ }
238
+
239
+ fn id(&self) -> String {
240
+ self.inner.id.clone()
241
+ }
242
+
243
+ fn timestamp(&self) -> u64 {
244
+ self.inner.timestamp
245
+ }
246
+
247
+ fn serialized_data(&self) -> String {
248
+ self.inner.serialized_data.clone()
249
+ }
250
+ }
251
+
252
+ impl TryConvert for RTriggerEvent {
253
+ fn try_convert(val: Value) -> Result<Self, Error> {
254
+ let id: String = val.funcall("id", ())?;
255
+ let timestamp: u64 = val.funcall("timestamp", ())?;
256
+ let serialized_data: String = val.funcall("serialized_data", ())?;
257
+
258
+ let inner = TriggerEvent {
259
+ id,
260
+ timestamp,
261
+ serialized_data,
262
+ };
263
+
264
+ Ok(Self { inner })
265
+ }
266
+ }
267
+
268
+ #[magnus::init]
269
+ fn init(ruby: &Ruby) -> Result<(), Error> {
270
+ let module = ruby.define_module("AppBridge")?;
271
+
272
+ // Define the Accout class
273
+ let account_class = module.define_class("Account", ruby.class_object())?;
274
+ account_class.define_singleton_method("new", function!(RAccount::new, 3))?;
275
+ account_class.define_method("id", method!(RAccount::id, 0))?;
276
+ account_class.define_method("name", method!(RAccount::name, 0))?;
277
+ account_class.define_method("serialized_data", method!(RAccount::serialized_data, 0))?;
278
+
279
+ let trigger_event_class = module.define_class("TriggerEvent", ruby.class_object())?;
280
+ trigger_event_class.define_singleton_method("new", function!(RTriggerEvent::new, 3))?;
281
+ trigger_event_class.define_method("id", method!(RTriggerEvent::id, 0))?;
282
+ trigger_event_class.define_method("timestamp", method!(RTriggerEvent::timestamp, 0))?;
283
+ trigger_event_class.define_method(
284
+ "serialized_data",
285
+ method!(RTriggerEvent::serialized_data, 0),
286
+ )?;
287
+
288
+ let trigger_response_class = module.define_class("TriggerResponse", ruby.class_object())?;
289
+ trigger_response_class.define_singleton_method("new", function!(RTriggerResponse::new, 2))?;
290
+ trigger_response_class.define_method("store", method!(RTriggerResponse::store, 0))?;
291
+ trigger_response_class.define_method("events", method!(RTriggerResponse::events, 0))?;
292
+
293
+ let trigger_context_class = module.define_class("TriggerContext", ruby.class_object())?;
294
+ trigger_context_class.define_singleton_method("new", function!(RTriggerContext::new, 3))?;
295
+ trigger_context_class.define_method("trigger_id", method!(RTriggerContext::trigger_id, 0))?;
296
+ trigger_context_class.define_method("account", method!(RTriggerContext::account, 0))?;
297
+ trigger_context_class.define_method("store", method!(RTriggerContext::store, 0))?;
298
+
299
+ // Define the App class
300
+ let app_class = module.define_class("App", ruby.class_object())?;
301
+ app_class.define_alloc_func::<MutRApp>();
302
+ app_class.define_method("initialize", method!(MutRApp::initialize, 1))?;
303
+ app_class.define_method("triggers", method!(MutRApp::triggers, 0))?;
304
+ app_class.define_method("fetch_events", method!(MutRApp::rb_fetch_events, 1))?;
305
+
306
+ Ok(())
307
+ }