spikard 0.13.0 → 0.15.2
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/Steepfile +6 -0
- data/ext/spikard_rb/extconf.rb +1 -2
- data/ext/spikard_rb/{Cargo.lock → native/Cargo.lock} +819 -424
- data/ext/spikard_rb/native/Cargo.toml +24 -0
- data/ext/spikard_rb/src/lib.rs +5366 -3
- data/lib/spikard/native.rb +86 -0
- data/lib/spikard/version.rb +6 -1
- data/lib/spikard.rb +8 -52
- data/lib/spikard_rb.so +0 -0
- data/sig/types.rbs +427 -0
- metadata +14 -243
- data/LICENSE +0 -1
- data/README.md +0 -285
- data/ext/spikard_rb/Cargo.toml +0 -17
- data/lib/spikard/app.rb +0 -458
- data/lib/spikard/background.rb +0 -58
- data/lib/spikard/config.rb +0 -506
- data/lib/spikard/converters.rb +0 -13
- data/lib/spikard/grpc.rb +0 -232
- data/lib/spikard/handler_wrapper.rb +0 -113
- data/lib/spikard/provide.rb +0 -315
- data/lib/spikard/response.rb +0 -198
- data/lib/spikard/schema.rb +0 -243
- data/lib/spikard/sse.rb +0 -111
- data/lib/spikard/streaming_response.rb +0 -44
- data/lib/spikard/testing.rb +0 -474
- data/lib/spikard/upload_file.rb +0 -131
- data/lib/spikard/websocket.rb +0 -59
- data/sig/spikard.rbs +0 -739
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +0 -75
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +0 -132
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +0 -905
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +0 -210
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +0 -252
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +0 -404
- data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +0 -199
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +0 -252
- data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +0 -829
- data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +0 -587
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +0 -33
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +0 -298
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +0 -594
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +0 -743
- data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +0 -944
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +0 -260
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +0 -369
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +0 -192
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +0 -383
- data/vendor/crates/spikard-bindings-shared/tests/full_coverage.rs +0 -459
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +0 -280
- data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +0 -669
- data/vendor/crates/spikard-core/Cargo.toml +0 -55
- data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +0 -130
- data/vendor/crates/spikard-core/src/debug.rs +0 -127
- data/vendor/crates/spikard-core/src/di/container.rs +0 -711
- data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
- data/vendor/crates/spikard-core/src/di/error.rs +0 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +0 -548
- data/vendor/crates/spikard-core/src/di/graph.rs +0 -507
- data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +0 -428
- data/vendor/crates/spikard-core/src/di/value.rs +0 -282
- data/vendor/crates/spikard-core/src/errors.rs +0 -72
- data/vendor/crates/spikard-core/src/http.rs +0 -492
- data/vendor/crates/spikard-core/src/lib.rs +0 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +0 -1273
- data/vendor/crates/spikard-core/src/metadata.rs +0 -378
- data/vendor/crates/spikard-core/src/parameters.rs +0 -2546
- data/vendor/crates/spikard-core/src/problem.rs +0 -358
- data/vendor/crates/spikard-core/src/request_data.rs +0 -1146
- data/vendor/crates/spikard-core/src/router.rs +0 -530
- data/vendor/crates/spikard-core/src/schema_registry.rs +0 -197
- data/vendor/crates/spikard-core/src/type_hints.rs +0 -311
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +0 -710
- data/vendor/crates/spikard-core/src/validation/mod.rs +0 -470
- data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +0 -136
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +0 -37
- data/vendor/crates/spikard-core/tests/error_mapper.rs +0 -761
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +0 -106
- data/vendor/crates/spikard-core/tests/parameters_full.rs +0 -701
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +0 -301
- data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +0 -67
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +0 -250
- data/vendor/crates/spikard-core/tests/validation_error_paths.rs +0 -45
- data/vendor/crates/spikard-http/Cargo.toml +0 -82
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +0 -148
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +0 -92
- data/vendor/crates/spikard-http/src/auth.rs +0 -301
- data/vendor/crates/spikard-http/src/background.rs +0 -1859
- data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
- data/vendor/crates/spikard-http/src/cors.rs +0 -1026
- data/vendor/crates/spikard-http/src/debug.rs +0 -128
- data/vendor/crates/spikard-http/src/di_handler.rs +0 -1672
- data/vendor/crates/spikard-http/src/grpc/framing.rs +0 -653
- data/vendor/crates/spikard-http/src/grpc/handler.rs +0 -1211
- data/vendor/crates/spikard-http/src/grpc/mod.rs +0 -556
- data/vendor/crates/spikard-http/src/grpc/service.rs +0 -706
- data/vendor/crates/spikard-http/src/grpc/streaming.rs +0 -319
- data/vendor/crates/spikard-http/src/handler_response.rs +0 -901
- data/vendor/crates/spikard-http/src/handler_trait.rs +0 -1015
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -290
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +0 -502
- data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +0 -648
- data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +0 -60
- data/vendor/crates/spikard-http/src/jsonrpc/openrpc.rs +0 -325
- data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +0 -1207
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +0 -2262
- data/vendor/crates/spikard-http/src/lib.rs +0 -566
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -230
- data/vendor/crates/spikard-http/src/lifecycle.rs +0 -1193
- data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -560
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -912
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -513
- data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -768
- data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -535
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -1363
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -667
- data/vendor/crates/spikard-http/src/query_parser.rs +0 -793
- data/vendor/crates/spikard-http/src/response.rs +0 -720
- data/vendor/crates/spikard-http/src/server/fast_router.rs +0 -186
- data/vendor/crates/spikard-http/src/server/grpc_routing.rs +0 -1243
- data/vendor/crates/spikard-http/src/server/handler.rs +0 -1661
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -253
- data/vendor/crates/spikard-http/src/server/mod.rs +0 -1717
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -871
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +0 -618
- data/vendor/crates/spikard-http/src/sse.rs +0 -1409
- data/vendor/crates/spikard-http/src/testing/form.rs +0 -52
- data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -64
- data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -825
- data/vendor/crates/spikard-http/src/testing.rs +0 -617
- data/vendor/crates/spikard-http/src/websocket.rs +0 -1477
- data/vendor/crates/spikard-http/tests/auth_integration.rs +0 -645
- data/vendor/crates/spikard-http/tests/background_behavior.rs +0 -832
- data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +0 -1012
- data/vendor/crates/spikard-http/tests/common/handlers.rs +0 -309
- data/vendor/crates/spikard-http/tests/common/mod.rs +0 -33
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +0 -628
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +0 -162
- data/vendor/crates/spikard-http/tests/di_integration.rs +0 -192
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +0 -5
- data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +0 -430
- data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +0 -738
- data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +0 -652
- data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +0 -334
- data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +0 -532
- data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +0 -495
- data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +0 -975
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +0 -1093
- data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +0 -389
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +0 -656
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +0 -513
- data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +0 -328
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +0 -335
- data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +0 -374
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +0 -83
- data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +0 -464
- data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +0 -286
- data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +0 -118
- data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +0 -99
- data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +0 -204
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +0 -427
- data/vendor/crates/spikard-http/tests/server_router_behavior.rs +0 -121
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +0 -620
- data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +0 -584
- data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +0 -130
- data/vendor/crates/spikard-http/tests/test_client_requests.rs +0 -167
- data/vendor/crates/spikard-http/tests/testing_helpers.rs +0 -87
- data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +0 -155
- data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +0 -82
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +0 -663
- data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +0 -440
- data/vendor/crates/spikard-http/tests/websocket_integration.rs +0 -150
- data/vendor/crates/spikard-rb/Cargo.toml +0 -63
- data/vendor/crates/spikard-rb/build.rs +0 -200
- data/vendor/crates/spikard-rb/src/background.rs +0 -63
- data/vendor/crates/spikard-rb/src/config/mod.rs +0 -5
- data/vendor/crates/spikard-rb/src/config/server_config.rs +0 -401
- data/vendor/crates/spikard-rb/src/conversion.rs +0 -688
- data/vendor/crates/spikard-rb/src/di/builder.rs +0 -100
- data/vendor/crates/spikard-rb/src/di/mod.rs +0 -410
- data/vendor/crates/spikard-rb/src/grpc/handler.rs +0 -875
- data/vendor/crates/spikard-rb/src/grpc/mod.rs +0 -13
- data/vendor/crates/spikard-rb/src/gvl.rs +0 -80
- data/vendor/crates/spikard-rb/src/handler.rs +0 -699
- data/vendor/crates/spikard-rb/src/integration/mod.rs +0 -3
- data/vendor/crates/spikard-rb/src/lib.rs +0 -2268
- data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -334
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +0 -5
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +0 -507
- data/vendor/crates/spikard-rb/src/request.rs +0 -439
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +0 -5
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +0 -368
- data/vendor/crates/spikard-rb/src/server.rs +0 -304
- data/vendor/crates/spikard-rb/src/sse.rs +0 -231
- data/vendor/crates/spikard-rb/src/testing/client.rs +0 -698
- data/vendor/crates/spikard-rb/src/testing/mod.rs +0 -7
- data/vendor/crates/spikard-rb/src/testing/sse.rs +0 -108
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +0 -573
- data/vendor/crates/spikard-rb/src/websocket.rs +0 -521
- data/vendor/crates/spikard-rb-macros/Cargo.toml +0 -20
- data/vendor/crates/spikard-rb-macros/src/lib.rs +0 -51
data/lib/spikard/testing.rb
DELETED
|
@@ -1,474 +0,0 @@
|
|
|
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
|
-
GRAPHQL_WS_MAX_CONTROL_MESSAGES = 32
|
|
10
|
-
@open_test_clients = []
|
|
11
|
-
|
|
12
|
-
module_function
|
|
13
|
-
|
|
14
|
-
def create_test_client(app, config: nil)
|
|
15
|
-
trace('create_test_client:start')
|
|
16
|
-
ensure_native_test_client!
|
|
17
|
-
config = resolve_test_config(app, config)
|
|
18
|
-
native = build_native_test_client(app, config)
|
|
19
|
-
trace('create_test_client:done')
|
|
20
|
-
TestClient.new(native)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def ensure_native_test_client!
|
|
24
|
-
return if defined?(Spikard::Native::TestClient)
|
|
25
|
-
|
|
26
|
-
raise LoadError, 'Spikard native test client is not available. Build the native extension before running tests.'
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def resolve_test_config(app, config)
|
|
30
|
-
return config if config
|
|
31
|
-
|
|
32
|
-
if app.instance_variable_defined?(:@__spikard_test_config)
|
|
33
|
-
return app.instance_variable_get(:@__spikard_test_config)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
Spikard::ServerConfig.new
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def build_native_test_client(app, config)
|
|
40
|
-
routes_json = app.normalized_routes_json
|
|
41
|
-
handlers = app.handler_map.transform_keys(&:to_sym)
|
|
42
|
-
ws_handlers = app.websocket_handlers || {}
|
|
43
|
-
sse_producers = app.sse_producers || {}
|
|
44
|
-
hooks = app.instance_variable_get(:@native_hooks)
|
|
45
|
-
dependencies = app.dependencies || {}
|
|
46
|
-
payload = { hooks: hooks, dependencies: dependencies }
|
|
47
|
-
Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers, payload)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def register_test_client(client)
|
|
51
|
-
@open_test_clients << client
|
|
52
|
-
client
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def unregister_test_client(client)
|
|
56
|
-
@open_test_clients.delete(client)
|
|
57
|
-
client
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def close_all_test_clients
|
|
61
|
-
clients = @open_test_clients.dup
|
|
62
|
-
@open_test_clients.clear
|
|
63
|
-
|
|
64
|
-
clients.each do |client|
|
|
65
|
-
client.close
|
|
66
|
-
rescue StandardError
|
|
67
|
-
nil
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
if defined?(Spikard::Native) &&
|
|
71
|
-
Spikard::Native.respond_to?(:__shutdown_websocket_workers__, true)
|
|
72
|
-
Spikard::Native.__shutdown_websocket_workers__
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# High level wrapper around the native test client.
|
|
77
|
-
# rubocop:disable Metrics/ClassLength
|
|
78
|
-
class TestClient
|
|
79
|
-
def initialize(native)
|
|
80
|
-
@native = native
|
|
81
|
-
@closed = false
|
|
82
|
-
Spikard::Testing.register_test_client(self)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Factory method for creating test client from an app
|
|
86
|
-
def self.new(app_or_native, config: nil)
|
|
87
|
-
# If passed a native client directly, use it
|
|
88
|
-
return super(app_or_native) if native_test_client_candidate?(app_or_native)
|
|
89
|
-
|
|
90
|
-
# Otherwise, create test client from app
|
|
91
|
-
Spikard::Testing.create_test_client(app_or_native, config: config)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def request(method, path, headers = nil, body = nil, **options)
|
|
95
|
-
payload = build_request_payload(headers, body, options)
|
|
96
|
-
payload = @native.request(method.to_s.upcase, path, payload)
|
|
97
|
-
Response.new(payload)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def websocket(path)
|
|
101
|
-
Testing.trace("websocket:start #{path}")
|
|
102
|
-
native_ws = @native.websocket(path)
|
|
103
|
-
Testing.trace("websocket:connected #{path}")
|
|
104
|
-
WebSocketTestConnection.new(native_ws)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def sse(path)
|
|
108
|
-
native_sse = @native.sse(path)
|
|
109
|
-
SseStream.new(native_sse)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def graphql(query, variables = nil, operation_name = nil, path: '/graphql')
|
|
113
|
-
payload = { query: query }
|
|
114
|
-
payload[:variables] = variables unless variables.nil?
|
|
115
|
-
payload[:operationName] = operation_name unless operation_name.nil?
|
|
116
|
-
request('POST', path, nil, nil, json: payload)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def graphql_with_status(query, variables = nil, operation_name = nil, path: '/graphql')
|
|
120
|
-
response = graphql(query, variables, operation_name, path: path)
|
|
121
|
-
[response.status, response]
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity
|
|
125
|
-
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
126
|
-
def graphql_subscription(query, variables = nil, operation_name = nil, path: '/graphql')
|
|
127
|
-
operation_id = 'spikard-subscription-1'
|
|
128
|
-
payload = { query: query }
|
|
129
|
-
payload[:variables] = variables unless variables.nil?
|
|
130
|
-
payload[:operationName] = operation_name unless operation_name.nil?
|
|
131
|
-
|
|
132
|
-
ws = websocket(path)
|
|
133
|
-
ws.send_json({ 'type' => 'connection_init' })
|
|
134
|
-
|
|
135
|
-
acknowledged = false
|
|
136
|
-
GRAPHQL_WS_MAX_CONTROL_MESSAGES.times do
|
|
137
|
-
message = websocket_protocol_message(ws.receive_json)
|
|
138
|
-
message_type = websocket_field(message, :type)
|
|
139
|
-
|
|
140
|
-
if message_type == 'connection_ack'
|
|
141
|
-
acknowledged = true
|
|
142
|
-
break
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
if message_type == 'ping'
|
|
146
|
-
pong = { 'type' => 'pong' }
|
|
147
|
-
pong['payload'] = websocket_field(message, :payload) if message.key?('payload') || message.key?(:payload)
|
|
148
|
-
ws.send_json(pong)
|
|
149
|
-
next
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
if %w[connection_error error].include?(message_type)
|
|
153
|
-
raise "GraphQL subscription rejected during init: #{message}"
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
raise 'No GraphQL connection_ack received' unless acknowledged
|
|
158
|
-
|
|
159
|
-
ws.send_json({
|
|
160
|
-
'id' => operation_id,
|
|
161
|
-
'type' => 'subscribe',
|
|
162
|
-
'payload' => payload
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
event = nil
|
|
166
|
-
errors = []
|
|
167
|
-
complete_received = false
|
|
168
|
-
|
|
169
|
-
GRAPHQL_WS_MAX_CONTROL_MESSAGES.times do
|
|
170
|
-
message = websocket_protocol_message(ws.receive_json)
|
|
171
|
-
message_type = websocket_field(message, :type)
|
|
172
|
-
message_id = websocket_field(message, :id)
|
|
173
|
-
id_matches = message_id.nil? || message_id == operation_id
|
|
174
|
-
|
|
175
|
-
if message_type == 'next' && id_matches
|
|
176
|
-
event = websocket_field(message, :payload)
|
|
177
|
-
ws.send_json({ 'id' => operation_id, 'type' => 'complete' })
|
|
178
|
-
|
|
179
|
-
begin
|
|
180
|
-
maybe_complete = websocket_protocol_message(ws.receive_json)
|
|
181
|
-
complete_type = websocket_field(maybe_complete, :type)
|
|
182
|
-
complete_id = websocket_field(maybe_complete, :id)
|
|
183
|
-
complete_received = complete_type == 'complete' && (complete_id.nil? || complete_id == operation_id)
|
|
184
|
-
rescue StandardError
|
|
185
|
-
# Some servers close immediately after client complete.
|
|
186
|
-
end
|
|
187
|
-
break
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
if message_type == 'error'
|
|
191
|
-
errors << (websocket_field(message, :payload) || message)
|
|
192
|
-
break
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
if message_type == 'complete' && id_matches
|
|
196
|
-
complete_received = true
|
|
197
|
-
break
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
next unless message_type == 'ping'
|
|
201
|
-
|
|
202
|
-
pong = { 'type' => 'pong' }
|
|
203
|
-
pong['payload'] = websocket_field(message, :payload) if message.key?('payload') || message.key?(:payload)
|
|
204
|
-
ws.send_json(pong)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
if event.nil? && errors.empty? && !complete_received
|
|
208
|
-
raise 'No GraphQL subscription event received before timeout'
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
{
|
|
212
|
-
'operation_id' => operation_id,
|
|
213
|
-
'acknowledged' => true,
|
|
214
|
-
'event' => event,
|
|
215
|
-
'errors' => errors,
|
|
216
|
-
'complete_received' => complete_received
|
|
217
|
-
}
|
|
218
|
-
ensure
|
|
219
|
-
ws&.close
|
|
220
|
-
end
|
|
221
|
-
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
222
|
-
# rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity
|
|
223
|
-
|
|
224
|
-
def close
|
|
225
|
-
return if @closed
|
|
226
|
-
|
|
227
|
-
@closed = true
|
|
228
|
-
@native.close
|
|
229
|
-
ensure
|
|
230
|
-
Spikard::Testing.unregister_test_client(self)
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
%w[get post put patch delete head options trace].each do |verb|
|
|
234
|
-
define_method(verb) do |path, headers = nil, body = nil, **options|
|
|
235
|
-
request(verb.upcase, path, headers, body, **options)
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def self.native_test_client_candidate?(candidate)
|
|
240
|
-
return true if candidate.is_a?(Spikard::Native::TestClient)
|
|
241
|
-
return false if candidate.respond_to?(:normalized_routes_json)
|
|
242
|
-
|
|
243
|
-
%i[request websocket sse close].any? { |method_name| candidate.respond_to?(method_name) }
|
|
244
|
-
end
|
|
245
|
-
private_class_method :native_test_client_candidate?
|
|
246
|
-
|
|
247
|
-
private
|
|
248
|
-
|
|
249
|
-
def build_request_payload(headers, body, options)
|
|
250
|
-
payload = {}
|
|
251
|
-
headers = options.delete(:headers) || headers
|
|
252
|
-
cookies = options.delete(:cookies)
|
|
253
|
-
query = options.delete(:query) || options.delete(:params)
|
|
254
|
-
|
|
255
|
-
payload[:headers] = headers if headers
|
|
256
|
-
payload[:cookies] = cookies if cookies
|
|
257
|
-
payload[:query] = query if query
|
|
258
|
-
payload.merge!(body_payload_from(options, body))
|
|
259
|
-
payload
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def body_payload_from(options, body)
|
|
263
|
-
json = options.delete(:json)
|
|
264
|
-
data = options.delete(:data)
|
|
265
|
-
raw_body = options.delete(:raw_body)
|
|
266
|
-
files = options.delete(:files)
|
|
267
|
-
body_option = options.delete(:body)
|
|
268
|
-
|
|
269
|
-
return explicit_body_payload(json, data, raw_body, files) if json || data || raw_body || files
|
|
270
|
-
|
|
271
|
-
body_value = body_option.nil? ? body : body_option
|
|
272
|
-
body_value.nil? ? {} : { json: body_value }
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def explicit_body_payload(json, data, raw_body, files)
|
|
276
|
-
payload = {}
|
|
277
|
-
payload[:json] = json if json
|
|
278
|
-
payload[:data] = data if data
|
|
279
|
-
payload[:raw_body] = raw_body if raw_body
|
|
280
|
-
payload[:files] = files if files
|
|
281
|
-
payload
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def websocket_protocol_message(raw)
|
|
285
|
-
case raw
|
|
286
|
-
when String
|
|
287
|
-
parsed = JSON.parse(raw)
|
|
288
|
-
raise 'Expected GraphQL WebSocket JSON object message' unless parsed.is_a?(Hash)
|
|
289
|
-
|
|
290
|
-
parsed
|
|
291
|
-
when Hash
|
|
292
|
-
raw
|
|
293
|
-
else
|
|
294
|
-
raise "Expected GraphQL WebSocket message object, got #{raw.class}"
|
|
295
|
-
end
|
|
296
|
-
rescue JSON::ParserError => e
|
|
297
|
-
raise "Invalid GraphQL WebSocket message: #{e.message}"
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def websocket_field(message, key)
|
|
301
|
-
message[key.to_s] || message[key.to_sym]
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
# rubocop:enable Metrics/ClassLength
|
|
305
|
-
|
|
306
|
-
# WebSocket test connection wrapper
|
|
307
|
-
class WebSocketTestConnection
|
|
308
|
-
def initialize(native_ws)
|
|
309
|
-
@native_ws = native_ws
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def send_text(text)
|
|
313
|
-
Testing.trace('websocket:send_text')
|
|
314
|
-
@native_ws.send_text(JSON.generate(text))
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def send_json(obj)
|
|
318
|
-
Testing.trace('websocket:send_json')
|
|
319
|
-
@native_ws.send_json(obj)
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def receive_text
|
|
323
|
-
Testing.trace('websocket:receive_text')
|
|
324
|
-
raw = with_timeout { @native_ws.receive_text }
|
|
325
|
-
JSON.parse(raw)
|
|
326
|
-
rescue JSON::ParserError
|
|
327
|
-
raw
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def receive_json
|
|
331
|
-
Testing.trace('websocket:receive_json')
|
|
332
|
-
with_timeout { @native_ws.receive_json }
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def receive_bytes
|
|
336
|
-
receive_text
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def receive_message
|
|
340
|
-
native_msg = @native_ws.receive_message
|
|
341
|
-
WebSocketMessage.new(native_msg)
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
def close
|
|
345
|
-
Testing.trace('websocket:close')
|
|
346
|
-
@native_ws.close
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
private
|
|
350
|
-
|
|
351
|
-
def with_timeout(&)
|
|
352
|
-
timeout_ms = ENV.fetch('SPIKARD_RB_TEST_TIMEOUT_MS', nil)
|
|
353
|
-
return yield unless timeout_ms
|
|
354
|
-
|
|
355
|
-
Timeout.timeout(timeout_ms.to_f / 1000.0, &)
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
# WebSocket message wrapper
|
|
360
|
-
class WebSocketMessage
|
|
361
|
-
def initialize(native_msg)
|
|
362
|
-
@native_msg = native_msg
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
def as_text
|
|
366
|
-
raw = @native_msg.as_text
|
|
367
|
-
return unless raw
|
|
368
|
-
|
|
369
|
-
JSON.parse(raw)
|
|
370
|
-
rescue JSON::ParserError
|
|
371
|
-
raw
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def as_json
|
|
375
|
-
@native_msg.as_json
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
def as_binary
|
|
379
|
-
@native_msg.as_binary
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
def close?
|
|
383
|
-
@native_msg.is_close
|
|
384
|
-
end
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
# SSE stream wrapper
|
|
388
|
-
class SseStream
|
|
389
|
-
def initialize(native_sse)
|
|
390
|
-
@native_sse = native_sse
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def body
|
|
394
|
-
@native_sse.body
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
def events
|
|
398
|
-
parsed_chunks.map { |chunk| InlineSseEvent.new(chunk) }
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
def events_as_json
|
|
402
|
-
parsed_chunks.filter_map do |chunk|
|
|
403
|
-
JSON.parse(chunk)
|
|
404
|
-
rescue JSON::ParserError
|
|
405
|
-
nil
|
|
406
|
-
end
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
private
|
|
410
|
-
|
|
411
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
412
|
-
def parsed_chunks
|
|
413
|
-
raw = body.to_s.gsub("\r\n", "\n")
|
|
414
|
-
events = []
|
|
415
|
-
current = []
|
|
416
|
-
|
|
417
|
-
raw.each_line do |line|
|
|
418
|
-
stripped = line.chomp
|
|
419
|
-
if stripped.start_with?('data:')
|
|
420
|
-
current << stripped[5..].strip
|
|
421
|
-
elsif stripped.empty?
|
|
422
|
-
unless current.empty?
|
|
423
|
-
data = current.join("\n").strip
|
|
424
|
-
events << data unless data.empty?
|
|
425
|
-
current = []
|
|
426
|
-
end
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
unless current.empty?
|
|
431
|
-
data = current.join("\n").strip
|
|
432
|
-
events << data unless data.empty?
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
events
|
|
436
|
-
end
|
|
437
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
# SSE event wrapper
|
|
441
|
-
class SseEvent
|
|
442
|
-
def initialize(native_event)
|
|
443
|
-
@native_event = native_event
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
def data
|
|
447
|
-
@native_event.data
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
def as_json
|
|
451
|
-
@native_event.as_json
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
# Lightweight wrapper for parsed SSE events backed by strings.
|
|
456
|
-
class InlineSseEvent
|
|
457
|
-
attr_reader :data
|
|
458
|
-
|
|
459
|
-
def initialize(data)
|
|
460
|
-
@data = data
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
def as_json
|
|
464
|
-
JSON.parse(@data)
|
|
465
|
-
end
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
def trace(message)
|
|
469
|
-
return unless ENV['SPIKARD_RB_TEST_TRACE'] == '1'
|
|
470
|
-
|
|
471
|
-
warn("[spikard-rb-test] #{message}")
|
|
472
|
-
end
|
|
473
|
-
end
|
|
474
|
-
end
|
data/lib/spikard/upload_file.rb
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'stringio'
|
|
4
|
-
require 'base64'
|
|
5
|
-
|
|
6
|
-
module Spikard
|
|
7
|
-
# File upload handling for multipart/form-data requests
|
|
8
|
-
#
|
|
9
|
-
# This class provides an interface for handling file uploads,
|
|
10
|
-
# designed to be compatible with Rails patterns while optimized
|
|
11
|
-
# for Spikard's Rust-backed request processing.
|
|
12
|
-
#
|
|
13
|
-
# @example
|
|
14
|
-
# app.post('/upload') do |body|
|
|
15
|
-
# file = body[:file] # UploadFile instance
|
|
16
|
-
# content = file.read
|
|
17
|
-
# {
|
|
18
|
-
# filename: file.filename,
|
|
19
|
-
# size: file.size,
|
|
20
|
-
# content_type: file.content_type,
|
|
21
|
-
# description: body[:description]
|
|
22
|
-
# }
|
|
23
|
-
# end
|
|
24
|
-
class UploadFile
|
|
25
|
-
# @return [String] Original filename from the client
|
|
26
|
-
attr_reader :filename
|
|
27
|
-
|
|
28
|
-
# @return [String] MIME type of the uploaded file
|
|
29
|
-
attr_reader :content_type
|
|
30
|
-
|
|
31
|
-
# @return [Integer] Size of the file in bytes
|
|
32
|
-
attr_reader :size
|
|
33
|
-
|
|
34
|
-
# @return [Hash<String, String>] Additional headers associated with this file field
|
|
35
|
-
attr_reader :headers
|
|
36
|
-
|
|
37
|
-
# Create a new UploadFile instance
|
|
38
|
-
#
|
|
39
|
-
# @param filename [String] Original filename from the client
|
|
40
|
-
# @param content [String] File contents (may be base64 encoded)
|
|
41
|
-
# @param content_type [String, nil] MIME type (defaults to "application/octet-stream")
|
|
42
|
-
# @param size [Integer, nil] File size in bytes (computed from content if not provided)
|
|
43
|
-
# @param headers [Hash<String, String>, nil] Additional headers from the multipart field
|
|
44
|
-
# @param content_encoding [String, nil] Encoding type (e.g., "base64")
|
|
45
|
-
def initialize(filename, content, content_type: nil, size: nil, headers: nil, content_encoding: nil)
|
|
46
|
-
@filename = filename
|
|
47
|
-
@content_type = content_type || 'application/octet-stream'
|
|
48
|
-
@headers = headers || {}
|
|
49
|
-
|
|
50
|
-
# Decode content if base64 encoded
|
|
51
|
-
@content = if content_encoding == 'base64' || base64_encoded?(content)
|
|
52
|
-
Base64.decode64(content)
|
|
53
|
-
else
|
|
54
|
-
content
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
@size = size || @content.bytesize
|
|
58
|
-
@io = StringIO.new(@content)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Read file contents
|
|
62
|
-
#
|
|
63
|
-
# @param size [Integer, nil] Number of bytes to read (nil for all remaining)
|
|
64
|
-
# @return [String] File contents
|
|
65
|
-
def read(size = nil)
|
|
66
|
-
@io.read(size)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Read file contents as text
|
|
70
|
-
#
|
|
71
|
-
# @param encoding [String] Character encoding (defaults to UTF-8)
|
|
72
|
-
# @return [String] File contents as text
|
|
73
|
-
def text(encoding: 'UTF-8')
|
|
74
|
-
@content.force_encoding(encoding)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Seek to a specific position in the file
|
|
78
|
-
#
|
|
79
|
-
# @param offset [Integer] Byte offset
|
|
80
|
-
# @param whence [Integer] Position reference (IO::SEEK_SET, IO::SEEK_CUR, IO::SEEK_END)
|
|
81
|
-
# @return [Integer] New position
|
|
82
|
-
def seek(offset, whence = IO::SEEK_SET)
|
|
83
|
-
@io.seek(offset, whence)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Get current position in the file
|
|
87
|
-
#
|
|
88
|
-
# @return [Integer] Current byte offset
|
|
89
|
-
def tell
|
|
90
|
-
@io.tell
|
|
91
|
-
end
|
|
92
|
-
alias pos tell
|
|
93
|
-
|
|
94
|
-
# Rewind to the beginning of the file
|
|
95
|
-
#
|
|
96
|
-
# @return [Integer] Always returns 0
|
|
97
|
-
def rewind
|
|
98
|
-
@io.rewind
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Close the file (no-op for StringIO-based implementation)
|
|
102
|
-
#
|
|
103
|
-
# @return [nil]
|
|
104
|
-
def close
|
|
105
|
-
@io.close
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Check if file is closed
|
|
109
|
-
#
|
|
110
|
-
# @return [Boolean]
|
|
111
|
-
def closed?
|
|
112
|
-
@io.closed?
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Get the raw content as a string
|
|
116
|
-
#
|
|
117
|
-
# @return [String] Raw file content
|
|
118
|
-
attr_reader :content
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
# Check if a string appears to be base64 encoded
|
|
123
|
-
#
|
|
124
|
-
# @param str [String] String to check
|
|
125
|
-
# @return [Boolean]
|
|
126
|
-
def base64_encoded?(str)
|
|
127
|
-
# Simple heuristic: check if string matches base64 pattern
|
|
128
|
-
str.is_a?(String) && str.match?(%r{\A[A-Za-z0-9+/]*={0,2}\z})
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
data/lib/spikard/websocket.rb
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Spikard
|
|
4
|
-
# Base class for WebSocket message handlers.
|
|
5
|
-
#
|
|
6
|
-
# Implement this class to handle WebSocket connections and messages.
|
|
7
|
-
#
|
|
8
|
-
# @example
|
|
9
|
-
# class ChatHandler < Spikard::WebSocketHandler
|
|
10
|
-
# def handle_message(message)
|
|
11
|
-
# # Echo message back
|
|
12
|
-
# message
|
|
13
|
-
# end
|
|
14
|
-
#
|
|
15
|
-
# def on_connect
|
|
16
|
-
# puts "Client connected"
|
|
17
|
-
# end
|
|
18
|
-
#
|
|
19
|
-
# def on_disconnect
|
|
20
|
-
# puts "Client disconnected"
|
|
21
|
-
# end
|
|
22
|
-
# end
|
|
23
|
-
#
|
|
24
|
-
# app = Spikard::App.new
|
|
25
|
-
#
|
|
26
|
-
# app.websocket('/chat') do
|
|
27
|
-
# ChatHandler.new
|
|
28
|
-
# end
|
|
29
|
-
#
|
|
30
|
-
# app.run
|
|
31
|
-
class WebSocketHandler
|
|
32
|
-
# Handle an incoming WebSocket message.
|
|
33
|
-
#
|
|
34
|
-
# @param message [Hash] Parsed JSON message from the client
|
|
35
|
-
# @return [Hash, nil] Optional response message to send back to the client.
|
|
36
|
-
# Return nil to not send a response.
|
|
37
|
-
def handle_message(message)
|
|
38
|
-
raise NotImplementedError, "#{self.class.name} must implement #handle_message"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Called when a client connects.
|
|
42
|
-
#
|
|
43
|
-
# Override this method to perform initialization when a client connects.
|
|
44
|
-
#
|
|
45
|
-
# @return [void]
|
|
46
|
-
def on_connect
|
|
47
|
-
# Optional hook - default implementation does nothing
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Called when a client disconnects.
|
|
51
|
-
#
|
|
52
|
-
# Override this method to perform cleanup when a client disconnects.
|
|
53
|
-
#
|
|
54
|
-
# @return [void]
|
|
55
|
-
def on_disconnect
|
|
56
|
-
# Optional hook - default implementation does nothing
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|