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
data/lib/spikard/app.rb DELETED
@@ -1,428 +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
- @native_hooks = Spikard::Native::LifecycleRegistry.new
131
- @native_dependencies = Spikard::Native::DependencyRegistry.new
132
- @named_handlers = {}
133
- end
134
-
135
- def register_route(method, path, handler_name: nil, **options, &block)
136
- method = method.to_s
137
- path = path.to_s
138
- handler_name = handler_name&.to_s
139
- handler = block || (handler_name && @named_handlers[handler_name])
140
- validate_route_arguments!(handler, handler_name, options)
141
- metadata = build_route_metadata_for(method, path, handler_name, options, handler)
142
-
143
- @routes << RouteEntry.new(metadata, handler)
144
- handler
145
- end
146
-
147
- HTTP_METHODS.each do |verb|
148
- define_method(verb.downcase) do |path, handler_name: nil, **options, &block|
149
- register_route(verb, path, handler_name: handler_name, **options, &block)
150
- end
151
- end
152
-
153
- def route_metadata
154
- @routes.map(&:metadata)
155
- end
156
-
157
- def handler_map
158
- map = {}
159
- @routes.each do |entry|
160
- name = entry.metadata[:handler_name]
161
- # Pass raw handler - DI resolution happens in Rust layer
162
- map[name] = entry.handler
163
- end
164
- map.merge!(@named_handlers)
165
- map
166
- end
167
-
168
- def handler(name, &block)
169
- raise ArgumentError, 'block required for handler' unless block
170
-
171
- handler_name = name.to_s
172
- @named_handlers[handler_name] = block
173
-
174
- @routes.each do |entry|
175
- next unless entry.metadata[:handler_name] == handler_name
176
-
177
- entry.handler = block
178
- next unless entry.metadata.is_a?(Hash)
179
-
180
- deps = extract_handler_dependencies(block)
181
- entry.metadata[:handler_dependencies] = deps unless deps.empty?
182
- end
183
-
184
- block
185
- end
186
-
187
- def normalized_routes_json
188
- json = JSON.generate(route_metadata)
189
- if defined?(Spikard::Native) && Spikard::Native.respond_to?(:normalize_route_metadata)
190
- Spikard::Native.normalize_route_metadata(json)
191
- else
192
- json
193
- end
194
- end
195
-
196
- def default_handler_name(method, path)
197
- normalized_path = path.gsub(/[^a-zA-Z0-9]+/, '_').gsub(/__+/, '_')
198
- # ReDoS mitigation: use bounded quantifier {1,100} instead of + to prevent
199
- # polynomial time complexity with excessive trailing underscores
200
- normalized_path = normalized_path.sub(/^_{1,100}/, '').sub(/_{1,100}$/, '')
201
- normalized_path = 'root' if normalized_path.empty?
202
- "#{method.to_s.downcase}_#{normalized_path}"
203
- end
204
-
205
- # Register a WebSocket endpoint
206
- #
207
- # @param path [String] URL path for the WebSocket endpoint
208
- # @yield Factory block that returns a WebSocketHandler instance
209
- # @return [Proc] The factory block (for chaining)
210
- #
211
- # @example
212
- # app.websocket('/chat') do
213
- # ChatHandler.new
214
- # end
215
- def websocket(path, _handler_name: nil, **_options, &factory)
216
- raise ArgumentError, 'block required for WebSocket handler factory' unless factory
217
-
218
- @websocket_handlers[path] = factory
219
- factory
220
- end
221
-
222
- # Register a Server-Sent Events endpoint
223
- #
224
- # @param path [String] URL path for the SSE endpoint
225
- # @yield Factory block that returns a SseEventProducer instance
226
- # @return [Proc] The factory block (for chaining)
227
- #
228
- # @example
229
- # app.sse('/notifications') do
230
- # NotificationProducer.new
231
- # end
232
- def sse(path, _handler_name: nil, **_options, &factory)
233
- raise ArgumentError, 'block required for SSE producer factory' unless factory
234
-
235
- @sse_producers[path] = factory
236
- factory
237
- end
238
-
239
- # Get all registered WebSocket handlers
240
- #
241
- # @return [Hash] Dictionary mapping paths to handler factory blocks
242
- def websocket_handlers
243
- @websocket_handlers.dup
244
- end
245
-
246
- # Get all registered SSE producers
247
- #
248
- # @return [Hash] Dictionary mapping paths to producer factory blocks
249
- def sse_producers
250
- @sse_producers.dup
251
- end
252
-
253
- # Run the Spikard server with the given configuration
254
- #
255
- # @param config [ServerConfig, Hash, nil] Server configuration
256
- # Can be a ServerConfig object, a Hash with configuration keys, or nil to use defaults.
257
- # If a Hash is provided, it will be converted to a ServerConfig.
258
- # For backward compatibility, also accepts host: and port: keyword arguments.
259
- #
260
- # @example With ServerConfig
261
- # config = Spikard::ServerConfig.new(
262
- # host: '0.0.0.0',
263
- # port: 8080,
264
- # compression: Spikard::CompressionConfig.new(quality: 9)
265
- # )
266
- # app.run(config: config)
267
- #
268
- # @example With Hash
269
- # app.run(config: { host: '0.0.0.0', port: 8080 })
270
- #
271
- # @example Backward compatible (deprecated)
272
- # app.run(host: '0.0.0.0', port: 8000)
273
- # rubocop:disable Metrics/MethodLength
274
- def run(config: nil, host: nil, port: nil)
275
- require 'json'
276
-
277
- # Backward compatibility: if host/port are provided directly, create a config
278
- if config.nil? && (host || port)
279
- config = ServerConfig.new(
280
- host: host || '127.0.0.1',
281
- port: port || 8000
282
- )
283
- elsif config.nil?
284
- config = ServerConfig.new
285
- elsif config.is_a?(Hash)
286
- config = ServerConfig.new(**config)
287
- end
288
-
289
- routes_json = normalized_routes_json
290
-
291
- # Get handler map
292
- handlers = handler_map
293
-
294
- # Get lifecycle hooks
295
- hooks = @native_hooks
296
-
297
- # Get WebSocket handlers and SSE producers
298
- ws_handlers = websocket_handlers
299
- sse_prods = sse_producers
300
-
301
- # Get dependencies for DI
302
- deps = @native_dependencies
303
-
304
- # Call the Rust extension's run_server function
305
- Spikard::Native.run_server(routes_json, handlers, config, hooks, ws_handlers, sse_prods, deps)
306
-
307
- # Keep Ruby process alive while server runs
308
- sleep
309
- rescue LoadError => e
310
- raise 'Failed to load Spikard extension. ' \
311
- "Build it with: task build:ruby\n#{e.message}"
312
- end
313
- # rubocop:enable Metrics/MethodLength
314
-
315
- private
316
-
317
- def normalize_path(path)
318
- # Preserve trailing slash for consistent routing
319
- has_trailing_slash = path.end_with?('/')
320
-
321
- segments = path.split('/').map do |segment|
322
- if segment.start_with?(':') && segment.length > 1
323
- "{#{segment[1..]}}"
324
- else
325
- segment
326
- end
327
- end
328
-
329
- normalized = segments.join('/')
330
- # Restore trailing slash if original path had one
331
- has_trailing_slash && !normalized.end_with?('/') ? "#{normalized}/" : normalized
332
- end
333
-
334
- def validate_route_arguments!(block, handler_name, options)
335
- if block.nil? && (handler_name.nil? || handler_name.empty?)
336
- raise ArgumentError, 'block required for route handler'
337
- end
338
-
339
- unknown_keys = options.keys - SUPPORTED_OPTIONS
340
- return if unknown_keys.empty?
341
-
342
- raise ArgumentError, "unknown route options: #{unknown_keys.join(', ')}"
343
- end
344
-
345
- def build_route_metadata_for(method, path, handler_name, options, block)
346
- if block && native_route_metadata_supported?
347
- build_native_route_metadata(method, path, handler_name, options, block)
348
- else
349
- build_fallback_route_metadata(method, path, handler_name, options, block)
350
- end
351
- end
352
-
353
- def native_route_metadata_supported?
354
- defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_route_metadata)
355
- end
356
-
357
- def build_native_route_metadata(method, path, handler_name, options, block)
358
- Spikard::Native.build_route_metadata(
359
- *native_route_metadata_args(method, path, handler_name, options, block, include_jsonrpc: true)
360
- )
361
- rescue ArgumentError => e
362
- raise unless e.message.include?('wrong number of arguments')
363
-
364
- Spikard::Native.build_route_metadata(
365
- *native_route_metadata_args(method, path, handler_name, options, block, include_jsonrpc: false)
366
- )
367
- end
368
-
369
- def native_route_metadata_args(method, path, handler_name, options, block, include_jsonrpc:)
370
- args = [
371
- method, path, handler_name, options[:request_schema], options[:response_schema],
372
- options[:parameter_schema], options[:file_params], options.fetch(:is_async, false),
373
- options[:cors], options[:body_param_name]&.to_s
374
- ]
375
- args << options[:jsonrpc_method] if include_jsonrpc
376
- args << block
377
- args
378
- end
379
-
380
- def build_fallback_route_metadata(method, path, handler_name, options, block)
381
- handler_name ||= default_handler_name(method, path)
382
- handler_dependencies = block ? extract_handler_dependencies(block) : []
383
- build_metadata(method, path, handler_name, options, handler_dependencies)
384
- end
385
-
386
- def extract_handler_dependencies(block)
387
- # Get the block's parameters
388
- params = block.parameters
389
-
390
- # Extract keyword parameters (dependencies)
391
- # Parameters come in the format [:req/:opt/:keyreq/:key, :param_name]
392
- # :keyreq and :key are keyword parameters (required and optional)
393
- dependencies = []
394
-
395
- params.each do |param_type, param_name|
396
- # Skip the request parameter (usually first positional param)
397
- # Only collect keyword parameters
398
- next unless %i[keyreq key].include?(param_type)
399
-
400
- dep_name = param_name.to_s
401
- # Collect ALL keyword parameters, not just registered ones
402
- # This allows the DI system to validate missing dependencies
403
- dependencies << dep_name
404
- end
405
-
406
- dependencies
407
- end
408
-
409
- def build_metadata(method, path, handler_name, options, handler_dependencies)
410
- base = {
411
- method: method,
412
- path: normalize_path(path),
413
- handler_name: handler_name,
414
- is_async: options.fetch(:is_async, false)
415
- }
416
-
417
- # Add handler_dependencies if present
418
- base[:handler_dependencies] = handler_dependencies unless handler_dependencies.empty?
419
-
420
- SUPPORTED_OPTIONS.each_with_object(base) do |key, metadata|
421
- next if key == :is_async || !options.key?(key)
422
-
423
- metadata[key] = options[key]
424
- end
425
- end
426
- end
427
- # rubocop:enable Metrics/ClassLength
428
- 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