spikard 0.4.0-arm64-darwin-23

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 (138) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -0
  3. data/README.md +659 -0
  4. data/ext/spikard_rb/Cargo.toml +17 -0
  5. data/ext/spikard_rb/extconf.rb +10 -0
  6. data/ext/spikard_rb/src/lib.rs +6 -0
  7. data/lib/spikard/app.rb +405 -0
  8. data/lib/spikard/background.rb +27 -0
  9. data/lib/spikard/config.rb +396 -0
  10. data/lib/spikard/converters.rb +13 -0
  11. data/lib/spikard/handler_wrapper.rb +113 -0
  12. data/lib/spikard/provide.rb +214 -0
  13. data/lib/spikard/response.rb +173 -0
  14. data/lib/spikard/schema.rb +243 -0
  15. data/lib/spikard/sse.rb +111 -0
  16. data/lib/spikard/streaming_response.rb +44 -0
  17. data/lib/spikard/testing.rb +221 -0
  18. data/lib/spikard/upload_file.rb +131 -0
  19. data/lib/spikard/version.rb +5 -0
  20. data/lib/spikard/websocket.rb +59 -0
  21. data/lib/spikard.rb +43 -0
  22. data/sig/spikard.rbs +366 -0
  23. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +5 -0
  24. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
  25. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  26. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +139 -0
  27. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +561 -0
  28. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  29. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  30. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +403 -0
  31. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +274 -0
  32. data/vendor/crates/spikard-bindings-shared/src/lib.rs +25 -0
  33. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +298 -0
  34. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +637 -0
  35. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +309 -0
  36. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  37. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +355 -0
  38. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +502 -0
  39. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +389 -0
  40. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +413 -0
  41. data/vendor/crates/spikard-core/Cargo.toml +40 -0
  42. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
  43. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
  44. data/vendor/crates/spikard-core/src/debug.rs +63 -0
  45. data/vendor/crates/spikard-core/src/di/container.rs +726 -0
  46. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
  47. data/vendor/crates/spikard-core/src/di/error.rs +118 -0
  48. data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
  49. data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
  50. data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
  51. data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
  52. data/vendor/crates/spikard-core/src/di/value.rs +283 -0
  53. data/vendor/crates/spikard-core/src/errors.rs +39 -0
  54. data/vendor/crates/spikard-core/src/http.rs +153 -0
  55. data/vendor/crates/spikard-core/src/lib.rs +29 -0
  56. data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
  57. data/vendor/crates/spikard-core/src/metadata.rs +397 -0
  58. data/vendor/crates/spikard-core/src/parameters.rs +723 -0
  59. data/vendor/crates/spikard-core/src/problem.rs +310 -0
  60. data/vendor/crates/spikard-core/src/request_data.rs +189 -0
  61. data/vendor/crates/spikard-core/src/router.rs +249 -0
  62. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
  63. data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
  64. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +689 -0
  65. data/vendor/crates/spikard-core/src/validation/mod.rs +459 -0
  66. data/vendor/crates/spikard-http/Cargo.toml +58 -0
  67. data/vendor/crates/spikard-http/examples/sse-notifications.rs +147 -0
  68. data/vendor/crates/spikard-http/examples/websocket-chat.rs +91 -0
  69. data/vendor/crates/spikard-http/src/auth.rs +247 -0
  70. data/vendor/crates/spikard-http/src/background.rs +1562 -0
  71. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
  72. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
  73. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
  74. data/vendor/crates/spikard-http/src/cors.rs +490 -0
  75. data/vendor/crates/spikard-http/src/debug.rs +63 -0
  76. data/vendor/crates/spikard-http/src/di_handler.rs +1878 -0
  77. data/vendor/crates/spikard-http/src/handler_response.rs +532 -0
  78. data/vendor/crates/spikard-http/src/handler_trait.rs +861 -0
  79. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
  80. data/vendor/crates/spikard-http/src/lib.rs +524 -0
  81. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
  82. data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
  83. data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
  84. data/vendor/crates/spikard-http/src/middleware/multipart.rs +930 -0
  85. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +541 -0
  86. data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
  87. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
  88. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -0
  89. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +867 -0
  90. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +678 -0
  91. data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
  92. data/vendor/crates/spikard-http/src/response.rs +399 -0
  93. data/vendor/crates/spikard-http/src/server/handler.rs +1557 -0
  94. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
  95. data/vendor/crates/spikard-http/src/server/mod.rs +806 -0
  96. data/vendor/crates/spikard-http/src/server/request_extraction.rs +630 -0
  97. data/vendor/crates/spikard-http/src/server/routing_factory.rs +497 -0
  98. data/vendor/crates/spikard-http/src/sse.rs +961 -0
  99. data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
  100. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
  101. data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
  102. data/vendor/crates/spikard-http/src/testing.rs +377 -0
  103. data/vendor/crates/spikard-http/src/websocket.rs +831 -0
  104. data/vendor/crates/spikard-http/tests/background_behavior.rs +918 -0
  105. data/vendor/crates/spikard-http/tests/common/handlers.rs +308 -0
  106. data/vendor/crates/spikard-http/tests/common/mod.rs +21 -0
  107. data/vendor/crates/spikard-http/tests/di_integration.rs +202 -0
  108. data/vendor/crates/spikard-http/tests/doc_snippets.rs +4 -0
  109. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1135 -0
  110. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +688 -0
  111. data/vendor/crates/spikard-http/tests/server_config_builder.rs +324 -0
  112. data/vendor/crates/spikard-http/tests/sse_behavior.rs +728 -0
  113. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +724 -0
  114. data/vendor/crates/spikard-rb/Cargo.toml +43 -0
  115. data/vendor/crates/spikard-rb/build.rs +199 -0
  116. data/vendor/crates/spikard-rb/src/background.rs +63 -0
  117. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  118. data/vendor/crates/spikard-rb/src/config/server_config.rs +283 -0
  119. data/vendor/crates/spikard-rb/src/conversion.rs +459 -0
  120. data/vendor/crates/spikard-rb/src/di/builder.rs +105 -0
  121. data/vendor/crates/spikard-rb/src/di/mod.rs +413 -0
  122. data/vendor/crates/spikard-rb/src/handler.rs +612 -0
  123. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  124. data/vendor/crates/spikard-rb/src/lib.rs +1857 -0
  125. data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -0
  126. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  127. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +427 -0
  128. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  129. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +326 -0
  130. data/vendor/crates/spikard-rb/src/server.rs +283 -0
  131. data/vendor/crates/spikard-rb/src/sse.rs +231 -0
  132. data/vendor/crates/spikard-rb/src/testing/client.rs +404 -0
  133. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  134. data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -0
  135. data/vendor/crates/spikard-rb/src/testing/websocket.rs +221 -0
  136. data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
  137. data/vendor/crates/spikard-rb/tests/magnus_ffi_tests.rs +14 -0
  138. metadata +213 -0
