spikard 0.13.0 → 0.15.3

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 (207) 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} +818 -423
  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 -52
  10. data/lib/spikard_rb.so +0 -0
  11. data/sig/types.rbs +427 -0
  12. metadata +14 -243
  13. data/LICENSE +0 -1
  14. data/README.md +0 -285
  15. data/ext/spikard_rb/Cargo.toml +0 -17
  16. data/lib/spikard/app.rb +0 -458
  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 -232
  21. data/lib/spikard/handler_wrapper.rb +0 -113
  22. data/lib/spikard/provide.rb +0 -315
  23. data/lib/spikard/response.rb +0 -198
  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 -474
  28. data/lib/spikard/upload_file.rb +0 -131
  29. data/lib/spikard/websocket.rb +0 -59
  30. data/sig/spikard.rbs +0 -739
  31. data/vendor/crates/spikard-bindings-shared/Cargo.toml +0 -75
  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 -55
  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 -711
  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 -548
  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 -82
  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 -1859
  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 -653
  99. data/vendor/crates/spikard-http/src/grpc/handler.rs +0 -1211
  100. data/vendor/crates/spikard-http/src/grpc/mod.rs +0 -556
  101. data/vendor/crates/spikard-http/src/grpc/service.rs +0 -706
  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 -60
  109. data/vendor/crates/spikard-http/src/jsonrpc/openrpc.rs +0 -325
  110. data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +0 -1207
  111. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +0 -2262
  112. data/vendor/crates/spikard-http/src/lib.rs +0 -566
  113. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -230
  114. data/vendor/crates/spikard-http/src/lifecycle.rs +0 -1193
  115. data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -560
  116. data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -912
  117. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -513
  118. data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -768
  119. data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
  120. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -535
  121. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -1363
  122. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -667
  123. data/vendor/crates/spikard-http/src/query_parser.rs +0 -793
  124. data/vendor/crates/spikard-http/src/response.rs +0 -720
  125. data/vendor/crates/spikard-http/src/server/fast_router.rs +0 -186
  126. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +0 -1243
  127. data/vendor/crates/spikard-http/src/server/handler.rs +0 -1661
  128. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -253
  129. data/vendor/crates/spikard-http/src/server/mod.rs +0 -1717
  130. data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -871
  131. data/vendor/crates/spikard-http/src/server/routing_factory.rs +0 -618
  132. data/vendor/crates/spikard-http/src/sse.rs +0 -1409
  133. data/vendor/crates/spikard-http/src/testing/form.rs +0 -52
  134. data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -64
  135. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -825
  136. data/vendor/crates/spikard-http/src/testing.rs +0 -617
  137. data/vendor/crates/spikard-http/src/websocket.rs +0 -1477
  138. data/vendor/crates/spikard-http/tests/auth_integration.rs +0 -645
  139. data/vendor/crates/spikard-http/tests/background_behavior.rs +0 -832
  140. data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +0 -1012
  141. data/vendor/crates/spikard-http/tests/common/handlers.rs +0 -309
  142. data/vendor/crates/spikard-http/tests/common/mod.rs +0 -33
  143. data/vendor/crates/spikard-http/tests/common/test_builders.rs +0 -628
  144. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +0 -162
  145. data/vendor/crates/spikard-http/tests/di_integration.rs +0 -192
  146. data/vendor/crates/spikard-http/tests/doc_snippets.rs +0 -5
  147. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +0 -430
  148. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +0 -738
  149. data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +0 -652
  150. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +0 -334
  151. data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +0 -532
  152. data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +0 -495
  153. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +0 -975
  154. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +0 -1093
  155. data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +0 -389
  156. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +0 -656
  157. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +0 -513
  158. data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +0 -328
  159. data/vendor/crates/spikard-http/tests/server_config_builder.rs +0 -335
  160. data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +0 -374
  161. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +0 -83
  162. data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +0 -464
  163. data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +0 -286
  164. data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +0 -118
  165. data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +0 -99
  166. data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +0 -204
  167. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +0 -427
  168. data/vendor/crates/spikard-http/tests/server_router_behavior.rs +0 -121
  169. data/vendor/crates/spikard-http/tests/sse_behavior.rs +0 -620
  170. data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +0 -584
  171. data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +0 -130
  172. data/vendor/crates/spikard-http/tests/test_client_requests.rs +0 -167
  173. data/vendor/crates/spikard-http/tests/testing_helpers.rs +0 -87
  174. data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +0 -155
  175. data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +0 -82
  176. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +0 -663
  177. data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +0 -440
  178. data/vendor/crates/spikard-http/tests/websocket_integration.rs +0 -150
  179. data/vendor/crates/spikard-rb/Cargo.toml +0 -63
  180. data/vendor/crates/spikard-rb/build.rs +0 -200
  181. data/vendor/crates/spikard-rb/src/background.rs +0 -63
  182. data/vendor/crates/spikard-rb/src/config/mod.rs +0 -5
  183. data/vendor/crates/spikard-rb/src/config/server_config.rs +0 -401
  184. data/vendor/crates/spikard-rb/src/conversion.rs +0 -688
  185. data/vendor/crates/spikard-rb/src/di/builder.rs +0 -100
  186. data/vendor/crates/spikard-rb/src/di/mod.rs +0 -410
  187. data/vendor/crates/spikard-rb/src/grpc/handler.rs +0 -875
  188. data/vendor/crates/spikard-rb/src/grpc/mod.rs +0 -13
  189. data/vendor/crates/spikard-rb/src/gvl.rs +0 -80
  190. data/vendor/crates/spikard-rb/src/handler.rs +0 -699
  191. data/vendor/crates/spikard-rb/src/integration/mod.rs +0 -3
  192. data/vendor/crates/spikard-rb/src/lib.rs +0 -2268
  193. data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -334
  194. data/vendor/crates/spikard-rb/src/metadata/mod.rs +0 -5
  195. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +0 -507
  196. data/vendor/crates/spikard-rb/src/request.rs +0 -439
  197. data/vendor/crates/spikard-rb/src/runtime/mod.rs +0 -5
  198. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +0 -368
  199. data/vendor/crates/spikard-rb/src/server.rs +0 -304
  200. data/vendor/crates/spikard-rb/src/sse.rs +0 -231
  201. data/vendor/crates/spikard-rb/src/testing/client.rs +0 -698
  202. data/vendor/crates/spikard-rb/src/testing/mod.rs +0 -7
  203. data/vendor/crates/spikard-rb/src/testing/sse.rs +0 -108
  204. data/vendor/crates/spikard-rb/src/testing/websocket.rs +0 -573
  205. data/vendor/crates/spikard-rb/src/websocket.rs +0 -521
  206. data/vendor/crates/spikard-rb-macros/Cargo.toml +0 -20
  207. data/vendor/crates/spikard-rb-macros/src/lib.rs +0 -51
