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.
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppBridge
4
+ VERSION = "0.1.0"
5
+ end
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
@@ -0,0 +1,10 @@
1
+ module AppBridge
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+
5
+ class App
6
+ def initialize: (String) -> void
7
+
8
+ def triggers : () -> Array[String]
9
+ end
10
+ end
@@ -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: []