spikard 0.12.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.
Files changed (206) hide show
  1. checksums.yaml +4 -4
  2. data/Steepfile +6 -0
  3. data/ext/spikard_rb/extconf.rb +1 -2
  4. data/ext/spikard_rb/{Cargo.lock → native/Cargo.lock} +897 -451
  5. data/ext/spikard_rb/native/Cargo.toml +24 -0
  6. data/ext/spikard_rb/src/lib.rs +5366 -3
  7. data/lib/spikard/native.rb +86 -0
  8. data/lib/spikard/version.rb +6 -1
  9. data/lib/spikard.rb +8 -45
  10. data/lib/spikard_rb.so +0 -0
  11. data/sig/types.rbs +427 -0
  12. metadata +14 -242
  13. data/LICENSE +0 -1
  14. data/README.md +0 -267
  15. data/ext/spikard_rb/Cargo.toml +0 -17
  16. data/lib/spikard/app.rb +0 -428
  17. data/lib/spikard/background.rb +0 -58
  18. data/lib/spikard/config.rb +0 -506
  19. data/lib/spikard/converters.rb +0 -13
  20. data/lib/spikard/grpc.rb +0 -182
  21. data/lib/spikard/handler_wrapper.rb +0 -113
  22. data/lib/spikard/provide.rb +0 -214
  23. data/lib/spikard/response.rb +0 -173
  24. data/lib/spikard/schema.rb +0 -243
  25. data/lib/spikard/sse.rb +0 -111
  26. data/lib/spikard/streaming_response.rb +0 -44
  27. data/lib/spikard/testing.rb +0 -432
  28. data/lib/spikard/upload_file.rb +0 -131
  29. data/lib/spikard/websocket.rb +0 -59
  30. data/sig/spikard.rbs +0 -719
  31. data/vendor/crates/spikard-bindings-shared/Cargo.toml +0 -80
  32. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +0 -132
  33. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +0 -905
  34. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +0 -210
  35. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +0 -252
  36. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +0 -404
  37. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +0 -199
  38. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +0 -252
  39. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +0 -829
  40. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +0 -587
  41. data/vendor/crates/spikard-bindings-shared/src/lib.rs +0 -33
  42. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +0 -298
  43. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +0 -594
  44. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +0 -743
  45. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +0 -944
  46. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +0 -260
  47. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +0 -369
  48. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +0 -192
  49. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +0 -383
  50. data/vendor/crates/spikard-bindings-shared/tests/full_coverage.rs +0 -459
  51. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +0 -280
  52. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +0 -669
  53. data/vendor/crates/spikard-core/Cargo.toml +0 -60
  54. data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
  55. data/vendor/crates/spikard-core/src/bindings/response.rs +0 -130
  56. data/vendor/crates/spikard-core/src/debug.rs +0 -127
  57. data/vendor/crates/spikard-core/src/di/container.rs +0 -702
  58. data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
  59. data/vendor/crates/spikard-core/src/di/error.rs +0 -118
  60. data/vendor/crates/spikard-core/src/di/factory.rs +0 -538
  61. data/vendor/crates/spikard-core/src/di/graph.rs +0 -507
  62. data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
  63. data/vendor/crates/spikard-core/src/di/resolved.rs +0 -428
  64. data/vendor/crates/spikard-core/src/di/value.rs +0 -282
  65. data/vendor/crates/spikard-core/src/errors.rs +0 -72
  66. data/vendor/crates/spikard-core/src/http.rs +0 -492
  67. data/vendor/crates/spikard-core/src/lib.rs +0 -29
  68. data/vendor/crates/spikard-core/src/lifecycle.rs +0 -1273
  69. data/vendor/crates/spikard-core/src/metadata.rs +0 -378
  70. data/vendor/crates/spikard-core/src/parameters.rs +0 -2546
  71. data/vendor/crates/spikard-core/src/problem.rs +0 -358
  72. data/vendor/crates/spikard-core/src/request_data.rs +0 -1146
  73. data/vendor/crates/spikard-core/src/router.rs +0 -530
  74. data/vendor/crates/spikard-core/src/schema_registry.rs +0 -197
  75. data/vendor/crates/spikard-core/src/type_hints.rs +0 -311
  76. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +0 -710
  77. data/vendor/crates/spikard-core/src/validation/mod.rs +0 -470
  78. data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +0 -136
  79. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +0 -37
  80. data/vendor/crates/spikard-core/tests/error_mapper.rs +0 -761
  81. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +0 -106
  82. data/vendor/crates/spikard-core/tests/parameters_full.rs +0 -701
  83. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +0 -301
  84. data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +0 -67
  85. data/vendor/crates/spikard-core/tests/validation_coverage.rs +0 -250
  86. data/vendor/crates/spikard-core/tests/validation_error_paths.rs +0 -45
  87. data/vendor/crates/spikard-http/Cargo.toml +0 -87
  88. data/vendor/crates/spikard-http/examples/sse-notifications.rs +0 -148
  89. data/vendor/crates/spikard-http/examples/websocket-chat.rs +0 -92
  90. data/vendor/crates/spikard-http/src/auth.rs +0 -301
  91. data/vendor/crates/spikard-http/src/background.rs +0 -1860
  92. data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
  93. data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
  94. data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
  95. data/vendor/crates/spikard-http/src/cors.rs +0 -1026
  96. data/vendor/crates/spikard-http/src/debug.rs +0 -128
  97. data/vendor/crates/spikard-http/src/di_handler.rs +0 -1672
  98. data/vendor/crates/spikard-http/src/grpc/framing.rs +0 -469
  99. data/vendor/crates/spikard-http/src/grpc/handler.rs +0 -1122
  100. data/vendor/crates/spikard-http/src/grpc/mod.rs +0 -434
  101. data/vendor/crates/spikard-http/src/grpc/service.rs +0 -622
  102. data/vendor/crates/spikard-http/src/grpc/streaming.rs +0 -319
  103. data/vendor/crates/spikard-http/src/handler_response.rs +0 -901
  104. data/vendor/crates/spikard-http/src/handler_trait.rs +0 -1015
  105. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -290
  106. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +0 -502
  107. data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +0 -648
  108. data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +0 -58
  109. data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +0 -1207
  110. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +0 -2262
  111. data/vendor/crates/spikard-http/src/lib.rs +0 -548
  112. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -230
  113. data/vendor/crates/spikard-http/src/lifecycle.rs +0 -1193
  114. data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -560
  115. data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -912
  116. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -513
  117. data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -768
  118. data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
  119. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -535
  120. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -1363
  121. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -667
  122. data/vendor/crates/spikard-http/src/query_parser.rs +0 -793
  123. data/vendor/crates/spikard-http/src/response.rs +0 -720
  124. data/vendor/crates/spikard-http/src/server/fast_router.rs +0 -186
  125. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +0 -858
  126. data/vendor/crates/spikard-http/src/server/handler.rs +0 -1661
  127. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -253
  128. data/vendor/crates/spikard-http/src/server/mod.rs +0 -1649
  129. data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -871
  130. data/vendor/crates/spikard-http/src/server/routing_factory.rs +0 -618
  131. data/vendor/crates/spikard-http/src/sse.rs +0 -1409
  132. data/vendor/crates/spikard-http/src/testing/form.rs +0 -52
  133. data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -64
  134. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -787
  135. data/vendor/crates/spikard-http/src/testing.rs +0 -617
  136. data/vendor/crates/spikard-http/src/websocket.rs +0 -1477
  137. data/vendor/crates/spikard-http/tests/auth_integration.rs +0 -645
  138. data/vendor/crates/spikard-http/tests/background_behavior.rs +0 -832
  139. data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +0 -1012
  140. data/vendor/crates/spikard-http/tests/common/handlers.rs +0 -309
  141. data/vendor/crates/spikard-http/tests/common/mod.rs +0 -33
  142. data/vendor/crates/spikard-http/tests/common/test_builders.rs +0 -628
  143. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +0 -162
  144. data/vendor/crates/spikard-http/tests/di_integration.rs +0 -192
  145. data/vendor/crates/spikard-http/tests/doc_snippets.rs +0 -5
  146. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +0 -430
  147. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +0 -738
  148. data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +0 -652
  149. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +0 -334
  150. data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +0 -532
  151. data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +0 -495
  152. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +0 -974
  153. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +0 -1093
  154. data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +0 -389
  155. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +0 -656
  156. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +0 -513
  157. data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +0 -328
  158. data/vendor/crates/spikard-http/tests/server_config_builder.rs +0 -314
  159. data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +0 -200
  160. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +0 -83
  161. data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +0 -464
  162. data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +0 -286
  163. data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +0 -118
  164. data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +0 -99
  165. data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +0 -204
  166. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +0 -421
  167. data/vendor/crates/spikard-http/tests/server_router_behavior.rs +0 -121
  168. data/vendor/crates/spikard-http/tests/sse_behavior.rs +0 -620
  169. data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +0 -584
  170. data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +0 -130
  171. data/vendor/crates/spikard-http/tests/test_client_requests.rs +0 -167
  172. data/vendor/crates/spikard-http/tests/testing_helpers.rs +0 -87
  173. data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +0 -155
  174. data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +0 -82
  175. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +0 -663
  176. data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +0 -440
  177. data/vendor/crates/spikard-http/tests/websocket_integration.rs +0 -150
  178. data/vendor/crates/spikard-rb/Cargo.toml +0 -68
  179. data/vendor/crates/spikard-rb/build.rs +0 -200
  180. data/vendor/crates/spikard-rb/src/background.rs +0 -63
  181. data/vendor/crates/spikard-rb/src/config/mod.rs +0 -5
  182. data/vendor/crates/spikard-rb/src/config/server_config.rs +0 -401
  183. data/vendor/crates/spikard-rb/src/conversion.rs +0 -688
  184. data/vendor/crates/spikard-rb/src/di/builder.rs +0 -100
  185. data/vendor/crates/spikard-rb/src/di/mod.rs +0 -375
  186. data/vendor/crates/spikard-rb/src/grpc/handler.rs +0 -834
  187. data/vendor/crates/spikard-rb/src/grpc/mod.rs +0 -13
  188. data/vendor/crates/spikard-rb/src/gvl.rs +0 -80
  189. data/vendor/crates/spikard-rb/src/handler.rs +0 -699
  190. data/vendor/crates/spikard-rb/src/integration/mod.rs +0 -3
  191. data/vendor/crates/spikard-rb/src/lib.rs +0 -2264
  192. data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -303
  193. data/vendor/crates/spikard-rb/src/metadata/mod.rs +0 -5
  194. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +0 -507
  195. data/vendor/crates/spikard-rb/src/request.rs +0 -439
  196. data/vendor/crates/spikard-rb/src/runtime/mod.rs +0 -5
  197. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +0 -344
  198. data/vendor/crates/spikard-rb/src/server.rs +0 -307
  199. data/vendor/crates/spikard-rb/src/sse.rs +0 -231
  200. data/vendor/crates/spikard-rb/src/testing/client.rs +0 -698
  201. data/vendor/crates/spikard-rb/src/testing/mod.rs +0 -7
  202. data/vendor/crates/spikard-rb/src/testing/sse.rs +0 -108
  203. data/vendor/crates/spikard-rb/src/testing/websocket.rs +0 -573
  204. data/vendor/crates/spikard-rb/src/websocket.rs +0 -475
  205. data/vendor/crates/spikard-rb-macros/Cargo.toml +0 -25
  206. data/vendor/crates/spikard-rb-macros/src/lib.rs +0 -51