data/lib/spikard/app.rb DELETED
@@ -1,458 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spikard
4
- RouteEntry = Struct.new(:metadata, :handler)
5
-
6
- # Lifecycle hooks support for Spikard applications
7
- module LifecycleHooks
8
- # Register an onRequest lifecycle hook
9
- #
10
- # Runs before routing. Can inspect/modify the request or short-circuit with a response.
11
- #
12
- # @param hook [Proc] A proc that receives a request and returns either:
13
- # - The (possibly modified) request to continue processing
14
- # - A Response object to short-circuit the request pipeline
15
- # @return [Proc] The hook proc (for chaining)
16
- #
17
- # @example
18
- # app.on_request do |request|
19
- # puts "Request: #{request.method} #{request.path}"
20
- # request
21
- # end
22
- def on_request(&hook)
23
- native_hooks.add_on_request(hook)
24
- hook
25
- end
26
-
27
- # Register a preValidation lifecycle hook
28
- #
29
- # Runs after routing but before validation. Useful for rate limiting.
30
- #
31
- # @param hook [Proc] A proc that receives a request and returns either:
32
- # - The (possibly modified) request to continue processing
33
- # - A Response object to short-circuit the request pipeline
34
- # @return [Proc] The hook proc (for chaining)
35
- #
36
- # @example
37
- # app.pre_validation do |request|
38
- # if too_many_requests?
39
- # Spikard::Response.new(content: { error: "Rate limit exceeded" }, status_code: 429)
40
- # else
41
- # request
42
- # end
43
- # end
44
- def pre_validation(&hook)
45
- native_hooks.add_pre_validation(hook)
46
- hook
47
- end
48
-
49
- # Register a preHandler lifecycle hook
50
- #
51
- # Runs after validation but before the handler. Ideal for authentication/authorization.
52
- #
53
- # @param hook [Proc] A proc that receives a request and returns either:
54
- # - The (possibly modified) request to continue processing
55
- # - A Response object to short-circuit the request pipeline
56
- # @return [Proc] The hook proc (for chaining)
57
- #
58
- # @example
59
- # app.pre_handler do |request|
60
- # if invalid_token?(request.headers['Authorization'])
61
- # Spikard::Response.new(content: { error: "Unauthorized" }, status_code: 401)
62
- # else
63
- # request
64
- # end
65
- # end
66
- def pre_handler(&hook)
67
- native_hooks.add_pre_handler(hook)
68
- hook
69
- end
70
-
71
- # Register an onResponse lifecycle hook
72
- #
73
- # Runs after the handler executes. Can modify the response.
74
- #
75
- # @param hook [Proc] A proc that receives a response and returns the (possibly modified) response
76
- # @return [Proc] The hook proc (for chaining)
77
- #
78
- # @example
79
- # app.on_response do |response|
80
- # response.headers['X-Frame-Options'] = 'DENY'
81
- # response
82
- # end
83
- def on_response(&hook)
84
- native_hooks.add_on_response(hook)
85
- hook
86
- end
87
-
88
- # Register an onError lifecycle hook
89
- #
90
- # Runs when an error occurs. Can customize error responses.
91
- #
92
- # @param hook [Proc] A proc that receives an error response and returns a (possibly modified) response
93
- # @return [Proc] The hook proc (for chaining)
94
- #
95
- # @example
96
- # app.on_error do |response|
97
- # response.headers['Content-Type'] = 'application/json'
98
- # response
99
- # end
100
- def on_error(&hook)
101
- native_hooks.add_on_error(hook)
102
- hook
103
- end
104
-
105
- private
106
-
107
- def native_hooks
108
- raise 'Spikard native lifecycle registry unavailable' unless defined?(@native_hooks) && @native_hooks
109
-
110
- @native_hooks
111
- end
112
- end
113
-
114
- # Collects route metadata so the Rust engine can execute handlers.
115
- # rubocop:disable Metrics/ClassLength
116
- class App
117
- include LifecycleHooks
118
- include ProvideSupport
119
-
120
- HTTP_METHODS = %w[GET POST PUT PATCH DELETE OPTIONS HEAD TRACE].freeze
121
- SUPPORTED_OPTIONS = %i[request_schema response_schema parameter_schema file_params is_async cors
122
- body_param_name jsonrpc_method].freeze
123
-
124
- attr_reader :routes
125
-
126
- def initialize
127
- @routes = []
128
- @websocket_handlers = {}
129
- @sse_producers = {}
130
- @grpc_services = {}
131
- @native_hooks = Spikard::Native::LifecycleRegistry.new
132
- @native_dependencies = Spikard::Native::DependencyRegistry.new
133
- @named_handlers = {}
134
- end
135
-
136
- def register_route(method, path, handler_name: nil, **options, &block)
137
- method = method.to_s
138
- path = path.to_s
139
- handler_name = handler_name&.to_s
140
- handler = block || (handler_name && @named_handlers[handler_name])
141
- validate_route_arguments!(handler, handler_name, options)
142
- metadata = build_route_metadata_for(method, path, handler_name, options, handler)
143
-
144
- @routes << RouteEntry.new(metadata, handler)
145
- handler
146
- end
147
-
148
- HTTP_METHODS.each do |verb|
149
- define_method(verb.downcase) do |path, handler_name: nil, **options, &block|
150
- register_route(verb, path, handler_name: handler_name, **options, &block)
151
- end
152
- end
153
-
154
- def route_metadata
155
- @routes.map(&:metadata)
156
- end
157
-
158
- def handler_map
159
- map = {}
160
- @routes.each do |entry|
161
- name = entry.metadata[:handler_name]
162
- # Pass raw handler - DI resolution happens in Rust layer
163
- map[name] = entry.handler
164
- end
165
- map.merge!(@named_handlers)
166
- map
167
- end
168
-
169
- def handler(name, &block)
170
- raise ArgumentError, 'block required for handler' unless block
171
-
172
- handler_name = name.to_s
173
- @named_handlers[handler_name] = block
174
-
175
- @routes.each do |entry|
176
- next unless entry.metadata[:handler_name] == handler_name
177
-
178
- entry.handler = block
179
- next unless entry.metadata.is_a?(Hash)
180
-
181
- deps = extract_handler_dependencies(block)
182
- entry.metadata[:handler_dependencies] = deps unless deps.empty?
183
- end
184
-
185
- block
186
- end
187
-
188
- def normalized_routes_json
189
- json = JSON.generate(route_metadata)
190
- if defined?(Spikard::Native) && Spikard::Native.respond_to?(:normalize_route_metadata)
191
- Spikard::Native.normalize_route_metadata(json)
192
- else
193
- json
194
- end
195
- end
196
-
197
- def default_handler_name(method, path)
198
- normalized_path = path.gsub(/[^a-zA-Z0-9]+/, '_').gsub(/__+/, '_')
199
- # ReDoS mitigation: use bounded quantifier {1,100} instead of + to prevent
200
- # polynomial time complexity with excessive trailing underscores
201
- normalized_path = normalized_path.sub(/^_{1,100}/, '').sub(/_{1,100}$/, '')
202
- normalized_path = 'root' if normalized_path.empty?
203
- "#{method.to_s.downcase}_#{normalized_path}"
204
- end
205
-
206
- # Register a WebSocket endpoint
207
- #
208
- # @param path [String] URL path for the WebSocket endpoint
209
- # @yield Factory block that returns a WebSocketHandler instance
210
- # @return [Proc] The factory block (for chaining)
211
- #
212
- # @example
213
- # app.websocket('/chat') do
214
- # ChatHandler.new
215
- # end
216
- def websocket(path, _handler_name: nil, **_options, &factory)
217
- raise ArgumentError, 'block required for WebSocket handler factory' unless factory
218
-
219
- @websocket_handlers[path] = factory
220
- factory
221
- end
222
-
223
- # Register a Server-Sent Events endpoint
224
- #
225
- # @param path [String] URL path for the SSE endpoint
226
- # @yield Factory block that returns a SseEventProducer instance
227
- # @return [Proc] The factory block (for chaining)
228
- #
229
- # @example
230
- # app.sse('/notifications') do
231
- # NotificationProducer.new
232
- # end
233
- def sse(path, _handler_name: nil, **_options, &factory)
234
- raise ArgumentError, 'block required for SSE producer factory' unless factory
235
-
236
- @sse_producers[path] = factory
237
- factory
238
- end
239
-
240
- # Get all registered WebSocket handlers
241
- #
242
- # @return [Hash] Dictionary mapping paths to handler factory blocks
243
- def websocket_handlers
244
- @websocket_handlers.dup
245
- end
246
-
247
- # Get all registered SSE producers
248
- #
249
- # @return [Hash] Dictionary mapping paths to producer factory blocks
250
- def sse_producers
251
- @sse_producers.dup
252
- end
253
-
254
- # Register a unary gRPC service handler
255
- #
256
- # @param service_name [String] Fully qualified service name
257
- # @param handler [#handle_request, #call] gRPC handler implementation
258
- # @return [self]
259
- def add_grpc_service(service_name, handler)
260
- raise ArgumentError, 'service_name required' if service_name.nil? || service_name.empty?
261
- unless handler.respond_to?(:handle_request) || handler.respond_to?(:call)
262
- raise ArgumentError, 'handler must respond to #handle_request or #call'
263
- end
264
-
265
- @grpc_services[service_name] = handler
266
- self
267
- end
268
-
269
- # Mount all unary gRPC handlers from a registry
270
- #
271
- # @param service [Spikard::Grpc::Service] service registry
272
- # @return [self]
273
- def use_grpc(service)
274
- service.service_names.each do |service_name|
275
- handler = service.get_handler(service_name)
276
- add_grpc_service(service_name, handler) if handler
277
- end
278
- self
279
- end
280
-
281
- # Get all registered gRPC service handlers
282
- #
283
- # @return [Hash<String, Object>]
284
- def grpc_services
285
- @grpc_services.dup
286
- end
287
-
288
- # Run the Spikard server with the given configuration
289
- #
290
- # @param config [ServerConfig, Hash, nil] Server configuration
291
- # Can be a ServerConfig object, a Hash with configuration keys, or nil to use defaults.
292
- # If a Hash is provided, it will be converted to a ServerConfig.
293
- #
294
- # @example With ServerConfig
295
- # config = Spikard::ServerConfig.new(
296
- # host: '0.0.0.0',
297
- # port: 8080,
298
- # compression: Spikard::CompressionConfig.new(quality: 9)
299
- # )
300
- # app.run(config: config)
301
- #
302
- # @example With Hash
303
- # app.run(config: { host: '0.0.0.0', port: 8080 })
304
- #
305
- # @example With Hash shorthand
306
- # app.run(config: { host: '0.0.0.0', port: 8000 })
307
- # rubocop:disable Metrics/MethodLength
308
- def run(config: nil)
309
- require 'json'
310
-
311
- if config.nil?
312
- config = ServerConfig.new
313
- elsif config.is_a?(Hash)
314
- config = ServerConfig.new(**config)
315
- end
316
-
317
- routes_json = normalized_routes_json
318
-
319
- # Get handler map
320
- handlers = handler_map
321
-
322
- # Get lifecycle hooks
323
- hooks = @native_hooks
324
-
325
- # Get WebSocket handlers and SSE producers
326
- ws_handlers = websocket_handlers
327
- sse_prods = sse_producers
328
-
329
- grpc_services = @grpc_services
330
-
331
- # Get dependencies for DI
332
- deps = @native_dependencies
333
-
334
- # Call the Rust extension's run_server function
335
- Spikard::Native.run_server(routes_json, handlers, config, hooks, ws_handlers, sse_prods, grpc_services, deps)
336
-
337
- # Keep Ruby process alive while server runs
338
- sleep
339
- rescue LoadError => e
340
- raise 'Failed to load Spikard extension. ' \
341
- "Build it with: task build:ruby\n#{e.message}"
342
- end
343
- # rubocop:enable Metrics/MethodLength
344
-
345
- private
346
-
347
- def normalize_path(path)
348
- # Preserve trailing slash for consistent routing
349
- has_trailing_slash = path.end_with?('/')
350
-
351
- segments = path.split('/').map do |segment|
352
- if segment.start_with?(':') && segment.length > 1
353
- "{#{segment[1..]}}"
354
- else
355
- segment
356
- end
357
- end
358
-
359
- normalized = segments.join('/')
360
- # Restore trailing slash if original path had one
361
- has_trailing_slash && !normalized.end_with?('/') ? "#{normalized}/" : normalized
362
- end
363
-
364
- def validate_route_arguments!(block, handler_name, options)
365
- if block.nil? && (handler_name.nil? || handler_name.empty?)
366
- raise ArgumentError, 'block required for route handler'
367
- end
368
-
369
- unknown_keys = options.keys - SUPPORTED_OPTIONS
370
- return if unknown_keys.empty?
371
-
372
- raise ArgumentError, "unknown route options: #{unknown_keys.join(', ')}"
373
- end
374
-
375
- def build_route_metadata_for(method, path, handler_name, options, block)
376
- if block && native_route_metadata_supported?
377
- build_native_route_metadata(method, path, handler_name, options, block)
378
- else
379
- build_fallback_route_metadata(method, path, handler_name, options, block)
380
- end
381
- end
382
-
383
- def native_route_metadata_supported?
384
- defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_route_metadata)
385
- end
386
-
387
- def build_native_route_metadata(method, path, handler_name, options, block)
388
- Spikard::Native.build_route_metadata(
389
- *native_route_metadata_args(method, path, handler_name, options, block, include_jsonrpc: true)
390
- )
391
- rescue ArgumentError => e
392
- raise unless e.message.include?('wrong number of arguments')
393
-
394
- Spikard::Native.build_route_metadata(
395
- *native_route_metadata_args(method, path, handler_name, options, block, include_jsonrpc: false)
396
- )
397
- end
398
-
399
- def native_route_metadata_args(method, path, handler_name, options, block, include_jsonrpc:)
400
- args = [
401
- method, path, handler_name, options[:request_schema], options[:response_schema],
402
- options[:parameter_schema], options[:file_params], options.fetch(:is_async, false),
403
- options[:cors], options[:body_param_name]&.to_s
404
- ]
405
- args << options[:jsonrpc_method] if include_jsonrpc
406
- args << block
407
- args
408
- end
409
-
410
- def build_fallback_route_metadata(method, path, handler_name, options, block)
411
- handler_name ||= default_handler_name(method, path)
412
- handler_dependencies = block ? extract_handler_dependencies(block) : []
413
- build_metadata(method, path, handler_name, options, handler_dependencies)
414
- end
415
-
416
- def extract_handler_dependencies(block)
417
- # Get the block's parameters
418
- params = block.parameters
419
-
420
- # Extract keyword parameters (dependencies)
421
- # Parameters come in the format [:req/:opt/:keyreq/:key, :param_name]
422
- # :keyreq and :key are keyword parameters (required and optional)
423
- dependencies = []
424
-
425
- params.each do |param_type, param_name|
426
- # Skip the request parameter (usually first positional param)
427
- # Only collect keyword parameters
428
- next unless %i[keyreq key].include?(param_type)
429
-
430
- dep_name = param_name.to_s
431
- # Collect ALL keyword parameters, not just registered ones
432
- # This allows the DI system to validate missing dependencies
433
- dependencies << dep_name
434
- end
435
-
436
- dependencies
437
- end
438
-
439
- def build_metadata(method, path, handler_name, options, handler_dependencies)
440
- base = {
441
- method: method,
442
- path: normalize_path(path),
443
- handler_name: handler_name,
444
- is_async: options.fetch(:is_async, false)
445
- }
446
-
447
- # Add handler_dependencies if present
448
- base[:handler_dependencies] = handler_dependencies unless handler_dependencies.empty?
449
-
450
- SUPPORTED_OPTIONS.each_with_object(base) do |key, metadata|
451
- next if key == :is_async || !options.key?(key)
452
-
453
- metadata[key] = options[key]
454
- end
455
- end
456
- end
457
- # rubocop:enable Metrics/ClassLength
458
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spikard
4
- # Background job helpers.
5
- module Background
6
- module_function
7
-
8
- @queue = Queue.new
9
- @worker = nil
10
- @worker_mutex = Mutex.new
11
- SHUTDOWN = Object.new
12
-
13
- def ensure_worker
14
- return if @worker&.alive?
15
-
16
- @worker_mutex.synchronize do
17
- return if @worker&.alive?
18
-
19
- @worker = Thread.new do
20
- loop do
21
- job = @queue.pop
22
- break if job.equal?(SHUTDOWN)
23
-
24
- begin
25
- job.call
26
- rescue StandardError => e
27
- warn("[spikard.background] job failed: #{e.message}")
28
- end
29
- end
30
- end
31
- end
32
- end
33
-
34
- # Schedule a block to run after the response has been returned.
35
- def run(&block)
36
- raise ArgumentError, 'background.run requires a block' unless block
37
-
38
- ensure_worker
39
- @queue << block
40
- end
41
-
42
- # Stop the background worker thread to allow process shutdown.
43
- def shutdown
44
- @worker_mutex.synchronize do
45
- return unless @worker&.alive?
46
-
47
- @queue << SHUTDOWN
48
- @worker.join(1)
49
- @worker.kill if @worker.alive?
50
- @worker = nil
51
- end
52
- end
53
-
54
- at_exit do
55
- shutdown
56
- end
57
- end
58
- end