spikard 0.11.0 → 0.12.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 +4 -4
- data/ext/spikard_rb/Cargo.toml +1 -1
- data/lib/spikard/config.rb +36 -2
- data/lib/spikard/testing.rb +136 -0
- data/lib/spikard/version.rb +1 -1
- data/lib/spikard.rb +3 -7
- data/sig/spikard.rbs +422 -73
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +12 -1
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +118 -3
- data/vendor/crates/spikard-core/Cargo.toml +4 -1
- data/vendor/crates/spikard-http/Cargo.toml +16 -4
- data/vendor/crates/spikard-http/src/auth.rs +8 -3
- data/vendor/crates/spikard-http/src/server/grpc_routing.rs +14 -11
- data/vendor/crates/spikard-http/src/server/mod.rs +16 -67
- data/vendor/crates/spikard-http/src/testing/test_client.rs +334 -11
- data/vendor/crates/spikard-http/src/testing.rs +1 -1
- data/vendor/crates/spikard-http/tests/auth_integration.rs +4 -6
- data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +3 -3
- data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +85 -1
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -1
- data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +1 -1
- data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +6 -8
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +3 -3
- data/vendor/crates/spikard-http/tests/testing_helpers.rs +2 -2
- data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +4 -5
- data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +2 -2
- data/vendor/crates/spikard-http/tests/websocket_integration.rs +2 -4
- data/vendor/crates/spikard-rb/Cargo.toml +11 -2
- data/vendor/crates/spikard-rb/src/config/server_config.rs +120 -5
- data/vendor/crates/spikard-rb/src/lib.rs +2 -16
- data/vendor/crates/spikard-rb/src/testing/client.rs +2 -2
- data/vendor/crates/spikard-rb-macros/Cargo.toml +4 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6d3ace9a6aa31de25bf31238a3a2425546c5137f85036cce599ac251cf63e25
|
|
4
|
+
data.tar.gz: b60af042fe08fca1e779d15fea771e6f29c864491cad98260fd3c1aa4887f538
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8786dce5521a0d9d4c38dbcb4c433bed930bc77dba34ad149a42a00d03803ffba58318f87c44abee93175d842ba2d94bb3af0b3c6a0e156179d431c441921473
|
|
7
|
+
data.tar.gz: 6cf9e10315120acf2593693f9a998487e0c3613ba50a3c4693ae408747f7b9efae82cc57a4962c9d32eb337745593e70fbc00bd2cb1e57949d72363db0da265b
|
data/ext/spikard_rb/Cargo.toml
CHANGED
data/lib/spikard/config.rb
CHANGED
|
@@ -340,6 +340,37 @@ module Spikard
|
|
|
340
340
|
end
|
|
341
341
|
end
|
|
342
342
|
|
|
343
|
+
# JSON-RPC endpoint configuration.
|
|
344
|
+
class JsonRpcConfig
|
|
345
|
+
attr_accessor :enabled, :endpoint_path, :enable_batch, :max_batch_size
|
|
346
|
+
|
|
347
|
+
# @param enabled [Boolean] Enable JSON-RPC endpoint registration (default: true)
|
|
348
|
+
# @param endpoint_path [String] JSON-RPC endpoint path (default: "/rpc")
|
|
349
|
+
# @param enable_batch [Boolean] Enable JSON-RPC batch support (default: true)
|
|
350
|
+
# @param max_batch_size [Integer] Maximum batch size (default: 100)
|
|
351
|
+
def initialize(enabled: true, endpoint_path: '/rpc', enable_batch: true, max_batch_size: 100)
|
|
352
|
+
@enabled = normalize_boolean('enabled', enabled)
|
|
353
|
+
@endpoint_path = endpoint_path
|
|
354
|
+
@enable_batch = normalize_boolean('enable_batch', enable_batch)
|
|
355
|
+
@max_batch_size = normalize_positive_integer('max_batch_size', max_batch_size)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
private
|
|
359
|
+
|
|
360
|
+
def normalize_boolean(name, value)
|
|
361
|
+
return value if [true, false].include?(value)
|
|
362
|
+
|
|
363
|
+
raise ArgumentError, "#{name} must be a boolean"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def normalize_positive_integer(name, value)
|
|
367
|
+
raise ArgumentError, "#{name} must be an Integer" unless value.is_a?(Integer)
|
|
368
|
+
return value if value.positive?
|
|
369
|
+
|
|
370
|
+
raise ArgumentError, "#{name} must be > 0"
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
343
374
|
# Complete server configuration for Spikard.
|
|
344
375
|
#
|
|
345
376
|
# This is the main configuration object that controls all aspects of the server
|
|
@@ -369,7 +400,7 @@ module Spikard
|
|
|
369
400
|
:enable_request_id, :max_body_size, :request_timeout,
|
|
370
401
|
:compression, :rate_limit, :jwt_auth, :api_key_auth,
|
|
371
402
|
:static_files, :graceful_shutdown, :shutdown_timeout,
|
|
372
|
-
:openapi
|
|
403
|
+
:openapi, :jsonrpc
|
|
373
404
|
|
|
374
405
|
# @param host [String] Host address to bind to (default: "127.0.0.1")
|
|
375
406
|
# @param port [Integer] Port number to listen on (default: 8000, range: 1-65535)
|
|
@@ -385,6 +416,7 @@ module Spikard
|
|
|
385
416
|
# @param graceful_shutdown [Boolean] Enable graceful shutdown (default: true)
|
|
386
417
|
# @param shutdown_timeout [Integer] Graceful shutdown timeout in seconds (default: 30)
|
|
387
418
|
# @param openapi [OpenApiConfig, nil] OpenAPI configuration (default: nil/disabled)
|
|
419
|
+
# @param jsonrpc [JsonRpcConfig, nil] JSON-RPC configuration (default: nil/disabled)
|
|
388
420
|
def initialize(
|
|
389
421
|
host: '127.0.0.1',
|
|
390
422
|
port: 8000,
|
|
@@ -399,7 +431,8 @@ module Spikard
|
|
|
399
431
|
static_files: [],
|
|
400
432
|
graceful_shutdown: true,
|
|
401
433
|
shutdown_timeout: 30,
|
|
402
|
-
openapi: nil
|
|
434
|
+
openapi: nil,
|
|
435
|
+
jsonrpc: nil
|
|
403
436
|
)
|
|
404
437
|
@host = host
|
|
405
438
|
@port = normalize_port(port)
|
|
@@ -415,6 +448,7 @@ module Spikard
|
|
|
415
448
|
@graceful_shutdown = normalize_boolean('graceful_shutdown', graceful_shutdown)
|
|
416
449
|
@shutdown_timeout = normalize_timeout('shutdown_timeout', shutdown_timeout)
|
|
417
450
|
@openapi = openapi
|
|
451
|
+
@jsonrpc = jsonrpc
|
|
418
452
|
end
|
|
419
453
|
|
|
420
454
|
private
|
data/lib/spikard/testing.rb
CHANGED
|
@@ -6,6 +6,8 @@ require 'timeout'
|
|
|
6
6
|
module Spikard
|
|
7
7
|
# Testing helpers that wrap the native Ruby extension.
|
|
8
8
|
module Testing
|
|
9
|
+
GRAPHQL_WS_MAX_CONTROL_MESSAGES = 32
|
|
10
|
+
|
|
9
11
|
module_function
|
|
10
12
|
|
|
11
13
|
def create_test_client(app, config: nil)
|
|
@@ -45,6 +47,7 @@ module Spikard
|
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
# High level wrapper around the native test client.
|
|
50
|
+
# rubocop:disable Metrics/ClassLength
|
|
48
51
|
class TestClient
|
|
49
52
|
def initialize(native)
|
|
50
53
|
@native = native
|
|
@@ -77,6 +80,118 @@ module Spikard
|
|
|
77
80
|
SseStream.new(native_sse)
|
|
78
81
|
end
|
|
79
82
|
|
|
83
|
+
def graphql(query, variables = nil, operation_name = nil, path: '/graphql')
|
|
84
|
+
payload = { query: query }
|
|
85
|
+
payload[:variables] = variables unless variables.nil?
|
|
86
|
+
payload[:operationName] = operation_name unless operation_name.nil?
|
|
87
|
+
request('POST', path, nil, nil, json: payload)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def graphql_with_status(query, variables = nil, operation_name = nil, path: '/graphql')
|
|
91
|
+
response = graphql(query, variables, operation_name, path: path)
|
|
92
|
+
[response.status, response]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity
|
|
96
|
+
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
97
|
+
def graphql_subscription(query, variables = nil, operation_name = nil, path: '/graphql')
|
|
98
|
+
operation_id = 'spikard-subscription-1'
|
|
99
|
+
payload = { query: query }
|
|
100
|
+
payload[:variables] = variables unless variables.nil?
|
|
101
|
+
payload[:operationName] = operation_name unless operation_name.nil?
|
|
102
|
+
|
|
103
|
+
ws = websocket(path)
|
|
104
|
+
ws.send_json({ 'type' => 'connection_init' })
|
|
105
|
+
|
|
106
|
+
acknowledged = false
|
|
107
|
+
GRAPHQL_WS_MAX_CONTROL_MESSAGES.times do
|
|
108
|
+
message = websocket_protocol_message(ws.receive_json)
|
|
109
|
+
message_type = websocket_field(message, :type)
|
|
110
|
+
|
|
111
|
+
if message_type == 'connection_ack'
|
|
112
|
+
acknowledged = true
|
|
113
|
+
break
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if message_type == 'ping'
|
|
117
|
+
pong = { 'type' => 'pong' }
|
|
118
|
+
pong['payload'] = websocket_field(message, :payload) if message.key?('payload') || message.key?(:payload)
|
|
119
|
+
ws.send_json(pong)
|
|
120
|
+
next
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if %w[connection_error error].include?(message_type)
|
|
124
|
+
raise "GraphQL subscription rejected during init: #{message}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
raise 'No GraphQL connection_ack received' unless acknowledged
|
|
129
|
+
|
|
130
|
+
ws.send_json({
|
|
131
|
+
'id' => operation_id,
|
|
132
|
+
'type' => 'subscribe',
|
|
133
|
+
'payload' => payload
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
event = nil
|
|
137
|
+
errors = []
|
|
138
|
+
complete_received = false
|
|
139
|
+
|
|
140
|
+
GRAPHQL_WS_MAX_CONTROL_MESSAGES.times do
|
|
141
|
+
message = websocket_protocol_message(ws.receive_json)
|
|
142
|
+
message_type = websocket_field(message, :type)
|
|
143
|
+
message_id = websocket_field(message, :id)
|
|
144
|
+
id_matches = message_id.nil? || message_id == operation_id
|
|
145
|
+
|
|
146
|
+
if message_type == 'next' && id_matches
|
|
147
|
+
event = websocket_field(message, :payload)
|
|
148
|
+
ws.send_json({ 'id' => operation_id, 'type' => 'complete' })
|
|
149
|
+
|
|
150
|
+
begin
|
|
151
|
+
maybe_complete = websocket_protocol_message(ws.receive_json)
|
|
152
|
+
complete_type = websocket_field(maybe_complete, :type)
|
|
153
|
+
complete_id = websocket_field(maybe_complete, :id)
|
|
154
|
+
complete_received = complete_type == 'complete' && (complete_id.nil? || complete_id == operation_id)
|
|
155
|
+
rescue StandardError
|
|
156
|
+
# Some servers close immediately after client complete.
|
|
157
|
+
end
|
|
158
|
+
break
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if message_type == 'error'
|
|
162
|
+
errors << (websocket_field(message, :payload) || message)
|
|
163
|
+
break
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if message_type == 'complete' && id_matches
|
|
167
|
+
complete_received = true
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
next unless message_type == 'ping'
|
|
172
|
+
|
|
173
|
+
pong = { 'type' => 'pong' }
|
|
174
|
+
pong['payload'] = websocket_field(message, :payload) if message.key?('payload') || message.key?(:payload)
|
|
175
|
+
ws.send_json(pong)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
if event.nil? && errors.empty? && !complete_received
|
|
179
|
+
raise 'No GraphQL subscription event received before timeout'
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
'operation_id' => operation_id,
|
|
184
|
+
'acknowledged' => true,
|
|
185
|
+
'event' => event,
|
|
186
|
+
'errors' => errors,
|
|
187
|
+
'complete_received' => complete_received
|
|
188
|
+
}
|
|
189
|
+
ensure
|
|
190
|
+
ws&.close
|
|
191
|
+
end
|
|
192
|
+
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
193
|
+
# rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity
|
|
194
|
+
|
|
80
195
|
def close
|
|
81
196
|
@native.close
|
|
82
197
|
end
|
|
@@ -123,7 +238,28 @@ module Spikard
|
|
|
123
238
|
payload[:files] = files if files
|
|
124
239
|
payload
|
|
125
240
|
end
|
|
241
|
+
|
|
242
|
+
def websocket_protocol_message(raw)
|
|
243
|
+
case raw
|
|
244
|
+
when String
|
|
245
|
+
parsed = JSON.parse(raw)
|
|
246
|
+
raise 'Expected GraphQL WebSocket JSON object message' unless parsed.is_a?(Hash)
|
|
247
|
+
|
|
248
|
+
parsed
|
|
249
|
+
when Hash
|
|
250
|
+
raw
|
|
251
|
+
else
|
|
252
|
+
raise "Expected GraphQL WebSocket message object, got #{raw.class}"
|
|
253
|
+
end
|
|
254
|
+
rescue JSON::ParserError => e
|
|
255
|
+
raise "Invalid GraphQL WebSocket message: #{e.message}"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def websocket_field(message, key)
|
|
259
|
+
message[key.to_s] || message[key.to_sym]
|
|
260
|
+
end
|
|
126
261
|
end
|
|
262
|
+
# rubocop:enable Metrics/ClassLength
|
|
127
263
|
|
|
128
264
|
# WebSocket test connection wrapper
|
|
129
265
|
class WebSocketTestConnection
|
data/lib/spikard/version.rb
CHANGED
data/lib/spikard.rb
CHANGED
|
@@ -45,10 +45,6 @@ if defined?(Spikard::Native::TestClient) && !Spikard::Native::TestClient.method_
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# Convenience aliases and methods
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# Handler wrapper utilities
|
|
53
|
-
extend HandlerWrapper
|
|
54
|
-
end
|
|
48
|
+
# Convenience aliases and methods
|
|
49
|
+
Spikard::TestClient = Spikard::Testing::TestClient
|
|
50
|
+
Spikard.extend Spikard::HandlerWrapper
|