spikard 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +626 -553
- data/ext/spikard_rb/Cargo.toml +17 -16
- data/ext/spikard_rb/extconf.rb +10 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +374 -328
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +85 -85
- data/lib/spikard/handler_wrapper.rb +116 -116
- data/lib/spikard/provide.rb +228 -0
- data/lib/spikard/response.rb +109 -109
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +21 -21
- data/lib/spikard/testing.rb +221 -220
- data/lib/spikard/upload_file.rb +131 -131
- data/lib/spikard/version.rb +5 -5
- data/lib/spikard/websocket.rb +59 -59
- data/lib/spikard.rb +43 -42
- data/sig/spikard.rbs +349 -336
- data/vendor/bundle/ruby/3.3.0/gems/diff-lcs-1.6.2/mise.toml +5 -0
- metadata +23 -5
data/lib/spikard/app.rb
CHANGED
|
@@ -1,328 +1,374 @@
|
|
|
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
|
-
@lifecycle_hooks[: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
|
-
@lifecycle_hooks[: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
|
-
@lifecycle_hooks[: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
|
-
@lifecycle_hooks[: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
|
-
@lifecycle_hooks[:on_error] << hook
|
|
102
|
-
hook
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Get all registered lifecycle hooks
|
|
106
|
-
#
|
|
107
|
-
# @return [Hash] Dictionary of hook arrays by type
|
|
108
|
-
def lifecycle_hooks
|
|
109
|
-
{
|
|
110
|
-
on_request: @lifecycle_hooks[:on_request].dup,
|
|
111
|
-
pre_validation: @lifecycle_hooks[:pre_validation].dup,
|
|
112
|
-
pre_handler: @lifecycle_hooks[:pre_handler].dup,
|
|
113
|
-
on_response: @lifecycle_hooks[:on_response].dup,
|
|
114
|
-
on_error: @lifecycle_hooks[:on_error].dup
|
|
115
|
-
}
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Collects route metadata so the Rust engine can execute handlers.
|
|
120
|
-
# rubocop:disable Metrics/ClassLength
|
|
121
|
-
class App
|
|
122
|
-
include LifecycleHooks
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
@
|
|
132
|
-
@
|
|
133
|
-
@
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
block
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Register a
|
|
194
|
-
#
|
|
195
|
-
# @param path [String] URL path for the
|
|
196
|
-
# @yield Factory block that returns a
|
|
197
|
-
# @return [Proc] The factory block (for chaining)
|
|
198
|
-
#
|
|
199
|
-
# @example
|
|
200
|
-
# app.
|
|
201
|
-
#
|
|
202
|
-
# end
|
|
203
|
-
def
|
|
204
|
-
raise ArgumentError, 'block required for
|
|
205
|
-
|
|
206
|
-
@
|
|
207
|
-
factory
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
# @
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
#
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
@lifecycle_hooks[: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
|
+
@lifecycle_hooks[: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
|
+
@lifecycle_hooks[: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
|
+
@lifecycle_hooks[: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
|
+
@lifecycle_hooks[:on_error] << hook
|
|
102
|
+
hook
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Get all registered lifecycle hooks
|
|
106
|
+
#
|
|
107
|
+
# @return [Hash] Dictionary of hook arrays by type
|
|
108
|
+
def lifecycle_hooks
|
|
109
|
+
{
|
|
110
|
+
on_request: @lifecycle_hooks[:on_request].dup,
|
|
111
|
+
pre_validation: @lifecycle_hooks[:pre_validation].dup,
|
|
112
|
+
pre_handler: @lifecycle_hooks[:pre_handler].dup,
|
|
113
|
+
on_response: @lifecycle_hooks[:on_response].dup,
|
|
114
|
+
on_error: @lifecycle_hooks[:on_error].dup
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Collects route metadata so the Rust engine can execute handlers.
|
|
120
|
+
# rubocop:disable Metrics/ClassLength
|
|
121
|
+
class App
|
|
122
|
+
include LifecycleHooks
|
|
123
|
+
include ProvideSupport
|
|
124
|
+
|
|
125
|
+
HTTP_METHODS = %w[GET POST PUT PATCH DELETE OPTIONS HEAD TRACE].freeze
|
|
126
|
+
SUPPORTED_OPTIONS = %i[request_schema response_schema parameter_schema file_params is_async cors].freeze
|
|
127
|
+
|
|
128
|
+
attr_reader :routes
|
|
129
|
+
|
|
130
|
+
def initialize
|
|
131
|
+
@routes = []
|
|
132
|
+
@websocket_handlers = {}
|
|
133
|
+
@sse_producers = {}
|
|
134
|
+
@dependencies = {}
|
|
135
|
+
@lifecycle_hooks = {
|
|
136
|
+
on_request: [],
|
|
137
|
+
pre_validation: [],
|
|
138
|
+
pre_handler: [],
|
|
139
|
+
on_response: [],
|
|
140
|
+
on_error: []
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def register_route(method, path, handler_name: nil, **options, &block)
|
|
145
|
+
validate_route_arguments!(block, options)
|
|
146
|
+
handler_name ||= default_handler_name(method, path)
|
|
147
|
+
|
|
148
|
+
# Extract handler dependencies from block parameters
|
|
149
|
+
handler_dependencies = extract_handler_dependencies(block)
|
|
150
|
+
|
|
151
|
+
metadata = build_metadata(method, path, handler_name, options, handler_dependencies)
|
|
152
|
+
|
|
153
|
+
@routes << RouteEntry.new(metadata, block)
|
|
154
|
+
block
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
HTTP_METHODS.each do |verb|
|
|
158
|
+
define_method(verb.downcase) do |path, handler_name: nil, **options, &block|
|
|
159
|
+
register_route(verb, path, handler_name: handler_name, **options, &block)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def route_metadata
|
|
164
|
+
# Extract handler dependencies when metadata is requested
|
|
165
|
+
# This allows dependencies to be registered after routes
|
|
166
|
+
@routes.map do |entry|
|
|
167
|
+
metadata = entry.metadata.dup
|
|
168
|
+
|
|
169
|
+
# Re-extract dependencies in case they were registered after the route
|
|
170
|
+
handler_dependencies = extract_handler_dependencies(entry.handler)
|
|
171
|
+
metadata[:handler_dependencies] = handler_dependencies unless handler_dependencies.empty?
|
|
172
|
+
|
|
173
|
+
metadata
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def handler_map
|
|
178
|
+
map = {}
|
|
179
|
+
@routes.each do |entry|
|
|
180
|
+
name = entry.metadata[:handler_name]
|
|
181
|
+
# Pass raw handler - DI resolution happens in Rust layer
|
|
182
|
+
map[name] = entry.handler
|
|
183
|
+
end
|
|
184
|
+
map
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def default_handler_name(method, path)
|
|
188
|
+
normalized_path = path.gsub(/[^a-zA-Z0-9]+/, '_').gsub(/__+/, '_').sub(/^_+|_+$/, '')
|
|
189
|
+
normalized_path = 'root' if normalized_path.empty?
|
|
190
|
+
"#{method.to_s.downcase}_#{normalized_path}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Register a WebSocket endpoint
|
|
194
|
+
#
|
|
195
|
+
# @param path [String] URL path for the WebSocket endpoint
|
|
196
|
+
# @yield Factory block that returns a WebSocketHandler instance
|
|
197
|
+
# @return [Proc] The factory block (for chaining)
|
|
198
|
+
#
|
|
199
|
+
# @example
|
|
200
|
+
# app.websocket('/chat') do
|
|
201
|
+
# ChatHandler.new
|
|
202
|
+
# end
|
|
203
|
+
def websocket(path, _handler_name: nil, **_options, &factory)
|
|
204
|
+
raise ArgumentError, 'block required for WebSocket handler factory' unless factory
|
|
205
|
+
|
|
206
|
+
@websocket_handlers[path] = factory
|
|
207
|
+
factory
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Register a Server-Sent Events endpoint
|
|
211
|
+
#
|
|
212
|
+
# @param path [String] URL path for the SSE endpoint
|
|
213
|
+
# @yield Factory block that returns a SseEventProducer instance
|
|
214
|
+
# @return [Proc] The factory block (for chaining)
|
|
215
|
+
#
|
|
216
|
+
# @example
|
|
217
|
+
# app.sse('/notifications') do
|
|
218
|
+
# NotificationProducer.new
|
|
219
|
+
# end
|
|
220
|
+
def sse(path, _handler_name: nil, **_options, &factory)
|
|
221
|
+
raise ArgumentError, 'block required for SSE producer factory' unless factory
|
|
222
|
+
|
|
223
|
+
@sse_producers[path] = factory
|
|
224
|
+
factory
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Get all registered WebSocket handlers
|
|
228
|
+
#
|
|
229
|
+
# @return [Hash] Dictionary mapping paths to handler factory blocks
|
|
230
|
+
def websocket_handlers
|
|
231
|
+
@websocket_handlers.dup
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Get all registered SSE producers
|
|
235
|
+
#
|
|
236
|
+
# @return [Hash] Dictionary mapping paths to producer factory blocks
|
|
237
|
+
def sse_producers
|
|
238
|
+
@sse_producers.dup
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Run the Spikard server with the given configuration
|
|
242
|
+
#
|
|
243
|
+
# @param config [ServerConfig, Hash, nil] Server configuration
|
|
244
|
+
# Can be a ServerConfig object, a Hash with configuration keys, or nil to use defaults.
|
|
245
|
+
# If a Hash is provided, it will be converted to a ServerConfig.
|
|
246
|
+
# For backward compatibility, also accepts host: and port: keyword arguments.
|
|
247
|
+
#
|
|
248
|
+
# @example With ServerConfig
|
|
249
|
+
# config = Spikard::ServerConfig.new(
|
|
250
|
+
# host: '0.0.0.0',
|
|
251
|
+
# port: 8080,
|
|
252
|
+
# compression: Spikard::CompressionConfig.new(quality: 9)
|
|
253
|
+
# )
|
|
254
|
+
# app.run(config: config)
|
|
255
|
+
#
|
|
256
|
+
# @example With Hash
|
|
257
|
+
# app.run(config: { host: '0.0.0.0', port: 8080 })
|
|
258
|
+
#
|
|
259
|
+
# @example Backward compatible (deprecated)
|
|
260
|
+
# app.run(host: '0.0.0.0', port: 8000)
|
|
261
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
262
|
+
def run(config: nil, host: nil, port: nil)
|
|
263
|
+
require 'json'
|
|
264
|
+
|
|
265
|
+
# Backward compatibility: if host/port are provided directly, create a config
|
|
266
|
+
if config.nil? && (host || port)
|
|
267
|
+
config = ServerConfig.new(
|
|
268
|
+
host: host || '127.0.0.1',
|
|
269
|
+
port: port || 8000
|
|
270
|
+
)
|
|
271
|
+
elsif config.nil?
|
|
272
|
+
config = ServerConfig.new
|
|
273
|
+
elsif config.is_a?(Hash)
|
|
274
|
+
config = ServerConfig.new(**config)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Convert route metadata to JSON
|
|
278
|
+
routes_json = JSON.generate(route_metadata)
|
|
279
|
+
|
|
280
|
+
# Get handler map
|
|
281
|
+
handlers = handler_map
|
|
282
|
+
|
|
283
|
+
# Get lifecycle hooks
|
|
284
|
+
hooks = lifecycle_hooks
|
|
285
|
+
|
|
286
|
+
# Get WebSocket handlers and SSE producers
|
|
287
|
+
ws_handlers = websocket_handlers
|
|
288
|
+
sse_prods = sse_producers
|
|
289
|
+
|
|
290
|
+
# Get dependencies for DI
|
|
291
|
+
deps = dependencies
|
|
292
|
+
|
|
293
|
+
# Call the Rust extension's run_server function
|
|
294
|
+
Spikard::Native.run_server(routes_json, handlers, config, hooks, ws_handlers, sse_prods, deps)
|
|
295
|
+
|
|
296
|
+
# Keep Ruby process alive while server runs
|
|
297
|
+
sleep
|
|
298
|
+
rescue LoadError => e
|
|
299
|
+
raise 'Failed to load Spikard extension. ' \
|
|
300
|
+
"Build it with: task build:ruby\n#{e.message}"
|
|
301
|
+
end
|
|
302
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
303
|
+
|
|
304
|
+
private
|
|
305
|
+
|
|
306
|
+
def normalize_path(path)
|
|
307
|
+
# Preserve trailing slash for consistent routing
|
|
308
|
+
has_trailing_slash = path.end_with?('/')
|
|
309
|
+
|
|
310
|
+
segments = path.split('/').map do |segment|
|
|
311
|
+
if segment.start_with?(':') && segment.length > 1
|
|
312
|
+
"{#{segment[1..]}}"
|
|
313
|
+
else
|
|
314
|
+
segment
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
normalized = segments.join('/')
|
|
319
|
+
# Restore trailing slash if original path had one
|
|
320
|
+
has_trailing_slash && !normalized.end_with?('/') ? "#{normalized}/" : normalized
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def validate_route_arguments!(block, options)
|
|
324
|
+
raise ArgumentError, 'block required for route handler' unless block
|
|
325
|
+
|
|
326
|
+
unknown_keys = options.keys - SUPPORTED_OPTIONS
|
|
327
|
+
return if unknown_keys.empty?
|
|
328
|
+
|
|
329
|
+
raise ArgumentError, "unknown route options: #{unknown_keys.join(', ')}"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def extract_handler_dependencies(block)
|
|
333
|
+
# Get the block's parameters
|
|
334
|
+
params = block.parameters
|
|
335
|
+
|
|
336
|
+
# Extract keyword parameters (dependencies)
|
|
337
|
+
# Parameters come in the format [:req/:opt/:keyreq/:key, :param_name]
|
|
338
|
+
# :keyreq and :key are keyword parameters (required and optional)
|
|
339
|
+
dependencies = []
|
|
340
|
+
|
|
341
|
+
params.each do |param_type, param_name|
|
|
342
|
+
# Skip the request parameter (usually first positional param)
|
|
343
|
+
# Only collect keyword parameters
|
|
344
|
+
next unless %i[keyreq key].include?(param_type)
|
|
345
|
+
|
|
346
|
+
dep_name = param_name.to_s
|
|
347
|
+
# Collect ALL keyword parameters, not just registered ones
|
|
348
|
+
# This allows the DI system to validate missing dependencies
|
|
349
|
+
dependencies << dep_name
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
dependencies
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def build_metadata(method, path, handler_name, options, handler_dependencies)
|
|
356
|
+
base = {
|
|
357
|
+
method: method,
|
|
358
|
+
path: normalize_path(path),
|
|
359
|
+
handler_name: handler_name,
|
|
360
|
+
is_async: options.fetch(:is_async, false)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# Add handler_dependencies if present
|
|
364
|
+
base[:handler_dependencies] = handler_dependencies unless handler_dependencies.empty?
|
|
365
|
+
|
|
366
|
+
SUPPORTED_OPTIONS.each_with_object(base) do |key, metadata|
|
|
367
|
+
next if key == :is_async || !options.key?(key)
|
|
368
|
+
|
|
369
|
+
metadata[key] = options[key]
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
# rubocop:enable Metrics/ClassLength
|
|
374
|
+
end
|