@@ -1,432 +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
-
11
- module_function
12
-
13
- def create_test_client(app, config: nil)
14
- trace('create_test_client:start')
15
- ensure_native_test_client!
16
- config = resolve_test_config(app, config)
17
- native = build_native_test_client(app, config)
18
- trace('create_test_client:done')
19
- TestClient.new(native)
20
- end
21
-
22
- def ensure_native_test_client!
23
- return if defined?(Spikard::Native::TestClient)
24
-
25
- raise LoadError, 'Spikard native test client is not available. Build the native extension before running tests.'
26
- end
27
-
28
- def resolve_test_config(app, config)
29
- return config if config
30
-
31
- if app.instance_variable_defined?(:@__spikard_test_config)
32
- return app.instance_variable_get(:@__spikard_test_config)
33
- end
34
-
35
- Spikard::ServerConfig.new
36
- end
37
-
38
- def build_native_test_client(app, config)
39
- routes_json = app.normalized_routes_json
40
- handlers = app.handler_map.transform_keys(&:to_sym)
41
- ws_handlers = app.websocket_handlers || {}
42
- sse_producers = app.sse_producers || {}
43
- hooks = app.instance_variable_get(:@native_hooks)
44
- dependencies = app.dependencies || {}
45
- payload = { hooks: hooks, dependencies: dependencies }
46
- Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers, payload)
47
- end
48
-
49
- # High level wrapper around the native test client.
50
- # rubocop:disable Metrics/ClassLength
51
- class TestClient
52
- def initialize(native)
53
- @native = native
54
- end
55
-
56
- # Factory method for creating test client from an app
57
- def self.new(app_or_native, config: nil)
58
- # If passed a native client directly, use it
59
- return super(app_or_native) if app_or_native.is_a?(Spikard::Native::TestClient)
60
-
61
- # Otherwise, create test client from app
62
- Spikard::Testing.create_test_client(app_or_native, config: config)
63
- end
64
-
65
- def request(method, path, headers = nil, body = nil, **options)
66
- payload = build_request_payload(headers, body, options)
67
- payload = @native.request(method.to_s.upcase, path, payload)
68
- Response.new(payload)
69
- end
70
-
71
- def websocket(path)
72
- Testing.trace("websocket:start #{path}")
73
- native_ws = @native.websocket(path)
74
- Testing.trace("websocket:connected #{path}")
75
- WebSocketTestConnection.new(native_ws)
76
- end
77
-
78
- def sse(path)
79
- native_sse = @native.sse(path)
80
- SseStream.new(native_sse)
81
- end
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
-
195
- def close
196
- @native.close
197
- end
198
-
199
- %w[get post put patch delete head options trace].each do |verb|
200
- define_method(verb) do |path, headers = nil, body = nil, **options|
201
- request(verb.upcase, path, headers, body, **options)
202
- end
203
- end
204
-
205
- private
206
-
207
- def build_request_payload(headers, body, options)
208
- payload = {}
209
- headers = options.delete(:headers) || headers
210
- cookies = options.delete(:cookies)
211
- query = options.delete(:query) || options.delete(:params)
212
-
213
- payload[:headers] = headers if headers
214
- payload[:cookies] = cookies if cookies
215
- payload[:query] = query if query
216
- payload.merge!(body_payload_from(options, body))
217
- payload
218
- end
219
-
220
- def body_payload_from(options, body)
221
- json = options.delete(:json)
222
- data = options.delete(:data)
223
- raw_body = options.delete(:raw_body)
224
- files = options.delete(:files)
225
- body_option = options.delete(:body)
226
-
227
- return explicit_body_payload(json, data, raw_body, files) if json || data || raw_body || files
228
-
229
- body_value = body_option.nil? ? body : body_option
230
- body_value.nil? ? {} : { json: body_value }
231
- end
232
-
233
- def explicit_body_payload(json, data, raw_body, files)
234
- payload = {}
235
- payload[:json] = json if json
236
- payload[:data] = data if data
237
- payload[:raw_body] = raw_body if raw_body
238
- payload[:files] = files if files
239
- payload
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
261
- end
262
- # rubocop:enable Metrics/ClassLength
263
-
264
- # WebSocket test connection wrapper
265
- class WebSocketTestConnection
266
- def initialize(native_ws)
267
- @native_ws = native_ws
268
- end
269
-
270
- def send_text(text)
271
- Testing.trace('websocket:send_text')
272
- @native_ws.send_text(JSON.generate(text))
273
- end
274
-
275
- def send_json(obj)
276
- Testing.trace('websocket:send_json')
277
- @native_ws.send_json(obj)
278
- end
279
-
280
- def receive_text
281
- Testing.trace('websocket:receive_text')
282
- raw = with_timeout { @native_ws.receive_text }
283
- JSON.parse(raw)
284
- rescue JSON::ParserError
285
- raw
286
- end
287
-
288
- def receive_json
289
- Testing.trace('websocket:receive_json')
290
- with_timeout { @native_ws.receive_json }
291
- end
292
-
293
- def receive_bytes
294
- receive_text
295
- end
296
-
297
- def receive_message
298
- native_msg = @native_ws.receive_message
299
- WebSocketMessage.new(native_msg)
300
- end
301
-
302
- def close
303
- Testing.trace('websocket:close')
304
- @native_ws.close
305
- end
306
-
307
- private
308
-
309
- def with_timeout(&)
310
- timeout_ms = ENV.fetch('SPIKARD_RB_TEST_TIMEOUT_MS', nil)
311
- return yield unless timeout_ms
312
-
313
- Timeout.timeout(timeout_ms.to_f / 1000.0, &)
314
- end
315
- end
316
-
317
- # WebSocket message wrapper
318
- class WebSocketMessage
319
- def initialize(native_msg)
320
- @native_msg = native_msg
321
- end
322
-
323
- def as_text
324
- raw = @native_msg.as_text
325
- return unless raw
326
-
327
- JSON.parse(raw)
328
- rescue JSON::ParserError
329
- raw
330
- end
331
-
332
- def as_json
333
- @native_msg.as_json
334
- end
335
-
336
- def as_binary
337
- @native_msg.as_binary
338
- end
339
-
340
- def close?
341
- @native_msg.is_close
342
- end
343
- end
344
-
345
- # SSE stream wrapper
346
- class SseStream
347
- def initialize(native_sse)
348
- @native_sse = native_sse
349
- end
350
-
351
- def body
352
- @native_sse.body
353
- end
354
-
355
- def events
356
- parsed_chunks.map { |chunk| InlineSseEvent.new(chunk) }
357
- end
358
-
359
- def events_as_json
360
- parsed_chunks.filter_map do |chunk|
361
- JSON.parse(chunk)
362
- rescue JSON::ParserError
363
- nil
364
- end
365
- end
366
-
367
- private
368
-
369
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
370
- def parsed_chunks
371
- raw = body.to_s.gsub("\r\n", "\n")
372
- events = []
373
- current = []
374
-
375
- raw.each_line do |line|
376
- stripped = line.chomp
377
- if stripped.start_with?('data:')
378
- current << stripped[5..].strip
379
- elsif stripped.empty?
380
- unless current.empty?
381
- data = current.join("\n").strip
382
- events << data unless data.empty?
383
- current = []
384
- end
385
- end
386
- end
387
-
388
- unless current.empty?
389
- data = current.join("\n").strip
390
- events << data unless data.empty?
391
- end
392
-
393
- events
394
- end
395
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
396
- end
397
-
398
- # SSE event wrapper
399
- class SseEvent
400
- def initialize(native_event)
401
- @native_event = native_event
402
- end
403
-
404
- def data
405
- @native_event.data
406
- end
407
-
408
- def as_json
409
- @native_event.as_json
410
- end
411
- end
412
-
413
- # Lightweight wrapper for parsed SSE events backed by strings.
414
- class InlineSseEvent
415
- attr_reader :data
416
-
417
- def initialize(data)
418
- @data = data
419
- end
420
-
421
- def as_json
422
- JSON.parse(@data)
423
- end
424
- end
425
-
426
- def trace(message)
427
- return unless ENV['SPIKARD_RB_TEST_TRACE'] == '1'
428
-
429
- warn("[spikard-rb-test] #{message}")
430
- end
431
- end
432
- end
@@ -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
@@ -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