@@ -0,0 +1,405 @@
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
+ end
133
+
134
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
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
+ validate_route_arguments!(block, options)
140
+ metadata = if defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_route_metadata)
141
+ begin
142
+ Spikard::Native.build_route_metadata(
143
+ method,
144
+ path,
145
+ handler_name,
146
+ options[:request_schema],
147
+ options[:response_schema],
148
+ options[:parameter_schema],
149
+ options[:file_params],
150
+ options.fetch(:is_async, false),
151
+ options[:cors],
152
+ options[:body_param_name]&.to_s,
153
+ options[:jsonrpc_method],
154
+ block
155
+ )
156
+ rescue ArgumentError => e
157
+ raise unless e.message.include?('wrong number of arguments')
158
+
159
+ Spikard::Native.build_route_metadata(
160
+ method,
161
+ path,
162
+ handler_name,
163
+ options[:request_schema],
164
+ options[:response_schema],
165
+ options[:parameter_schema],
166
+ options[:file_params],
167
+ options.fetch(:is_async, false),
168
+ options[:cors],
169
+ options[:body_param_name]&.to_s,
170
+ block
171
+ )
172
+ end
173
+ else
174
+ handler_name ||= default_handler_name(method, path)
175
+
176
+ # Extract handler dependencies from block parameters
177
+ handler_dependencies = extract_handler_dependencies(block)
178
+
179
+ build_metadata(method, path, handler_name, options, handler_dependencies)
180
+ end
181
+
182
+ @routes << RouteEntry.new(metadata, block)
183
+ block
184
+ end
185
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
186
+
187
+ HTTP_METHODS.each do |verb|
188
+ define_method(verb.downcase) do |path, handler_name: nil, **options, &block|
189
+ register_route(verb, path, handler_name: handler_name, **options, &block)
190
+ end
191
+ end
192
+
193
+ def route_metadata
194
+ @routes.map(&:metadata)
195
+ end
196
+
197
+ def handler_map
198
+ map = {}
199
+ @routes.each do |entry|
200
+ name = entry.metadata[:handler_name]
201
+ # Pass raw handler - DI resolution happens in Rust layer
202
+ map[name] = entry.handler
203
+ end
204
+ map
205
+ end
206
+
207
+ def normalized_routes_json
208
+ json = JSON.generate(route_metadata)
209
+ if defined?(Spikard::Native) && Spikard::Native.respond_to?(:normalize_route_metadata)
210
+ Spikard::Native.normalize_route_metadata(json)
211
+ else
212
+ json
213
+ end
214
+ end
215
+
216
+ def default_handler_name(method, path)
217
+ normalized_path = path.gsub(/[^a-zA-Z0-9]+/, '_').gsub(/__+/, '_')
218
+ # ReDoS mitigation: use bounded quantifier {1,100} instead of + to prevent
219
+ # polynomial time complexity with excessive trailing underscores
220
+ normalized_path = normalized_path.sub(/^_{1,100}/, '').sub(/_{1,100}$/, '')
221
+ normalized_path = 'root' if normalized_path.empty?
222
+ "#{method.to_s.downcase}_#{normalized_path}"
223
+ end
224
+
225
+ # Register a WebSocket endpoint
226
+ #
227
+ # @param path [String] URL path for the WebSocket endpoint
228
+ # @yield Factory block that returns a WebSocketHandler instance
229
+ # @return [Proc] The factory block (for chaining)
230
+ #
231
+ # @example
232
+ # app.websocket('/chat') do
233
+ # ChatHandler.new
234
+ # end
235
+ def websocket(path, _handler_name: nil, **_options, &factory)
236
+ raise ArgumentError, 'block required for WebSocket handler factory' unless factory
237
+
238
+ @websocket_handlers[path] = factory
239
+ factory
240
+ end
241
+
242
+ # Register a Server-Sent Events endpoint
243
+ #
244
+ # @param path [String] URL path for the SSE endpoint
245
+ # @yield Factory block that returns a SseEventProducer instance
246
+ # @return [Proc] The factory block (for chaining)
247
+ #
248
+ # @example
249
+ # app.sse('/notifications') do
250
+ # NotificationProducer.new
251
+ # end
252
+ def sse(path, _handler_name: nil, **_options, &factory)
253
+ raise ArgumentError, 'block required for SSE producer factory' unless factory
254
+
255
+ @sse_producers[path] = factory
256
+ factory
257
+ end
258
+
259
+ # Get all registered WebSocket handlers
260
+ #
261
+ # @return [Hash] Dictionary mapping paths to handler factory blocks
262
+ def websocket_handlers
263
+ @websocket_handlers.dup
264
+ end
265
+
266
+ # Get all registered SSE producers
267
+ #
268
+ # @return [Hash] Dictionary mapping paths to producer factory blocks
269
+ def sse_producers
270
+ @sse_producers.dup
271
+ end
272
+
273
+ # Run the Spikard server with the given configuration
274
+ #
275
+ # @param config [ServerConfig, Hash, nil] Server configuration
276
+ # Can be a ServerConfig object, a Hash with configuration keys, or nil to use defaults.
277
+ # If a Hash is provided, it will be converted to a ServerConfig.
278
+ # For backward compatibility, also accepts host: and port: keyword arguments.
279
+ #
280
+ # @example With ServerConfig
281
+ # config = Spikard::ServerConfig.new(
282
+ # host: '0.0.0.0',
283
+ # port: 8080,
284
+ # compression: Spikard::CompressionConfig.new(quality: 9)
285
+ # )
286
+ # app.run(config: config)
287
+ #
288
+ # @example With Hash
289
+ # app.run(config: { host: '0.0.0.0', port: 8080 })
290
+ #
291
+ # @example Backward compatible (deprecated)
292
+ # app.run(host: '0.0.0.0', port: 8000)
293
+ # rubocop:disable Metrics/MethodLength
294
+ def run(config: nil, host: nil, port: nil)
295
+ require 'json'
296
+
297
+ # Backward compatibility: if host/port are provided directly, create a config
298
+ if config.nil? && (host || port)
299
+ config = ServerConfig.new(
300
+ host: host || '127.0.0.1',
301
+ port: port || 8000
302
+ )
303
+ elsif config.nil?
304
+ config = ServerConfig.new
305
+ elsif config.is_a?(Hash)
306
+ config = ServerConfig.new(**config)
307
+ end
308
+
309
+ routes_json = normalized_routes_json
310
+
311
+ # Get handler map
312
+ handlers = handler_map
313
+
314
+ # Get lifecycle hooks
315
+ hooks = @native_hooks
316
+
317
+ # Get WebSocket handlers and SSE producers
318
+ ws_handlers = websocket_handlers
319
+ sse_prods = sse_producers
320
+
321
+ # Get dependencies for DI
322
+ deps = @native_dependencies
323
+
324
+ # Call the Rust extension's run_server function
325
+ Spikard::Native.run_server(routes_json, handlers, config, hooks, ws_handlers, sse_prods, deps)
326
+
327
+ # Keep Ruby process alive while server runs
328
+ sleep
329
+ rescue LoadError => e
330
+ raise 'Failed to load Spikard extension. ' \
331
+ "Build it with: task build:ruby\n#{e.message}"
332
+ end
333
+ # rubocop:enable Metrics/MethodLength
334
+
335
+ private
336
+
337
+ def normalize_path(path)
338
+ # Preserve trailing slash for consistent routing
339
+ has_trailing_slash = path.end_with?('/')
340
+
341
+ segments = path.split('/').map do |segment|
342
+ if segment.start_with?(':') && segment.length > 1
343
+ "{#{segment[1..]}}"
344
+ else
345
+ segment
346
+ end
347
+ end
348
+
349
+ normalized = segments.join('/')
350
+ # Restore trailing slash if original path had one
351
+ has_trailing_slash && !normalized.end_with?('/') ? "#{normalized}/" : normalized
352
+ end
353
+
354
+ def validate_route_arguments!(block, options)
355
+ raise ArgumentError, 'block required for route handler' unless block
356
+
357
+ unknown_keys = options.keys - SUPPORTED_OPTIONS
358
+ return if unknown_keys.empty?
359
+
360
+ raise ArgumentError, "unknown route options: #{unknown_keys.join(', ')}"
361
+ end
362
+
363
+ def extract_handler_dependencies(block)
364
+ # Get the block's parameters
365
+ params = block.parameters
366
+
367
+ # Extract keyword parameters (dependencies)
368
+ # Parameters come in the format [:req/:opt/:keyreq/:key, :param_name]
369
+ # :keyreq and :key are keyword parameters (required and optional)
370
+ dependencies = []
371
+
372
+ params.each do |param_type, param_name|
373
+ # Skip the request parameter (usually first positional param)
374
+ # Only collect keyword parameters
375
+ next unless %i[keyreq key].include?(param_type)
376
+
377
+ dep_name = param_name.to_s
378
+ # Collect ALL keyword parameters, not just registered ones
379
+ # This allows the DI system to validate missing dependencies
380
+ dependencies << dep_name
381
+ end
382
+
383
+ dependencies
384
+ end
385
+
386
+ def build_metadata(method, path, handler_name, options, handler_dependencies)
387
+ base = {
388
+ method: method,
389
+ path: normalize_path(path),
390
+ handler_name: handler_name,
391
+ is_async: options.fetch(:is_async, false)
392
+ }
393
+
394
+ # Add handler_dependencies if present
395
+ base[:handler_dependencies] = handler_dependencies unless handler_dependencies.empty?
396
+
397
+ SUPPORTED_OPTIONS.each_with_object(base) do |key, metadata|
398
+ next if key == :is_async || !options.key?(key)
399
+
400
+ metadata[key] = options[key]
401
+ end
402
+ end
403
+ end
404
+ # rubocop:enable Metrics/ClassLength
405
+ end
@@ -0,0 +1,27 @@
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 = Thread.new do
10
+ loop do
11
+ job = @queue.pop
12
+ begin
13
+ job.call
14
+ rescue StandardError => e
15
+ warn("[spikard.background] job failed: #{e.message}")
16
+ end
17
+ end
18
+ end
19
+
20
+ # Schedule a block to run after the response has been returned.
21
+ def run(&block)
22
+ raise ArgumentError, 'background.run requires a block' unless block
23
+
24
+ @queue << block
25
+ end
26
+ end
27
+ end