app_bridge 2.0.1 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.editorconfig +18 -0
- data/Cargo.lock +919 -210
- data/Rakefile +6 -3
- data/ext/app_bridge/Cargo.toml +8 -3
- data/ext/app_bridge/src/app_state.rs +21 -6
- data/ext/app_bridge/src/component.rs +34 -9
- data/ext/app_bridge/src/lib.rs +30 -10
- data/ext/app_bridge/src/request_builder.rs +39 -1
- data/ext/app_bridge/src/wrappers/action_context.rs +79 -0
- data/ext/app_bridge/src/wrappers/action_response.rs +32 -0
- data/ext/app_bridge/src/wrappers/app.rs +257 -14
- data/ext/app_bridge/src/wrappers/{account.rs → connection.rs} +12 -12
- data/ext/app_bridge/src/wrappers/mod.rs +3 -1
- data/ext/app_bridge/src/wrappers/trigger_context.rs +35 -14
- data/ext/app_bridge/wit/world.wit +88 -7
- data/lib/app_bridge/app.rb +31 -3
- data/lib/app_bridge/version.rb +4 -1
- data/lib/app_bridge.rb +8 -6
- data/sig/app_bridge.rbs +26 -4
- data/tasks/rust_test.rake +11 -0
- metadata +7 -3
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
|
-
|
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]
|
data/ext/app_bridge/Cargo.toml
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
[package]
|
2
2
|
name = "app_bridge"
|
3
|
-
version
|
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.1"
|
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 = "
|
14
|
-
wasmtime-wasi = "
|
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
|
34
|
-
fn
|
35
|
-
|
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()
|
27
|
+
pub fn build_store(engine: &Engine, env_vars: Option<HashMap<String, String>>) -> Store<AppState> {
|
28
|
+
let mut builder = WasiCtxBuilder::new();
|
24
29
|
|
25
|
-
//
|
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
|
-
|
39
|
+
// Create AppState with or without environment variables
|
40
|
+
let app_state = AppState::new(ctx, env_vars);
|
28
41
|
|
29
|
-
|
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
|
-
|
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
|
|
data/ext/app_bridge/src/lib.rs
CHANGED
@@ -5,10 +5,12 @@ mod request_builder;
|
|
5
5
|
mod error_mapping;
|
6
6
|
|
7
7
|
mod wrappers;
|
8
|
-
use wrappers::
|
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
|
31
|
-
let
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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,
|
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("
|
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
|
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
|
+
|