spikard 0.5.0 → 0.6.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/LICENSE +1 -1
- data/README.md +674 -674
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +13 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +405 -405
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +13 -13
- data/lib/spikard/handler_wrapper.rb +113 -113
- data/lib/spikard/provide.rb +214 -214
- data/lib/spikard/response.rb +173 -173
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +44 -44
- data/lib/spikard/testing.rb +256 -256
- data/lib/spikard/upload_file.rb +131 -131
- data/lib/spikard/version.rb +5 -5
- data/lib/spikard/websocket.rb +59 -59
- data/lib/spikard.rb +43 -43
- data/sig/spikard.rbs +366 -366
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -63
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -132
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -752
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -194
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -246
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -401
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -238
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -24
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -292
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -616
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -305
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -248
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -351
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -454
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -383
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -280
- data/vendor/crates/spikard-core/Cargo.toml +40 -40
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
- data/vendor/crates/spikard-core/src/debug.rs +127 -127
- data/vendor/crates/spikard-core/src/di/container.rs +702 -702
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
- data/vendor/crates/spikard-core/src/di/error.rs +118 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +534 -534
- data/vendor/crates/spikard-core/src/di/graph.rs +506 -506
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +405 -405
- data/vendor/crates/spikard-core/src/di/value.rs +281 -281
- data/vendor/crates/spikard-core/src/errors.rs +69 -69
- data/vendor/crates/spikard-core/src/http.rs +415 -415
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -1186
- data/vendor/crates/spikard-core/src/metadata.rs +389 -389
- data/vendor/crates/spikard-core/src/parameters.rs +2525 -2525
- data/vendor/crates/spikard-core/src/problem.rs +344 -344
- data/vendor/crates/spikard-core/src/request_data.rs +1154 -1154
- data/vendor/crates/spikard-core/src/router.rs +510 -510
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +696 -688
- data/vendor/crates/spikard-core/src/validation/mod.rs +457 -457
- data/vendor/crates/spikard-http/Cargo.toml +62 -64
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -148
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -92
- data/vendor/crates/spikard-http/src/auth.rs +296 -296
- data/vendor/crates/spikard-http/src/background.rs +1860 -1860
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
- data/vendor/crates/spikard-http/src/cors.rs +1005 -1005
- data/vendor/crates/spikard-http/src/debug.rs +128 -128
- data/vendor/crates/spikard-http/src/di_handler.rs +1668 -1668
- data/vendor/crates/spikard-http/src/handler_response.rs +901 -901
- data/vendor/crates/spikard-http/src/handler_trait.rs +838 -830
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -290
- data/vendor/crates/spikard-http/src/lib.rs +534 -534
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -230
- data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -1193
- data/vendor/crates/spikard-http/src/middleware/mod.rs +560 -540
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -912
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -513
- data/vendor/crates/spikard-http/src/middleware/validation.rs +768 -735
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -535
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -1363
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -665
- data/vendor/crates/spikard-http/src/query_parser.rs +793 -793
- data/vendor/crates/spikard-http/src/response.rs +720 -720
- data/vendor/crates/spikard-http/src/server/handler.rs +1650 -1650
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -234
- data/vendor/crates/spikard-http/src/server/mod.rs +1593 -1502
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +789 -770
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +629 -599
- data/vendor/crates/spikard-http/src/sse.rs +1409 -1409
- data/vendor/crates/spikard-http/src/testing/form.rs +52 -52
- data/vendor/crates/spikard-http/src/testing/multipart.rs +64 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +311 -283
- data/vendor/crates/spikard-http/src/testing.rs +406 -377
- data/vendor/crates/spikard-http/src/websocket.rs +1404 -1375
- data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -832
- data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -309
- data/vendor/crates/spikard-http/tests/common/mod.rs +26 -26
- data/vendor/crates/spikard-http/tests/di_integration.rs +192 -192
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -5
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -1093
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -656
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -314
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -620
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -663
- data/vendor/crates/spikard-rb/Cargo.toml +48 -48
- data/vendor/crates/spikard-rb/build.rs +199 -199
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/config/server_config.rs +285 -285
- data/vendor/crates/spikard-rb/src/conversion.rs +554 -554
- data/vendor/crates/spikard-rb/src/di/builder.rs +100 -100
- data/vendor/crates/spikard-rb/src/di/mod.rs +375 -375
- data/vendor/crates/spikard-rb/src/handler.rs +618 -618
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -3
- data/vendor/crates/spikard-rb/src/lib.rs +1806 -1810
- data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -275
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +442 -447
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -324
- data/vendor/crates/spikard-rb/src/server.rs +305 -308
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/testing/client.rs +538 -551
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -7
- data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +608 -635
- data/vendor/crates/spikard-rb/src/websocket.rs +377 -374
- metadata +15 -1
data/lib/spikard/testing.rb
CHANGED
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'timeout'
|
|
5
|
-
|
|
6
|
-
module Spikard
|
|
7
|
-
# Testing helpers that wrap the native Ruby extension.
|
|
8
|
-
module Testing
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
def create_test_client(app, config: nil)
|
|
12
|
-
trace('create_test_client:start')
|
|
13
|
-
ensure_native_test_client!
|
|
14
|
-
config = resolve_test_config(app, config)
|
|
15
|
-
native = build_native_test_client(app, config)
|
|
16
|
-
trace('create_test_client:done')
|
|
17
|
-
TestClient.new(native)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def ensure_native_test_client!
|
|
21
|
-
return if defined?(Spikard::Native::TestClient)
|
|
22
|
-
|
|
23
|
-
raise LoadError, 'Spikard native test client is not available. Build the native extension before running tests.'
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def resolve_test_config(app, config)
|
|
27
|
-
return config if config
|
|
28
|
-
|
|
29
|
-
if app.instance_variable_defined?(:@__spikard_test_config)
|
|
30
|
-
return app.instance_variable_get(:@__spikard_test_config)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
Spikard::ServerConfig.new
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def build_native_test_client(app, config)
|
|
37
|
-
routes_json = app.normalized_routes_json
|
|
38
|
-
handlers = app.handler_map.transform_keys(&:to_sym)
|
|
39
|
-
ws_handlers = app.websocket_handlers || {}
|
|
40
|
-
sse_producers = app.sse_producers || {}
|
|
41
|
-
dependencies = app.dependencies || {}
|
|
42
|
-
Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers, dependencies)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# High level wrapper around the native test client.
|
|
46
|
-
class TestClient
|
|
47
|
-
def initialize(native)
|
|
48
|
-
@native = native
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Factory method for creating test client from an app
|
|
52
|
-
def self.new(app_or_native, config: nil)
|
|
53
|
-
# If passed a native client directly, use it
|
|
54
|
-
return super(app_or_native) if app_or_native.is_a?(Spikard::Native::TestClient)
|
|
55
|
-
|
|
56
|
-
# Otherwise, create test client from app
|
|
57
|
-
Spikard::Testing.create_test_client(app_or_native, config: config)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def request(method, path, **options)
|
|
61
|
-
payload = @native.request(method.to_s.upcase, path, options)
|
|
62
|
-
Response.new(payload)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def websocket(path)
|
|
66
|
-
Testing.trace("websocket:start #{path}")
|
|
67
|
-
native_ws = @native.websocket(path)
|
|
68
|
-
Testing.trace("websocket:connected #{path}")
|
|
69
|
-
WebSocketTestConnection.new(native_ws)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def sse(path)
|
|
73
|
-
native_sse = @native.sse(path)
|
|
74
|
-
SseStream.new(native_sse)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def close
|
|
78
|
-
@native.close
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
%w[get post put patch delete head options trace].each do |verb|
|
|
82
|
-
define_method(verb) do |path, **options|
|
|
83
|
-
request(verb.upcase, path, **options)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# WebSocket test connection wrapper
|
|
89
|
-
class WebSocketTestConnection
|
|
90
|
-
def initialize(native_ws)
|
|
91
|
-
@native_ws = native_ws
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def send_text(text)
|
|
95
|
-
Testing.trace('websocket:send_text')
|
|
96
|
-
@native_ws.send_text(JSON.generate(text))
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def send_json(obj)
|
|
100
|
-
Testing.trace('websocket:send_json')
|
|
101
|
-
@native_ws.send_json(obj)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def receive_text
|
|
105
|
-
Testing.trace('websocket:receive_text')
|
|
106
|
-
raw = with_timeout { @native_ws.receive_text }
|
|
107
|
-
JSON.parse(raw)
|
|
108
|
-
rescue JSON::ParserError
|
|
109
|
-
raw
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def receive_json
|
|
113
|
-
Testing.trace('websocket:receive_json')
|
|
114
|
-
with_timeout { @native_ws.receive_json }
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def receive_bytes
|
|
118
|
-
receive_text
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def receive_message
|
|
122
|
-
native_msg = @native_ws.receive_message
|
|
123
|
-
WebSocketMessage.new(native_msg)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def close
|
|
127
|
-
Testing.trace('websocket:close')
|
|
128
|
-
@native_ws.close
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
private
|
|
132
|
-
|
|
133
|
-
def with_timeout(&)
|
|
134
|
-
timeout_ms = ENV.fetch('SPIKARD_RB_TEST_TIMEOUT_MS', nil)
|
|
135
|
-
return yield unless timeout_ms
|
|
136
|
-
|
|
137
|
-
Timeout.timeout(timeout_ms.to_f / 1000.0, &)
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# WebSocket message wrapper
|
|
142
|
-
class WebSocketMessage
|
|
143
|
-
def initialize(native_msg)
|
|
144
|
-
@native_msg = native_msg
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def as_text
|
|
148
|
-
raw = @native_msg.as_text
|
|
149
|
-
return unless raw
|
|
150
|
-
|
|
151
|
-
JSON.parse(raw)
|
|
152
|
-
rescue JSON::ParserError
|
|
153
|
-
raw
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def as_json
|
|
157
|
-
@native_msg.as_json
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def as_binary
|
|
161
|
-
@native_msg.as_binary
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def close?
|
|
165
|
-
@native_msg.is_close
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# SSE stream wrapper
|
|
170
|
-
class SseStream
|
|
171
|
-
def initialize(native_sse)
|
|
172
|
-
@native_sse = native_sse
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def body
|
|
176
|
-
@native_sse.body
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def events
|
|
180
|
-
parsed_chunks.map { |chunk| InlineSseEvent.new(chunk) }
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def events_as_json
|
|
184
|
-
parsed_chunks.filter_map do |chunk|
|
|
185
|
-
JSON.parse(chunk)
|
|
186
|
-
rescue JSON::ParserError
|
|
187
|
-
nil
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
private
|
|
192
|
-
|
|
193
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
194
|
-
def parsed_chunks
|
|
195
|
-
raw = body.to_s.gsub("\r\n", "\n")
|
|
196
|
-
events = []
|
|
197
|
-
current = []
|
|
198
|
-
|
|
199
|
-
raw.each_line do |line|
|
|
200
|
-
stripped = line.chomp
|
|
201
|
-
if stripped.start_with?('data:')
|
|
202
|
-
current << stripped[5..].strip
|
|
203
|
-
elsif stripped.empty?
|
|
204
|
-
unless current.empty?
|
|
205
|
-
data = current.join("\n").strip
|
|
206
|
-
events << data unless data.empty?
|
|
207
|
-
current = []
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
unless current.empty?
|
|
213
|
-
data = current.join("\n").strip
|
|
214
|
-
events << data unless data.empty?
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
events
|
|
218
|
-
end
|
|
219
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# SSE event wrapper
|
|
223
|
-
class SseEvent
|
|
224
|
-
def initialize(native_event)
|
|
225
|
-
@native_event = native_event
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def data
|
|
229
|
-
@native_event.data
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def as_json
|
|
233
|
-
@native_event.as_json
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# Lightweight wrapper for parsed SSE events backed by strings.
|
|
238
|
-
class InlineSseEvent
|
|
239
|
-
attr_reader :data
|
|
240
|
-
|
|
241
|
-
def initialize(data)
|
|
242
|
-
@data = data
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def as_json
|
|
246
|
-
JSON.parse(@data)
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def trace(message)
|
|
251
|
-
return unless ENV['SPIKARD_RB_TEST_TRACE'] == '1'
|
|
252
|
-
|
|
253
|
-
warn("[spikard-rb-test] #{message}")
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module Spikard
|
|
7
|
+
# Testing helpers that wrap the native Ruby extension.
|
|
8
|
+
module Testing
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def create_test_client(app, config: nil)
|
|
12
|
+
trace('create_test_client:start')
|
|
13
|
+
ensure_native_test_client!
|
|
14
|
+
config = resolve_test_config(app, config)
|
|
15
|
+
native = build_native_test_client(app, config)
|
|
16
|
+
trace('create_test_client:done')
|
|
17
|
+
TestClient.new(native)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ensure_native_test_client!
|
|
21
|
+
return if defined?(Spikard::Native::TestClient)
|
|
22
|
+
|
|
23
|
+
raise LoadError, 'Spikard native test client is not available. Build the native extension before running tests.'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def resolve_test_config(app, config)
|
|
27
|
+
return config if config
|
|
28
|
+
|
|
29
|
+
if app.instance_variable_defined?(:@__spikard_test_config)
|
|
30
|
+
return app.instance_variable_get(:@__spikard_test_config)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Spikard::ServerConfig.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_native_test_client(app, config)
|
|
37
|
+
routes_json = app.normalized_routes_json
|
|
38
|
+
handlers = app.handler_map.transform_keys(&:to_sym)
|
|
39
|
+
ws_handlers = app.websocket_handlers || {}
|
|
40
|
+
sse_producers = app.sse_producers || {}
|
|
41
|
+
dependencies = app.dependencies || {}
|
|
42
|
+
Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers, dependencies)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# High level wrapper around the native test client.
|
|
46
|
+
class TestClient
|
|
47
|
+
def initialize(native)
|
|
48
|
+
@native = native
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Factory method for creating test client from an app
|
|
52
|
+
def self.new(app_or_native, config: nil)
|
|
53
|
+
# If passed a native client directly, use it
|
|
54
|
+
return super(app_or_native) if app_or_native.is_a?(Spikard::Native::TestClient)
|
|
55
|
+
|
|
56
|
+
# Otherwise, create test client from app
|
|
57
|
+
Spikard::Testing.create_test_client(app_or_native, config: config)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def request(method, path, **options)
|
|
61
|
+
payload = @native.request(method.to_s.upcase, path, options)
|
|
62
|
+
Response.new(payload)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def websocket(path)
|
|
66
|
+
Testing.trace("websocket:start #{path}")
|
|
67
|
+
native_ws = @native.websocket(path)
|
|
68
|
+
Testing.trace("websocket:connected #{path}")
|
|
69
|
+
WebSocketTestConnection.new(native_ws)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sse(path)
|
|
73
|
+
native_sse = @native.sse(path)
|
|
74
|
+
SseStream.new(native_sse)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def close
|
|
78
|
+
@native.close
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
%w[get post put patch delete head options trace].each do |verb|
|
|
82
|
+
define_method(verb) do |path, **options|
|
|
83
|
+
request(verb.upcase, path, **options)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# WebSocket test connection wrapper
|
|
89
|
+
class WebSocketTestConnection
|
|
90
|
+
def initialize(native_ws)
|
|
91
|
+
@native_ws = native_ws
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def send_text(text)
|
|
95
|
+
Testing.trace('websocket:send_text')
|
|
96
|
+
@native_ws.send_text(JSON.generate(text))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def send_json(obj)
|
|
100
|
+
Testing.trace('websocket:send_json')
|
|
101
|
+
@native_ws.send_json(obj)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def receive_text
|
|
105
|
+
Testing.trace('websocket:receive_text')
|
|
106
|
+
raw = with_timeout { @native_ws.receive_text }
|
|
107
|
+
JSON.parse(raw)
|
|
108
|
+
rescue JSON::ParserError
|
|
109
|
+
raw
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def receive_json
|
|
113
|
+
Testing.trace('websocket:receive_json')
|
|
114
|
+
with_timeout { @native_ws.receive_json }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def receive_bytes
|
|
118
|
+
receive_text
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def receive_message
|
|
122
|
+
native_msg = @native_ws.receive_message
|
|
123
|
+
WebSocketMessage.new(native_msg)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def close
|
|
127
|
+
Testing.trace('websocket:close')
|
|
128
|
+
@native_ws.close
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def with_timeout(&)
|
|
134
|
+
timeout_ms = ENV.fetch('SPIKARD_RB_TEST_TIMEOUT_MS', nil)
|
|
135
|
+
return yield unless timeout_ms
|
|
136
|
+
|
|
137
|
+
Timeout.timeout(timeout_ms.to_f / 1000.0, &)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# WebSocket message wrapper
|
|
142
|
+
class WebSocketMessage
|
|
143
|
+
def initialize(native_msg)
|
|
144
|
+
@native_msg = native_msg
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def as_text
|
|
148
|
+
raw = @native_msg.as_text
|
|
149
|
+
return unless raw
|
|
150
|
+
|
|
151
|
+
JSON.parse(raw)
|
|
152
|
+
rescue JSON::ParserError
|
|
153
|
+
raw
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def as_json
|
|
157
|
+
@native_msg.as_json
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def as_binary
|
|
161
|
+
@native_msg.as_binary
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def close?
|
|
165
|
+
@native_msg.is_close
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# SSE stream wrapper
|
|
170
|
+
class SseStream
|
|
171
|
+
def initialize(native_sse)
|
|
172
|
+
@native_sse = native_sse
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def body
|
|
176
|
+
@native_sse.body
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def events
|
|
180
|
+
parsed_chunks.map { |chunk| InlineSseEvent.new(chunk) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def events_as_json
|
|
184
|
+
parsed_chunks.filter_map do |chunk|
|
|
185
|
+
JSON.parse(chunk)
|
|
186
|
+
rescue JSON::ParserError
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
194
|
+
def parsed_chunks
|
|
195
|
+
raw = body.to_s.gsub("\r\n", "\n")
|
|
196
|
+
events = []
|
|
197
|
+
current = []
|
|
198
|
+
|
|
199
|
+
raw.each_line do |line|
|
|
200
|
+
stripped = line.chomp
|
|
201
|
+
if stripped.start_with?('data:')
|
|
202
|
+
current << stripped[5..].strip
|
|
203
|
+
elsif stripped.empty?
|
|
204
|
+
unless current.empty?
|
|
205
|
+
data = current.join("\n").strip
|
|
206
|
+
events << data unless data.empty?
|
|
207
|
+
current = []
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
unless current.empty?
|
|
213
|
+
data = current.join("\n").strip
|
|
214
|
+
events << data unless data.empty?
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
events
|
|
218
|
+
end
|
|
219
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# SSE event wrapper
|
|
223
|
+
class SseEvent
|
|
224
|
+
def initialize(native_event)
|
|
225
|
+
@native_event = native_event
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def data
|
|
229
|
+
@native_event.data
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def as_json
|
|
233
|
+
@native_event.as_json
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Lightweight wrapper for parsed SSE events backed by strings.
|
|
238
|
+
class InlineSseEvent
|
|
239
|
+
attr_reader :data
|
|
240
|
+
|
|
241
|
+
def initialize(data)
|
|
242
|
+
@data = data
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def as_json
|
|
246
|
+
JSON.parse(@data)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def trace(message)
|
|
251
|
+
return unless ENV['SPIKARD_RB_TEST_TRACE'] == '1'
|
|
252
|
+
|
|
253
|
+
warn("[spikard-rb-test] #{message}")
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|