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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +15 -0
- data/CHANGELOG.md +5 -0
- data/Cargo.lock +3954 -0
- data/Cargo.toml +8 -0
- data/README.md +73 -0
- data/Rakefile +25 -0
- data/ext/app_bridge/Cargo.toml +19 -0
- data/ext/app_bridge/extconf.rb +6 -0
- data/ext/app_bridge/src/app_state.rs +46 -0
- data/ext/app_bridge/src/component.rs +55 -0
- data/ext/app_bridge/src/lib.rs +307 -0
- data/ext/app_bridge/src/request_builder.rs +196 -0
- data/ext/app_bridge/wit/world.wit +157 -0
- data/lib/app_bridge/version.rb +5 -0
- data/lib/app_bridge.rb +16 -0
- data/sig/app_bridge.rbs +10 -0
- data/tasks/fixtures.rake +37 -0
- metadata +82 -0
@@ -0,0 +1,196 @@
|
|
1
|
+
use crate::app_state::AppState;
|
2
|
+
use crate::component::standout::app::http::{
|
3
|
+
HostRequestBuilder, Method, Request, RequestBuilder, RequestError, Response,
|
4
|
+
};
|
5
|
+
use reqwest::Method as ReqwestMethod;
|
6
|
+
use std::result::Result::Ok;
|
7
|
+
use wasmtime::component::Resource;
|
8
|
+
|
9
|
+
impl HostRequestBuilder for AppState {
|
10
|
+
fn new(&mut self) -> Resource<RequestBuilder> {
|
11
|
+
let id = self.next_request_id;
|
12
|
+
self.next_request_id += 1;
|
13
|
+
self.request_list.insert(id, Request::default());
|
14
|
+
Resource::new_own(id)
|
15
|
+
}
|
16
|
+
|
17
|
+
fn method(
|
18
|
+
&mut self,
|
19
|
+
self_: Resource<RequestBuilder>,
|
20
|
+
method: Method,
|
21
|
+
) -> Resource<RequestBuilder> {
|
22
|
+
let id = self_.rep();
|
23
|
+
let mut request = self.request_list.get(&id).cloned().unwrap();
|
24
|
+
request.method = method;
|
25
|
+
let new_id = self.next_request_id;
|
26
|
+
self.next_request_id += 1;
|
27
|
+
self.request_list.insert(new_id, request);
|
28
|
+
Resource::new_own(new_id)
|
29
|
+
}
|
30
|
+
|
31
|
+
fn url(
|
32
|
+
&mut self,
|
33
|
+
self_: Resource<RequestBuilder>,
|
34
|
+
url: wasmtime::component::__internal::String,
|
35
|
+
) -> Resource<RequestBuilder> {
|
36
|
+
let id = self_.rep();
|
37
|
+
let mut request = self.request_list.get(&id).cloned().unwrap();
|
38
|
+
request.url = url;
|
39
|
+
let new_id = self.next_request_id;
|
40
|
+
self.next_request_id += 1;
|
41
|
+
self.request_list.insert(new_id, request);
|
42
|
+
Resource::new_own(new_id)
|
43
|
+
}
|
44
|
+
|
45
|
+
#[doc = " Add a header to the request"]
|
46
|
+
fn header(
|
47
|
+
&mut self,
|
48
|
+
self_: Resource<RequestBuilder>,
|
49
|
+
key: wasmtime::component::__internal::String,
|
50
|
+
value: wasmtime::component::__internal::String,
|
51
|
+
) -> Resource<RequestBuilder> {
|
52
|
+
let id = self_.rep();
|
53
|
+
let mut request = self.request_list.get(&id).cloned().unwrap_or_default();
|
54
|
+
request.headers.push((key, value));
|
55
|
+
let new_id = self.next_request_id;
|
56
|
+
self.next_request_id += 1;
|
57
|
+
self.request_list.insert(new_id, request);
|
58
|
+
Resource::new_own(new_id)
|
59
|
+
}
|
60
|
+
|
61
|
+
fn headers(
|
62
|
+
&mut self,
|
63
|
+
self_: Resource<RequestBuilder>,
|
64
|
+
headers: wasmtime::component::__internal::Vec<(
|
65
|
+
wasmtime::component::__internal::String,
|
66
|
+
wasmtime::component::__internal::String,
|
67
|
+
)>,
|
68
|
+
) -> Resource<RequestBuilder> {
|
69
|
+
let id = self_.rep();
|
70
|
+
let mut request = self.request_list.get(&id).cloned().unwrap_or_default();
|
71
|
+
for (key, value) in headers {
|
72
|
+
request.headers.push((key, value));
|
73
|
+
}
|
74
|
+
let new_id = self.next_request_id;
|
75
|
+
self.next_request_id += 1;
|
76
|
+
self.request_list.insert(new_id, request);
|
77
|
+
Resource::new_own(new_id)
|
78
|
+
}
|
79
|
+
|
80
|
+
#[doc = " Add a body to the request"]
|
81
|
+
fn body(
|
82
|
+
&mut self,
|
83
|
+
self_: Resource<RequestBuilder>,
|
84
|
+
body: wasmtime::component::__internal::String,
|
85
|
+
) -> Resource<RequestBuilder> {
|
86
|
+
let id = self_.rep();
|
87
|
+
let mut request = self.request_list.get(&id).cloned().unwrap_or_default();
|
88
|
+
request.body = body;
|
89
|
+
let new_id = self.next_request_id;
|
90
|
+
self.next_request_id += 1;
|
91
|
+
self.request_list.insert(new_id, request);
|
92
|
+
Resource::new_own(new_id)
|
93
|
+
}
|
94
|
+
|
95
|
+
#[doc = " Send the request"]
|
96
|
+
fn send(&mut self, self_: Resource<RequestBuilder>) -> Result<Response, RequestError> {
|
97
|
+
let id = self_.rep();
|
98
|
+
let request = match self.request_list.get(&id).cloned() {
|
99
|
+
Some(request) => request,
|
100
|
+
None => return Err(RequestError::Other("Request not found".to_string())),
|
101
|
+
};
|
102
|
+
let client = self.client.lock().unwrap();
|
103
|
+
let mut request_builder = client.request(request.method.into(), &request.url);
|
104
|
+
for (key, value) in request.headers {
|
105
|
+
request_builder = request_builder.header(key, value);
|
106
|
+
}
|
107
|
+
request_builder = request_builder.body(request.body);
|
108
|
+
|
109
|
+
let response = request_builder.send();
|
110
|
+
|
111
|
+
match response {
|
112
|
+
Ok(resp) => {
|
113
|
+
let mut response = Response::default();
|
114
|
+
response.status = resp.status().as_u16();
|
115
|
+
for (key, value) in resp.headers() {
|
116
|
+
response.headers.push((
|
117
|
+
key.as_str().to_string(),
|
118
|
+
value.to_str().unwrap_or_default().to_string(),
|
119
|
+
));
|
120
|
+
}
|
121
|
+
response.body = resp.text().unwrap_or_default();
|
122
|
+
Ok(response)
|
123
|
+
}
|
124
|
+
Err(error) => {
|
125
|
+
let error_message = format!(
|
126
|
+
"Request failed to {} {}: {}",
|
127
|
+
request.method,
|
128
|
+
request.url,
|
129
|
+
error
|
130
|
+
);
|
131
|
+
let error = RequestError::Other(error_message);
|
132
|
+
Err(error)
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
137
|
+
fn drop(&mut self, rep: Resource<RequestBuilder>) -> wasmtime::Result<()> {
|
138
|
+
let id = rep.rep();
|
139
|
+
self.request_list.remove(&id);
|
140
|
+
Ok(())
|
141
|
+
}
|
142
|
+
|
143
|
+
fn object(&mut self, self_: Resource<RequestBuilder>) -> Request {
|
144
|
+
let id = self_.rep();
|
145
|
+
self.request_list.get(&id).cloned().unwrap_or_default()
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
impl From<Method> for ReqwestMethod {
|
150
|
+
fn from(method: Method) -> Self {
|
151
|
+
match method {
|
152
|
+
Method::Get => ReqwestMethod::GET,
|
153
|
+
Method::Post => ReqwestMethod::POST,
|
154
|
+
Method::Put => ReqwestMethod::PUT,
|
155
|
+
Method::Delete => ReqwestMethod::DELETE,
|
156
|
+
Method::Patch => ReqwestMethod::PATCH,
|
157
|
+
Method::Head => ReqwestMethod::HEAD,
|
158
|
+
Method::Options => ReqwestMethod::OPTIONS,
|
159
|
+
}
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
impl Default for Request {
|
164
|
+
fn default() -> Self {
|
165
|
+
Self {
|
166
|
+
url: "".to_string(),
|
167
|
+
method: Method::Get,
|
168
|
+
body: "".to_string(),
|
169
|
+
headers: Vec::new(),
|
170
|
+
}
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
impl Default for Response {
|
175
|
+
fn default() -> Self {
|
176
|
+
Self {
|
177
|
+
status: 0,
|
178
|
+
headers: Vec::new(),
|
179
|
+
body: "".to_string(),
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
impl std::fmt::Display for Method {
|
185
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
186
|
+
match self {
|
187
|
+
Method::Get => write!(f, "GET"),
|
188
|
+
Method::Post => write!(f, "POST"),
|
189
|
+
Method::Put => write!(f, "PUT"),
|
190
|
+
Method::Delete => write!(f, "DELETE"),
|
191
|
+
Method::Patch => write!(f, "PATCH"),
|
192
|
+
Method::Head => write!(f, "HEAD"),
|
193
|
+
Method::Options => write!(f, "OPTIONS"),
|
194
|
+
}
|
195
|
+
}
|
196
|
+
}
|
@@ -0,0 +1,157 @@
|
|
1
|
+
package standout:app@0.3.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 account {
|
14
|
+
id: string,
|
15
|
+
name: string,
|
16
|
+
// The account 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 account that the trigger is invoked for.
|
27
|
+
account: account,
|
28
|
+
|
29
|
+
// The store will contain the data that was stored in the trigger store the
|
30
|
+
// last time the trigger was invoked.
|
31
|
+
store: trigger-store,
|
32
|
+
}
|
33
|
+
|
34
|
+
record trigger-response {
|
35
|
+
// The trigger events, each event will be used to spawn a new workflow
|
36
|
+
// execution in Standouts integration plattform.
|
37
|
+
events: list<trigger-event>,
|
38
|
+
|
39
|
+
// The updated store will be stored and used the next time the trigger is
|
40
|
+
// invoked.
|
41
|
+
store: trigger-store,
|
42
|
+
}
|
43
|
+
|
44
|
+
record trigger-event {
|
45
|
+
// The ID of the trigger event
|
46
|
+
//
|
47
|
+
// If the account used for the given instance of the trigger is the same,
|
48
|
+
// as seen before. Then the event will be ignored.
|
49
|
+
//
|
50
|
+
// A scheduler could therefore use an timestamp as the ID, to ensure that
|
51
|
+
// the event is only triggered once per given time.
|
52
|
+
//
|
53
|
+
// A trigger that acts on created orders in a e-commerce system could use
|
54
|
+
// the order ID as the ID, to ensure that the event is only triggered once
|
55
|
+
// per order.
|
56
|
+
//
|
57
|
+
// A trigger that acts on updated orders in a e-commerce system could use
|
58
|
+
// the order ID in combination with an updated at timestamp as the ID, to
|
59
|
+
// ensure that the event is only triggered once per order update.
|
60
|
+
id: string,
|
61
|
+
|
62
|
+
// The timestamp of the event.
|
63
|
+
// Must be a unix timestamp in milliseconds since epoch (UTC).
|
64
|
+
// In JavaScript `Date.now()` can be used to get the current timestamp in
|
65
|
+
// milliseconds.
|
66
|
+
timestamp: u64,
|
67
|
+
|
68
|
+
// Serialized data must be a JSON object serialized into a string
|
69
|
+
// Note that it is important that the root is a object, not an array,
|
70
|
+
// or another primitive type.
|
71
|
+
serialized-data: string,
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
|
76
|
+
interface triggers {
|
77
|
+
use types.{trigger-context, trigger-event, trigger-response};
|
78
|
+
|
79
|
+
get-triggers: func() -> list<string>;
|
80
|
+
|
81
|
+
// Fetch events
|
82
|
+
//
|
83
|
+
// There are some limitations to the function:
|
84
|
+
// - It must return within 30 seconds
|
85
|
+
// - It must return less than or equal to 100 events
|
86
|
+
//
|
87
|
+
// If you need to fetch more events, you can return up to 100 events and then
|
88
|
+
// store the data needed for you to remember where you left off in the store.
|
89
|
+
// The next time the trigger is invoked, you can use the store to continue
|
90
|
+
// where you left off.
|
91
|
+
//
|
92
|
+
// If you do not pass the limitations the return value will be ignored. We
|
93
|
+
// will not handle any events and we persist the store that was returned in
|
94
|
+
// the response.
|
95
|
+
//
|
96
|
+
// That also means that you should implement your fetch event function in a
|
97
|
+
// way that it can be called multiple times using the same context and return
|
98
|
+
// the same events. That will ensure that the user that is building an
|
99
|
+
// integration with your trigger will not miss any events if your system is
|
100
|
+
// down for a short period of time.
|
101
|
+
fetch-events: func(context: trigger-context) -> trigger-response;
|
102
|
+
}
|
103
|
+
|
104
|
+
interface http {
|
105
|
+
record response {
|
106
|
+
status: u16,
|
107
|
+
headers: headers,
|
108
|
+
body: string,
|
109
|
+
}
|
110
|
+
|
111
|
+
record request {
|
112
|
+
method: method,
|
113
|
+
url: string,
|
114
|
+
headers: headers,
|
115
|
+
body: string,
|
116
|
+
}
|
117
|
+
|
118
|
+
variant request-error {
|
119
|
+
other(string)
|
120
|
+
}
|
121
|
+
|
122
|
+
type headers = list<tuple<string, string>>;
|
123
|
+
|
124
|
+
resource request-builder {
|
125
|
+
constructor();
|
126
|
+
|
127
|
+
method: func(method: method) -> request-builder;
|
128
|
+
url: func(url: string) -> request-builder;
|
129
|
+
|
130
|
+
// Add a header to the request
|
131
|
+
header: func(key: string, value: string) -> request-builder;
|
132
|
+
headers: func(headers: list<tuple<string, string>>) -> request-builder;
|
133
|
+
|
134
|
+
// Add a body to the request
|
135
|
+
body: func(body: string) -> request-builder;
|
136
|
+
|
137
|
+
object: func() -> request;
|
138
|
+
|
139
|
+
// Send the request
|
140
|
+
send: func() -> result<response, request-error>;
|
141
|
+
}
|
142
|
+
|
143
|
+
variant method {
|
144
|
+
get,
|
145
|
+
post,
|
146
|
+
put,
|
147
|
+
delete,
|
148
|
+
patch,
|
149
|
+
options,
|
150
|
+
head,
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
world bridge {
|
155
|
+
import http;
|
156
|
+
export triggers;
|
157
|
+
}
|
data/lib/app_bridge.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "app_bridge/version"
|
4
|
+
require_relative "app_bridge/app_bridge"
|
5
|
+
|
6
|
+
module AppBridge
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
# Represents a trigger event that is recieved from the app.
|
10
|
+
class TriggerEvent
|
11
|
+
def inspect
|
12
|
+
"#<AppBridge::TriggerEvent(id: #{id.inspect}, timestamp: #{timestamp.inspect}, " \
|
13
|
+
"serialized_data: #{serialized_data.inspect})>"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/sig/app_bridge.rbs
ADDED
data/tasks/fixtures.rake
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
|
5
|
+
namespace :fixtures do
|
6
|
+
namespace :apps do
|
7
|
+
desc "Clean up build artifacts"
|
8
|
+
task :clean do
|
9
|
+
# In context of the path spec/fixtures/components/example.
|
10
|
+
# Execute cargo clean.
|
11
|
+
#
|
12
|
+
pwd = "spec/fixtures/components/example"
|
13
|
+
pid = Process.spawn("cargo clean", chdir: pwd)
|
14
|
+
Process.wait(pid)
|
15
|
+
raise "Failed to clean build artifacts" unless $CHILD_STATUS.success?
|
16
|
+
|
17
|
+
# Remove the built wasm artifact.
|
18
|
+
pid = Process.spawn("rm example.wasm", chdir: "spec/fixtures/components")
|
19
|
+
Process.wait(pid)
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Compile the fixture apps"
|
23
|
+
task :compile do
|
24
|
+
pwd = "spec/fixtures/components/example"
|
25
|
+
compile_pid = Process.spawn("cargo clean && cargo build --release --target wasm32-wasip2",
|
26
|
+
chdir: pwd)
|
27
|
+
Process.wait(compile_pid)
|
28
|
+
raise "Failed to build artifacts" unless $CHILD_STATUS.success?
|
29
|
+
|
30
|
+
move_pid = Process.spawn("mv #{pwd}/target/wasm32-wasip2/release/example.wasm #{pwd}/../example.wasm")
|
31
|
+
Process.wait(move_pid)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Build all fixtures"
|
37
|
+
task fixtures: %i[fixtures:apps:clean fixtures:apps:compile]
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: app_bridge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexander Ross
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-02-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rb_sys
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.9.91
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.9.91
|
27
|
+
description: The app_bridge gem is designed to enable seamless interaction with WebAssembly
|
28
|
+
components that adhere to the WIT specification `standout:app`. It is developed
|
29
|
+
for use in Standout's products.
|
30
|
+
email:
|
31
|
+
- ross@standout.se
|
32
|
+
executables: []
|
33
|
+
extensions:
|
34
|
+
- ext/app_bridge/extconf.rb
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- ".rspec"
|
38
|
+
- ".rubocop.yml"
|
39
|
+
- CHANGELOG.md
|
40
|
+
- Cargo.lock
|
41
|
+
- Cargo.toml
|
42
|
+
- README.md
|
43
|
+
- Rakefile
|
44
|
+
- ext/app_bridge/Cargo.toml
|
45
|
+
- ext/app_bridge/extconf.rb
|
46
|
+
- ext/app_bridge/src/app_state.rs
|
47
|
+
- ext/app_bridge/src/component.rs
|
48
|
+
- ext/app_bridge/src/lib.rs
|
49
|
+
- ext/app_bridge/src/request_builder.rs
|
50
|
+
- ext/app_bridge/wit/world.wit
|
51
|
+
- lib/app_bridge.rb
|
52
|
+
- lib/app_bridge/version.rb
|
53
|
+
- sig/app_bridge.rbs
|
54
|
+
- tasks/fixtures.rake
|
55
|
+
homepage: https://github.com/standout/app_bridge
|
56
|
+
licenses: []
|
57
|
+
metadata:
|
58
|
+
allowed_push_host: https://rubygems.org
|
59
|
+
homepage_uri: https://github.com/standout/app_bridge
|
60
|
+
source_code_uri: https://github.com/standout/app_bridge
|
61
|
+
changelog_uri: https://github.com/standout/app_bridge/blob/main/CHANGELOG.md
|
62
|
+
rubygems_mfa_required: 'true'
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 3.0.0
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 3.3.11
|
77
|
+
requirements: []
|
78
|
+
rubygems_version: 3.5.22
|
79
|
+
signing_key:
|
80
|
+
specification_version: 4
|
81
|
+
summary: Communication layer for Standout integration apps
|
82
|
+
test_files: []
|