spikard 0.2.5 → 0.3.0
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/README.md +34 -1
- data/ext/spikard_rb/Cargo.toml +3 -3
- data/lib/spikard/app.rb +61 -49
- data/lib/spikard/converters.rb +3 -75
- data/lib/spikard/handler_wrapper.rb +6 -9
- data/lib/spikard/provide.rb +14 -28
- data/lib/spikard/response.rb +75 -11
- data/lib/spikard/streaming_response.rb +24 -1
- data/lib/spikard/testing.rb +1 -1
- data/lib/spikard/version.rb +1 -1
- data/sig/spikard.rbs +14 -3
- data/vendor/bundle/ruby/3.3.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
- metadata +3 -80
- data/vendor/crates/spikard-core/Cargo.toml +0 -40
- data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +0 -133
- data/vendor/crates/spikard-core/src/debug.rs +0 -63
- data/vendor/crates/spikard-core/src/di/container.rs +0 -726
- data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
- data/vendor/crates/spikard-core/src/di/error.rs +0 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +0 -538
- data/vendor/crates/spikard-core/src/di/graph.rs +0 -545
- data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +0 -411
- data/vendor/crates/spikard-core/src/di/value.rs +0 -283
- data/vendor/crates/spikard-core/src/http.rs +0 -153
- data/vendor/crates/spikard-core/src/lib.rs +0 -28
- data/vendor/crates/spikard-core/src/lifecycle.rs +0 -422
- data/vendor/crates/spikard-core/src/parameters.rs +0 -719
- data/vendor/crates/spikard-core/src/problem.rs +0 -310
- data/vendor/crates/spikard-core/src/request_data.rs +0 -189
- data/vendor/crates/spikard-core/src/router.rs +0 -249
- data/vendor/crates/spikard-core/src/schema_registry.rs +0 -183
- data/vendor/crates/spikard-core/src/type_hints.rs +0 -304
- data/vendor/crates/spikard-core/src/validation.rs +0 -699
- data/vendor/crates/spikard-http/Cargo.toml +0 -58
- data/vendor/crates/spikard-http/src/auth.rs +0 -247
- data/vendor/crates/spikard-http/src/background.rs +0 -249
- data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
- data/vendor/crates/spikard-http/src/cors.rs +0 -490
- data/vendor/crates/spikard-http/src/debug.rs +0 -63
- data/vendor/crates/spikard-http/src/di_handler.rs +0 -423
- data/vendor/crates/spikard-http/src/handler_response.rs +0 -190
- data/vendor/crates/spikard-http/src/handler_trait.rs +0 -228
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -284
- data/vendor/crates/spikard-http/src/lib.rs +0 -529
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -149
- data/vendor/crates/spikard-http/src/lifecycle.rs +0 -428
- data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -285
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -86
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -147
- data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -287
- data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -190
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -308
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -195
- data/vendor/crates/spikard-http/src/parameters.rs +0 -1
- data/vendor/crates/spikard-http/src/problem.rs +0 -1
- data/vendor/crates/spikard-http/src/query_parser.rs +0 -369
- data/vendor/crates/spikard-http/src/response.rs +0 -399
- data/vendor/crates/spikard-http/src/router.rs +0 -1
- data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
- data/vendor/crates/spikard-http/src/server/handler.rs +0 -80
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -98
- data/vendor/crates/spikard-http/src/server/mod.rs +0 -805
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -119
- data/vendor/crates/spikard-http/src/sse.rs +0 -447
- data/vendor/crates/spikard-http/src/testing/form.rs +0 -14
- data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -285
- data/vendor/crates/spikard-http/src/testing.rs +0 -377
- data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
- data/vendor/crates/spikard-http/src/validation.rs +0 -1
- data/vendor/crates/spikard-http/src/websocket.rs +0 -324
- data/vendor/crates/spikard-rb/Cargo.toml +0 -42
- data/vendor/crates/spikard-rb/build.rs +0 -8
- data/vendor/crates/spikard-rb/src/background.rs +0 -63
- data/vendor/crates/spikard-rb/src/config.rs +0 -294
- data/vendor/crates/spikard-rb/src/conversion.rs +0 -392
- data/vendor/crates/spikard-rb/src/di.rs +0 -409
- data/vendor/crates/spikard-rb/src/handler.rs +0 -534
- data/vendor/crates/spikard-rb/src/lib.rs +0 -2020
- data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -267
- data/vendor/crates/spikard-rb/src/server.rs +0 -283
- data/vendor/crates/spikard-rb/src/sse.rs +0 -231
- data/vendor/crates/spikard-rb/src/test_client.rs +0 -404
- data/vendor/crates/spikard-rb/src/test_sse.rs +0 -143
- data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
- data/vendor/crates/spikard-rb/src/websocket.rs +0 -233
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c70da2d00ebf8c9d79e3ef9f0a76743f39c9a0ac5d13cf723f7f87920bf5d199
|
|
4
|
+
data.tar.gz: 1fdc0938e1974995cfb46d960c1d2e6d2266c64c81077a6c2d6a046e8d045f21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05d3e4e7859dbab147d66c246ffae6e412df42ebc532b1a55c60ed25fa952d25a092e53458759e2b8f046539c08ffd17d7159d3b0ef72e1b79ae48d2cf3a0f45
|
|
7
|
+
data.tar.gz: bc6c6d79bdf8f709443a08642b27939534f15cb4faf842f7a3380acf1f85ae4b449dc653b233234cdad0ad83b7fe6a5e3b120d39d0d7a341035a18618707fb73
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://rubygems.org/gems/spikard)
|
|
6
6
|
[](https://www.ruby-lang.org/)
|
|
7
7
|
[](LICENSE)
|
|
8
|
-
[](https://codecov.io/gh/Goldziher/spikard)
|
|
9
9
|
[](https://pypi.org/project/spikard/)
|
|
10
10
|
[](https://www.npmjs.com/package/spikard)
|
|
11
11
|
[](https://crates.io/crates/spikard)
|
|
@@ -47,6 +47,39 @@ bundle exec rake ext:build
|
|
|
47
47
|
- Bundler
|
|
48
48
|
- Rust toolchain (for building from source)
|
|
49
49
|
|
|
50
|
+
## Windows Development
|
|
51
|
+
|
|
52
|
+
On Windows, Spikard uses the GNU toolchain (not MSVC) to match Ruby's official RubyInstaller distribution.
|
|
53
|
+
|
|
54
|
+
### Prerequisites
|
|
55
|
+
|
|
56
|
+
1. **Install RubyInstaller with DevKit:**
|
|
57
|
+
- Download from [RubyInstaller.org](https://rubyinstaller.org/downloads/)
|
|
58
|
+
- Choose Ruby+Devkit 3.2.x (x64)
|
|
59
|
+
- During installation, select "MSYS2 development toolchain"
|
|
60
|
+
|
|
61
|
+
2. **Install Rust with GNU target:**
|
|
62
|
+
```powershell
|
|
63
|
+
rustup toolchain install stable-x86_64-pc-windows-gnu
|
|
64
|
+
rustup default stable-x86_64-pc-windows-gnu
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
3. **Verify setup:**
|
|
68
|
+
```powershell
|
|
69
|
+
ruby --version # Should show 3.2.x
|
|
70
|
+
rustup show # Should show *-pc-windows-gnu
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Building on Windows
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd packages/ruby
|
|
77
|
+
bundle install
|
|
78
|
+
bundle exec rake compile
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The build uses the GNU toolchain automatically via RubyInstaller's MSYS2 DevKit. No MSVC configuration needed.
|
|
82
|
+
|
|
50
83
|
## Quick Start
|
|
51
84
|
|
|
52
85
|
```ruby
|
data/ext/spikard_rb/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "spikard-rb-ext"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
|
|
@@ -13,5 +13,5 @@ crate-type = ["cdylib"]
|
|
|
13
13
|
|
|
14
14
|
[dependencies]
|
|
15
15
|
magnus = { git = "https://github.com/matsadler/magnus", rev = "f6db11769efb517427bf7f121f9c32e18b059b38", features = ["rb-sys"] }
|
|
16
|
-
#
|
|
17
|
-
spikard_rb_core = { package = "spikard-rb", path = "
|
|
16
|
+
# Use workspace crate directly (no vendoring for local builds/tests)
|
|
17
|
+
spikard_rb_core = { package = "spikard-rb", path = "../../../../crates/spikard-rb" }
|
data/lib/spikard/app.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Spikard
|
|
|
20
20
|
# request
|
|
21
21
|
# end
|
|
22
22
|
def on_request(&hook)
|
|
23
|
-
|
|
23
|
+
native_hooks.add_on_request(hook)
|
|
24
24
|
hook
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -42,7 +42,7 @@ module Spikard
|
|
|
42
42
|
# end
|
|
43
43
|
# end
|
|
44
44
|
def pre_validation(&hook)
|
|
45
|
-
|
|
45
|
+
native_hooks.add_pre_validation(hook)
|
|
46
46
|
hook
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -64,7 +64,7 @@ module Spikard
|
|
|
64
64
|
# end
|
|
65
65
|
# end
|
|
66
66
|
def pre_handler(&hook)
|
|
67
|
-
|
|
67
|
+
native_hooks.add_pre_handler(hook)
|
|
68
68
|
hook
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -81,7 +81,7 @@ module Spikard
|
|
|
81
81
|
# response
|
|
82
82
|
# end
|
|
83
83
|
def on_response(&hook)
|
|
84
|
-
|
|
84
|
+
native_hooks.add_on_response(hook)
|
|
85
85
|
hook
|
|
86
86
|
end
|
|
87
87
|
|
|
@@ -98,21 +98,16 @@ module Spikard
|
|
|
98
98
|
# response
|
|
99
99
|
# end
|
|
100
100
|
def on_error(&hook)
|
|
101
|
-
|
|
101
|
+
native_hooks.add_on_error(hook)
|
|
102
102
|
hook
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
}
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def native_hooks
|
|
108
|
+
raise 'Spikard native lifecycle registry unavailable' unless defined?(@native_hooks) && @native_hooks
|
|
109
|
+
|
|
110
|
+
@native_hooks
|
|
116
111
|
end
|
|
117
112
|
end
|
|
118
113
|
|
|
@@ -123,7 +118,8 @@ module Spikard
|
|
|
123
118
|
include ProvideSupport
|
|
124
119
|
|
|
125
120
|
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
|
|
121
|
+
SUPPORTED_OPTIONS = %i[request_schema response_schema parameter_schema file_params is_async cors
|
|
122
|
+
body_param_name].freeze
|
|
127
123
|
|
|
128
124
|
attr_reader :routes
|
|
129
125
|
|
|
@@ -131,28 +127,43 @@ module Spikard
|
|
|
131
127
|
@routes = []
|
|
132
128
|
@websocket_handlers = {}
|
|
133
129
|
@sse_producers = {}
|
|
134
|
-
@
|
|
135
|
-
@
|
|
136
|
-
on_request: [],
|
|
137
|
-
pre_validation: [],
|
|
138
|
-
pre_handler: [],
|
|
139
|
-
on_response: [],
|
|
140
|
-
on_error: []
|
|
141
|
-
}
|
|
130
|
+
@native_hooks = Spikard::Native::LifecycleRegistry.new
|
|
131
|
+
@native_dependencies = Spikard::Native::DependencyRegistry.new
|
|
142
132
|
end
|
|
143
133
|
|
|
134
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
144
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
|
|
145
139
|
validate_route_arguments!(block, options)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
140
|
+
metadata = if defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_route_metadata)
|
|
141
|
+
Spikard::Native.build_route_metadata(
|
|
142
|
+
method,
|
|
143
|
+
path,
|
|
144
|
+
handler_name,
|
|
145
|
+
options[:request_schema],
|
|
146
|
+
options[:response_schema],
|
|
147
|
+
options[:parameter_schema],
|
|
148
|
+
options[:file_params],
|
|
149
|
+
options.fetch(:is_async, false),
|
|
150
|
+
options[:cors],
|
|
151
|
+
options[:body_param_name]&.to_s,
|
|
152
|
+
block
|
|
153
|
+
)
|
|
154
|
+
else
|
|
155
|
+
handler_name ||= default_handler_name(method, path)
|
|
156
|
+
|
|
157
|
+
# Extract handler dependencies from block parameters
|
|
158
|
+
handler_dependencies = extract_handler_dependencies(block)
|
|
159
|
+
|
|
160
|
+
build_metadata(method, path, handler_name, options, handler_dependencies)
|
|
161
|
+
end
|
|
152
162
|
|
|
153
163
|
@routes << RouteEntry.new(metadata, block)
|
|
154
164
|
block
|
|
155
165
|
end
|
|
166
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
156
167
|
|
|
157
168
|
HTTP_METHODS.each do |verb|
|
|
158
169
|
define_method(verb.downcase) do |path, handler_name: nil, **options, &block|
|
|
@@ -161,17 +172,7 @@ module Spikard
|
|
|
161
172
|
end
|
|
162
173
|
|
|
163
174
|
def route_metadata
|
|
164
|
-
|
|
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
|
+
@routes.map(&:metadata)
|
|
175
176
|
end
|
|
176
177
|
|
|
177
178
|
def handler_map
|
|
@@ -184,8 +185,20 @@ module Spikard
|
|
|
184
185
|
map
|
|
185
186
|
end
|
|
186
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
|
+
|
|
187
197
|
def default_handler_name(method, path)
|
|
188
|
-
normalized_path = path.gsub(/[^a-zA-Z0-9]+/, '_').gsub(/__+/, '_')
|
|
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}$/, '')
|
|
189
202
|
normalized_path = 'root' if normalized_path.empty?
|
|
190
203
|
"#{method.to_s.downcase}_#{normalized_path}"
|
|
191
204
|
end
|
|
@@ -258,7 +271,7 @@ module Spikard
|
|
|
258
271
|
#
|
|
259
272
|
# @example Backward compatible (deprecated)
|
|
260
273
|
# app.run(host: '0.0.0.0', port: 8000)
|
|
261
|
-
# rubocop:disable Metrics/
|
|
274
|
+
# rubocop:disable Metrics/MethodLength
|
|
262
275
|
def run(config: nil, host: nil, port: nil)
|
|
263
276
|
require 'json'
|
|
264
277
|
|
|
@@ -274,21 +287,20 @@ module Spikard
|
|
|
274
287
|
config = ServerConfig.new(**config)
|
|
275
288
|
end
|
|
276
289
|
|
|
277
|
-
|
|
278
|
-
routes_json = JSON.generate(route_metadata)
|
|
290
|
+
routes_json = normalized_routes_json
|
|
279
291
|
|
|
280
292
|
# Get handler map
|
|
281
293
|
handlers = handler_map
|
|
282
294
|
|
|
283
295
|
# Get lifecycle hooks
|
|
284
|
-
hooks =
|
|
296
|
+
hooks = @native_hooks
|
|
285
297
|
|
|
286
298
|
# Get WebSocket handlers and SSE producers
|
|
287
299
|
ws_handlers = websocket_handlers
|
|
288
300
|
sse_prods = sse_producers
|
|
289
301
|
|
|
290
302
|
# Get dependencies for DI
|
|
291
|
-
deps =
|
|
303
|
+
deps = @native_dependencies
|
|
292
304
|
|
|
293
305
|
# Call the Rust extension's run_server function
|
|
294
306
|
Spikard::Native.run_server(routes_json, handlers, config, hooks, ws_handlers, sse_prods, deps)
|
|
@@ -299,7 +311,7 @@ module Spikard
|
|
|
299
311
|
raise 'Failed to load Spikard extension. ' \
|
|
300
312
|
"Build it with: task build:ruby\n#{e.message}"
|
|
301
313
|
end
|
|
302
|
-
# rubocop:enable Metrics/
|
|
314
|
+
# rubocop:enable Metrics/MethodLength
|
|
303
315
|
|
|
304
316
|
private
|
|
305
317
|
|
data/lib/spikard/converters.rb
CHANGED
|
@@ -1,85 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'upload_file'
|
|
4
|
-
|
|
5
3
|
module Spikard
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# This module handles converting validated JSON data from Rust into Ruby types,
|
|
9
|
-
# particularly for UploadFile instances.
|
|
4
|
+
# Conversion helpers between native Rust values and Ruby types.
|
|
10
5
|
module Converters
|
|
11
6
|
module_function
|
|
12
7
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @param value [Object] Value to check
|
|
16
|
-
# @return [Boolean]
|
|
17
|
-
def file_metadata?(value)
|
|
18
|
-
value.is_a?(Hash) && value.key?('filename') && value.key?('content')
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Convert file metadata hash to UploadFile instance
|
|
22
|
-
#
|
|
23
|
-
# @param file_data [Hash] File metadata from Rust (filename, content, size, content_type)
|
|
24
|
-
# @return [UploadFile] UploadFile instance
|
|
25
|
-
def convert_file_metadata_to_upload_file(file_data)
|
|
26
|
-
UploadFile.new(
|
|
27
|
-
file_data['filename'],
|
|
28
|
-
file_data['content'],
|
|
29
|
-
content_type: file_data['content_type'],
|
|
30
|
-
size: file_data['size'],
|
|
31
|
-
headers: file_data['headers'],
|
|
32
|
-
content_encoding: file_data['content_encoding']
|
|
33
|
-
)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Process handler parameters, converting file metadata to UploadFile instances
|
|
37
|
-
#
|
|
38
|
-
# This method recursively processes the body parameter, looking for file metadata
|
|
39
|
-
# structures and converting them to UploadFile instances.
|
|
40
|
-
#
|
|
41
|
-
# @param value [Object] The value to process (can be Hash, Array, or primitive)
|
|
42
|
-
# @return [Object] Processed value with UploadFile instances
|
|
43
|
-
def process_upload_file_fields(value)
|
|
44
|
-
# Handle nil
|
|
45
|
-
return value if value.nil?
|
|
46
|
-
|
|
47
|
-
# Handle primitives (String, Numeric, Boolean)
|
|
48
|
-
return value unless value.is_a?(Hash) || value.is_a?(Array)
|
|
49
|
-
|
|
50
|
-
# Handle arrays - recursively process each element
|
|
51
|
-
if value.is_a?(Array)
|
|
52
|
-
return value.map do |item|
|
|
53
|
-
# Check if this array item is file metadata
|
|
54
|
-
if file_metadata?(item)
|
|
55
|
-
convert_file_metadata_to_upload_file(item)
|
|
56
|
-
else
|
|
57
|
-
# Recursively process nested arrays/hashes
|
|
58
|
-
process_upload_file_fields(item)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Handle hashes - check if it's file metadata first
|
|
64
|
-
return convert_file_metadata_to_upload_file(value) if file_metadata?(value)
|
|
65
|
-
|
|
66
|
-
# Otherwise, recursively process hash values
|
|
67
|
-
value.transform_values { |v| process_upload_file_fields(v) }
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Process handler body parameter, handling UploadFile conversion
|
|
71
|
-
#
|
|
72
|
-
# This is the main entry point for converting Rust-provided request data
|
|
73
|
-
# into Ruby types. It handles:
|
|
74
|
-
# - Single UploadFile
|
|
75
|
-
# - Arrays of UploadFile
|
|
76
|
-
# - Hashes with UploadFile fields
|
|
77
|
-
# - Nested structures
|
|
78
|
-
#
|
|
79
|
-
# @param body [Object] The body parameter from Rust (already JSON-parsed)
|
|
80
|
-
# @return [Object] Processed body with UploadFile instances
|
|
8
|
+
# No-op conversion now that Rust materialises UploadFile.
|
|
81
9
|
def convert_handler_body(body)
|
|
82
|
-
|
|
10
|
+
body
|
|
83
11
|
end
|
|
84
12
|
end
|
|
85
13
|
end
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
require_relative 'converters'
|
|
4
4
|
|
|
5
5
|
module Spikard
|
|
6
|
-
# Handler wrapper utilities
|
|
6
|
+
# Handler wrapper utilities.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# UploadFile conversion now happens in the Rust binding, so these wrappers
|
|
9
|
+
# simply forward the already-converted body/params.
|
|
10
10
|
#
|
|
11
11
|
# @example Basic usage with body only
|
|
12
12
|
# app.post('/upload', &wrap_body_handler do |body|
|
|
@@ -46,8 +46,7 @@ module Spikard
|
|
|
46
46
|
# Return a proc that matches the signature expected by Spikard::App
|
|
47
47
|
# The actual handler receives path params, query params, and body from Rust
|
|
48
48
|
lambda do |_params, _query, body|
|
|
49
|
-
|
|
50
|
-
handler.call(converted_body)
|
|
49
|
+
handler.call(body)
|
|
51
50
|
end
|
|
52
51
|
end
|
|
53
52
|
|
|
@@ -74,8 +73,7 @@ module Spikard
|
|
|
74
73
|
raise ArgumentError, 'block required for wrap_handler' unless handler
|
|
75
74
|
|
|
76
75
|
lambda do |params, query, body|
|
|
77
|
-
|
|
78
|
-
handler.call(params, query, converted_body)
|
|
76
|
+
handler.call(params, query, body)
|
|
79
77
|
end
|
|
80
78
|
end
|
|
81
79
|
|
|
@@ -103,11 +101,10 @@ module Spikard
|
|
|
103
101
|
raise ArgumentError, 'block required for wrap_handler_with_context' unless handler
|
|
104
102
|
|
|
105
103
|
lambda do |params, query, body|
|
|
106
|
-
converted_body = Converters.convert_handler_body(body)
|
|
107
104
|
context = {
|
|
108
105
|
params: params,
|
|
109
106
|
query: query,
|
|
110
|
-
body:
|
|
107
|
+
body: body
|
|
111
108
|
}
|
|
112
109
|
handler.call(context)
|
|
113
110
|
end
|
data/lib/spikard/provide.rb
CHANGED
|
@@ -107,53 +107,39 @@ module Spikard
|
|
|
107
107
|
#
|
|
108
108
|
# @example Using Provide wrapper
|
|
109
109
|
# app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
|
|
110
|
-
# rubocop:disable Metrics/MethodLength
|
|
111
110
|
def provide(key, value = nil, depends_on: [], singleton: false, cacheable: true, &block)
|
|
112
111
|
key_str = key.to_s
|
|
113
|
-
|
|
112
|
+
registry = ensure_native_dependencies!
|
|
114
113
|
|
|
115
114
|
# Handle Provide wrapper instances
|
|
116
115
|
if value.is_a?(Provide)
|
|
117
|
-
|
|
118
|
-
@dependencies[key_str] = {
|
|
119
|
-
type: :factory,
|
|
120
|
-
factory: provider.factory,
|
|
121
|
-
depends_on: provider.depends_on,
|
|
122
|
-
singleton: provider.singleton,
|
|
123
|
-
cacheable: provider.cacheable
|
|
124
|
-
}
|
|
116
|
+
registry.register_factory(key_str, value.factory, value.depends_on, value.singleton, value.cacheable)
|
|
125
117
|
elsif block
|
|
126
|
-
|
|
127
|
-
@dependencies[key_str] = {
|
|
128
|
-
type: :factory,
|
|
129
|
-
factory: block,
|
|
130
|
-
depends_on: Array(depends_on).map(&:to_s),
|
|
131
|
-
singleton: singleton,
|
|
132
|
-
cacheable: cacheable
|
|
133
|
-
}
|
|
118
|
+
registry.register_factory(key_str, block, Array(depends_on).map(&:to_s), singleton, cacheable)
|
|
134
119
|
else
|
|
135
|
-
# Value dependency
|
|
136
120
|
raise ArgumentError, 'Either provide a value or a block, not both' if value.nil?
|
|
137
121
|
|
|
138
|
-
|
|
139
|
-
type: :value,
|
|
140
|
-
value: value,
|
|
141
|
-
singleton: true, # Values are always singleton
|
|
142
|
-
cacheable: true
|
|
143
|
-
}
|
|
122
|
+
registry.register_value(key_str, value)
|
|
144
123
|
end
|
|
145
124
|
|
|
146
125
|
self
|
|
147
126
|
end
|
|
148
|
-
# rubocop:enable Metrics/MethodLength
|
|
149
127
|
|
|
150
128
|
# Get all registered dependencies
|
|
151
129
|
#
|
|
152
130
|
# @return [Hash] Dictionary mapping dependency keys to their definitions
|
|
153
131
|
# @api private
|
|
154
132
|
def dependencies
|
|
155
|
-
|
|
156
|
-
|
|
133
|
+
ensure_native_dependencies!
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def ensure_native_dependencies!
|
|
139
|
+
registry = (@native_dependencies if instance_variable_defined?(:@native_dependencies) && @native_dependencies)
|
|
140
|
+
raise 'Spikard native dependency registry unavailable' unless registry
|
|
141
|
+
|
|
142
|
+
registry
|
|
157
143
|
end
|
|
158
144
|
end
|
|
159
145
|
|
data/lib/spikard/response.rb
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# ⚠️ GENERATED BY crates/spikard-rb/build.rs — DO NOT EDIT BY HAND
|
|
3
4
|
module Spikard
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# can extract status, headers, and JSON-serialisable content.
|
|
7
|
-
class Response
|
|
8
|
-
attr_accessor :content
|
|
9
|
-
attr_reader :status_code, :headers
|
|
5
|
+
class Response # :nodoc: Native-backed HTTP response facade generated from Rust metadata.
|
|
6
|
+
attr_reader :content, :status_code, :headers, :native_response
|
|
10
7
|
|
|
11
8
|
def initialize(content: nil, body: nil, status_code: 200, headers: nil, content_type: nil)
|
|
12
9
|
@content = content.nil? ? body : content
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
@status_code = Integer(status_code || 200)
|
|
11
|
+
@headers = normalize_headers(headers)
|
|
15
12
|
set_header('content-type', content_type) if content_type
|
|
13
|
+
rebuild_native!
|
|
16
14
|
end
|
|
17
15
|
|
|
18
16
|
def status
|
|
@@ -21,16 +19,24 @@ module Spikard
|
|
|
21
19
|
|
|
22
20
|
def status_code=(value)
|
|
23
21
|
@status_code = Integer(value)
|
|
22
|
+
rebuild_native!
|
|
24
23
|
rescue ArgumentError, TypeError
|
|
25
24
|
raise ArgumentError, 'status_code must be an integer'
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
def headers=(value)
|
|
29
28
|
@headers = normalize_headers(value)
|
|
29
|
+
rebuild_native!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def content=(value)
|
|
33
|
+
@content = value
|
|
34
|
+
rebuild_native!
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
def set_header(name, value)
|
|
33
38
|
@headers[name.to_s] = value.to_s
|
|
39
|
+
rebuild_native!
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
def set_cookie(name, value, **options)
|
|
@@ -40,8 +46,27 @@ module Spikard
|
|
|
40
46
|
set_header('set-cookie', header_value)
|
|
41
47
|
end
|
|
42
48
|
|
|
49
|
+
def to_native_response
|
|
50
|
+
@native_response
|
|
51
|
+
end
|
|
52
|
+
|
|
43
53
|
private
|
|
44
54
|
|
|
55
|
+
def rebuild_native!
|
|
56
|
+
ensure_native!
|
|
57
|
+
@native_response = Spikard::Native.build_response(@content, @status_code, @headers)
|
|
58
|
+
return unless @native_response
|
|
59
|
+
|
|
60
|
+
@status_code = @native_response.status_code
|
|
61
|
+
@headers = @native_response.headers
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def ensure_native!
|
|
65
|
+
return if defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_response)
|
|
66
|
+
|
|
67
|
+
raise 'Spikard native extension is not loaded'
|
|
68
|
+
end
|
|
69
|
+
|
|
45
70
|
def cookie_parts(options)
|
|
46
71
|
[
|
|
47
72
|
options[:max_age] && "Max-Age=#{Integer(options[:max_age])}",
|
|
@@ -59,7 +84,7 @@ module Spikard
|
|
|
59
84
|
{}
|
|
60
85
|
when Hash
|
|
61
86
|
value.each_with_object({}) do |(key, val), acc|
|
|
62
|
-
acc[key.to_s] = val.to_s
|
|
87
|
+
acc[key.to_s.downcase] = val.to_s
|
|
63
88
|
end
|
|
64
89
|
else
|
|
65
90
|
raise ArgumentError, 'headers must be a Hash'
|
|
@@ -67,9 +92,48 @@ module Spikard
|
|
|
67
92
|
end
|
|
68
93
|
end
|
|
69
94
|
|
|
95
|
+
class StreamingResponse # :nodoc: Streaming response wrapper backed by the native Rust builder.
|
|
96
|
+
attr_reader :stream, :status_code, :headers, :native_response
|
|
97
|
+
|
|
98
|
+
def initialize(stream, status_code: 200, headers: nil)
|
|
99
|
+
unless stream.respond_to?(:next) || stream.respond_to?(:each)
|
|
100
|
+
raise ArgumentError, 'StreamingResponse requires an object responding to #next or #each'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
@stream = stream.respond_to?(:to_enum) ? stream.to_enum : stream
|
|
104
|
+
@status_code = Integer(status_code || 200)
|
|
105
|
+
header_hash = headers || {}
|
|
106
|
+
@headers = header_hash.each_with_object({}) do |(key, value), memo|
|
|
107
|
+
memo[String(key)] = String(value)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
rebuild_native!
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def to_native_response
|
|
114
|
+
@native_response
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def rebuild_native!
|
|
120
|
+
ensure_native!
|
|
121
|
+
@native_response = Spikard::Native.build_streaming_response(@stream, @status_code, @headers)
|
|
122
|
+
return unless @native_response
|
|
123
|
+
|
|
124
|
+
@status_code = @native_response.status_code
|
|
125
|
+
@headers = @native_response.headers
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def ensure_native!
|
|
129
|
+
return if defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_streaming_response)
|
|
130
|
+
|
|
131
|
+
raise 'Spikard native extension is not loaded'
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
70
135
|
module Testing
|
|
71
|
-
# Lightweight wrapper
|
|
72
|
-
class Response
|
|
136
|
+
class Response # :nodoc: Lightweight response wrapper used by the test client.
|
|
73
137
|
attr_reader :status_code, :headers, :body
|
|
74
138
|
|
|
75
139
|
def initialize(payload)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Spikard
|
|
4
4
|
# Represents a streaming HTTP response made of chunks produced lazily.
|
|
5
5
|
class StreamingResponse
|
|
6
|
-
attr_reader :stream, :status_code, :headers
|
|
6
|
+
attr_reader :stream, :status_code, :headers, :native_response
|
|
7
7
|
|
|
8
8
|
def initialize(stream, status_code: 200, headers: nil)
|
|
9
9
|
unless stream.respond_to?(:next) || stream.respond_to?(:each)
|
|
@@ -16,6 +16,29 @@ module Spikard
|
|
|
16
16
|
@headers = header_hash.each_with_object({}) do |(key, value), memo|
|
|
17
17
|
memo[String(key)] = String(value)
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
rebuild_native!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_native_response
|
|
24
|
+
@native_response
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def rebuild_native!
|
|
30
|
+
ensure_native!
|
|
31
|
+
@native_response = Spikard::Native.build_streaming_response(@stream, @status_code, @headers)
|
|
32
|
+
return unless @native_response
|
|
33
|
+
|
|
34
|
+
@status_code = @native_response.status_code
|
|
35
|
+
@headers = @native_response.headers
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ensure_native!
|
|
39
|
+
return if defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_streaming_response)
|
|
40
|
+
|
|
41
|
+
raise 'Spikard native extension is not loaded'
|
|
19
42
|
end
|
|
20
43
|
end
|
|
21
44
|
end
|
data/lib/spikard/testing.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Spikard
|
|
|
20
20
|
# Use default config if none provided
|
|
21
21
|
config ||= Spikard::ServerConfig.new
|
|
22
22
|
|
|
23
|
-
routes_json =
|
|
23
|
+
routes_json = app.normalized_routes_json
|
|
24
24
|
handlers = app.handler_map.transform_keys(&:to_sym)
|
|
25
25
|
ws_handlers = app.websocket_handlers || {}
|
|
26
26
|
sse_producers = app.sse_producers || {}
|