spikard 0.1.1 → 0.2.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 +90 -17
- data/ext/spikard_rb/Cargo.toml +1 -1
- data/lib/spikard/app.rb +50 -4
- data/lib/spikard/provide.rb +228 -0
- data/lib/spikard/testing.rb +2 -1
- data/lib/spikard/version.rb +1 -1
- data/lib/spikard.rb +1 -0
- data/sig/spikard.rbs +14 -1
- metadata +22 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1f75c9c00309eba12ce9edad11b8469746dcb1eba392f2987e522816dbdf9a0
|
|
4
|
+
data.tar.gz: cad1676a6d180be90cbf412d54470eb0a582eb0f65e7a880cdc546c3ab14a52e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8af5d8195a604c05fc4b5f3c9e20bce22f92f31db1b5a025a30aa971f94cb4cf8c5ecec2bfa2f7626aff0267ad21b4b4a0e12ee1659e856ca4277dea93451e1
|
|
7
|
+
data.tar.gz: d4ccbff9878b866119b20800223dc82e6cb744fe2a06fccc8c6017826c3ea1ec1810618c0143e513e6304a655dae536d23b6856777749be2053603e1a19b2f4d
|
data/README.md
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
# Spikard Ruby
|
|
2
2
|
|
|
3
|
-
[](https://spikard.dev)
|
|
4
|
+
[](https://rubygems.org/gems/spikard)
|
|
5
|
+
[](https://rubygems.org/gems/spikard)
|
|
6
|
+
[](https://www.ruby-lang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/Goldziher/spikard/actions)
|
|
9
|
+
[](https://pypi.org/project/spikard/)
|
|
10
|
+
[](https://www.npmjs.com/package/@spikard/node)
|
|
11
|
+
[](https://crates.io/crates/spikard)
|
|
12
|
+
[](https://packagist.org/packages/spikard/spikard)
|
|
13
|
+
|
|
14
|
+
High-performance Ruby web framework with a Rust core. Build REST APIs with Sinatra-style routing and zero-overhead async handlers backed by Axum and Tower-HTTP.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Rust-powered performance**: High-throughput HTTP server backed by Tokio and Axum
|
|
19
|
+
- **Sinatra-style routing**: Familiar `get`, `post`, `put`, `patch`, `delete` DSL
|
|
20
|
+
- **Type-safe with RBS**: Full RBS type definitions for Steep type checking
|
|
21
|
+
- **Zero-copy serialization**: Direct Rust-to-Ruby object conversion via Magnus
|
|
22
|
+
- **Async-first**: Non-blocking handlers with full async/await support
|
|
23
|
+
- **Middleware stack**: Compression, rate limiting, request IDs, authentication
|
|
24
|
+
- **WebSockets & SSE**: Native real-time communication primitives
|
|
25
|
+
- **Request validation**: JSON Schema and dry-schema support
|
|
26
|
+
- **Lifecycle hooks**: onRequest, preValidation, preHandler, onResponse, onError
|
|
27
|
+
- **Dependency injection**: Built-in container for services and factories
|
|
10
28
|
|
|
11
29
|
## Installation
|
|
12
30
|
|
|
13
|
-
**
|
|
31
|
+
**Via RubyGems (recommended):**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
gem install spikard
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**From source (development):**
|
|
14
38
|
|
|
15
39
|
```bash
|
|
16
40
|
cd packages/ruby
|
|
@@ -19,14 +43,20 @@ bundle exec rake ext:build
|
|
|
19
43
|
```
|
|
20
44
|
|
|
21
45
|
**Requirements:**
|
|
22
|
-
- Ruby 3.2
|
|
46
|
+
- Ruby 3.2 or later
|
|
23
47
|
- Bundler
|
|
24
|
-
- Rust toolchain (for building
|
|
48
|
+
- Rust toolchain (for building from source)
|
|
25
49
|
|
|
26
50
|
## Quick Start
|
|
27
51
|
|
|
28
52
|
```ruby
|
|
29
53
|
require "spikard"
|
|
54
|
+
require "dry-schema"
|
|
55
|
+
|
|
56
|
+
UserSchema = Dry::Schema.JSON do
|
|
57
|
+
required(:name).filled(:str?)
|
|
58
|
+
required(:email).filled(:str?)
|
|
59
|
+
end
|
|
30
60
|
|
|
31
61
|
app = Spikard::App.new
|
|
32
62
|
|
|
@@ -35,8 +65,9 @@ app.get "/users/:id" do |request|
|
|
|
35
65
|
{ id: user_id, name: "Alice" }
|
|
36
66
|
end
|
|
37
67
|
|
|
38
|
-
app.post "/users" do |request|
|
|
39
|
-
|
|
68
|
+
app.post "/users", request_schema: UserSchema do |request|
|
|
69
|
+
body = request[:body]
|
|
70
|
+
{ id: 1, name: body["name"], email: body["email"] }
|
|
40
71
|
end
|
|
41
72
|
|
|
42
73
|
app.run(port: 8000)
|
|
@@ -171,6 +202,21 @@ app.post "/users", request_schema: user_schema do |request|
|
|
|
171
202
|
end
|
|
172
203
|
```
|
|
173
204
|
|
|
205
|
+
## Dependency Injection
|
|
206
|
+
|
|
207
|
+
Register values or factories and inject them as keyword parameters:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
app.provide("config", { "db_url" => "postgresql://localhost/app" })
|
|
211
|
+
app.provide("db_pool", depends_on: ["config"], singleton: true) do |config:|
|
|
212
|
+
{ url: config["db_url"], driver: "pool" }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
app.get "/stats" do |_params, _query, _body, config:, db_pool:|
|
|
216
|
+
{ db: db_pool[:url], env: config["db_url"] }
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
174
220
|
### With dry-struct
|
|
175
221
|
|
|
176
222
|
```ruby
|
|
@@ -540,14 +586,41 @@ Ruby bindings use:
|
|
|
540
586
|
|
|
541
587
|
## Examples
|
|
542
588
|
|
|
543
|
-
|
|
589
|
+
The [examples directory](../../examples/) contains comprehensive demonstrations:
|
|
590
|
+
|
|
591
|
+
**Ruby-specific examples:**
|
|
592
|
+
- [Basic Ruby Example](../../examples/di/ruby_basic.rb) - Simple server with DI
|
|
593
|
+
- [Database Integration](../../examples/di/ruby_database.rb) - DI with database pools
|
|
594
|
+
- Additional examples in [examples/](../../examples/)
|
|
595
|
+
|
|
596
|
+
**API Schemas** (language-agnostic, can be used with code generation):
|
|
597
|
+
- [Todo API](../../examples/schemas/todo-api.openapi.yaml) - REST CRUD with validation
|
|
598
|
+
- [File Service](../../examples/schemas/file-service.openapi.yaml) - File uploads/downloads
|
|
599
|
+
- [Auth Service](../../examples/schemas/auth-service.openapi.yaml) - JWT, API keys, OAuth
|
|
600
|
+
- [Chat Service](../../examples/schemas/chat-service.asyncapi.yaml) - WebSocket messaging
|
|
601
|
+
- [Event Streams](../../examples/schemas/events-stream.asyncapi.yaml) - SSE streaming
|
|
602
|
+
|
|
603
|
+
See [examples/README.md](../../examples/README.md) for code generation instructions.
|
|
544
604
|
|
|
545
605
|
## Documentation
|
|
546
606
|
|
|
547
|
-
|
|
548
|
-
- [
|
|
549
|
-
- [
|
|
607
|
+
**API Reference & Guides:**
|
|
608
|
+
- [Type Definitions (RBS)](sig/spikard.rbs) - Full type signatures for Steep
|
|
609
|
+
- [Configuration Reference](lib/spikard/config.rb) - ServerConfig and middleware options
|
|
610
|
+
- [Handler Documentation](lib/spikard/handler_wrapper.rb) - Request/response handling
|
|
611
|
+
|
|
612
|
+
**Project Resources:**
|
|
613
|
+
- [Main Project README](../../README.md) - Spikard overview and multi-language ecosystem
|
|
614
|
+
- [Contributing Guide](../../CONTRIBUTING.md) - Development guidelines
|
|
615
|
+
- [Architecture Decisions](../../docs/adr/) - ADRs on design choices
|
|
616
|
+
- [Examples](../../examples/ruby/) - Runnable example applications
|
|
617
|
+
|
|
618
|
+
**Cross-Language:**
|
|
619
|
+
- [Python (PyPI)](https://pypi.org/project/spikard/)
|
|
620
|
+
- [Node.js (npm)](https://www.npmjs.com/package/@spikard/node)
|
|
621
|
+
- [Rust (Crates.io)](https://crates.io/crates/spikard)
|
|
622
|
+
- [PHP (Packagist)](https://packagist.org/packages/spikard/spikard)
|
|
550
623
|
|
|
551
624
|
## License
|
|
552
625
|
|
|
553
|
-
MIT
|
|
626
|
+
MIT - See [LICENSE](../../LICENSE) for details
|
data/ext/spikard_rb/Cargo.toml
CHANGED
data/lib/spikard/app.rb
CHANGED
|
@@ -120,6 +120,7 @@ module Spikard
|
|
|
120
120
|
# rubocop:disable Metrics/ClassLength
|
|
121
121
|
class App
|
|
122
122
|
include LifecycleHooks
|
|
123
|
+
include ProvideSupport
|
|
123
124
|
|
|
124
125
|
HTTP_METHODS = %w[GET POST PUT PATCH DELETE OPTIONS HEAD TRACE].freeze
|
|
125
126
|
SUPPORTED_OPTIONS = %i[request_schema response_schema parameter_schema file_params is_async cors].freeze
|
|
@@ -130,6 +131,7 @@ module Spikard
|
|
|
130
131
|
@routes = []
|
|
131
132
|
@websocket_handlers = {}
|
|
132
133
|
@sse_producers = {}
|
|
134
|
+
@dependencies = {}
|
|
133
135
|
@lifecycle_hooks = {
|
|
134
136
|
on_request: [],
|
|
135
137
|
pre_validation: [],
|
|
@@ -142,7 +144,11 @@ module Spikard
|
|
|
142
144
|
def register_route(method, path, handler_name: nil, **options, &block)
|
|
143
145
|
validate_route_arguments!(block, options)
|
|
144
146
|
handler_name ||= default_handler_name(method, path)
|
|
145
|
-
|
|
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)
|
|
146
152
|
|
|
147
153
|
@routes << RouteEntry.new(metadata, block)
|
|
148
154
|
block
|
|
@@ -155,13 +161,24 @@ module Spikard
|
|
|
155
161
|
end
|
|
156
162
|
|
|
157
163
|
def route_metadata
|
|
158
|
-
|
|
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
|
|
159
175
|
end
|
|
160
176
|
|
|
161
177
|
def handler_map
|
|
162
178
|
map = {}
|
|
163
179
|
@routes.each do |entry|
|
|
164
180
|
name = entry.metadata[:handler_name]
|
|
181
|
+
# Pass raw handler - DI resolution happens in Rust layer
|
|
165
182
|
map[name] = entry.handler
|
|
166
183
|
end
|
|
167
184
|
map
|
|
@@ -270,8 +287,11 @@ module Spikard
|
|
|
270
287
|
ws_handlers = websocket_handlers
|
|
271
288
|
sse_prods = sse_producers
|
|
272
289
|
|
|
290
|
+
# Get dependencies for DI
|
|
291
|
+
deps = dependencies
|
|
292
|
+
|
|
273
293
|
# Call the Rust extension's run_server function
|
|
274
|
-
Spikard::Native.run_server(routes_json, handlers, config, hooks, ws_handlers, sse_prods)
|
|
294
|
+
Spikard::Native.run_server(routes_json, handlers, config, hooks, ws_handlers, sse_prods, deps)
|
|
275
295
|
|
|
276
296
|
# Keep Ruby process alive while server runs
|
|
277
297
|
sleep
|
|
@@ -309,7 +329,30 @@ module Spikard
|
|
|
309
329
|
raise ArgumentError, "unknown route options: #{unknown_keys.join(', ')}"
|
|
310
330
|
end
|
|
311
331
|
|
|
312
|
-
def
|
|
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)
|
|
313
356
|
base = {
|
|
314
357
|
method: method,
|
|
315
358
|
path: normalize_path(path),
|
|
@@ -317,6 +360,9 @@ module Spikard
|
|
|
317
360
|
is_async: options.fetch(:is_async, false)
|
|
318
361
|
}
|
|
319
362
|
|
|
363
|
+
# Add handler_dependencies if present
|
|
364
|
+
base[:handler_dependencies] = handler_dependencies unless handler_dependencies.empty?
|
|
365
|
+
|
|
320
366
|
SUPPORTED_OPTIONS.each_with_object(base) do |key, metadata|
|
|
321
367
|
next if key == :is_async || !options.key?(key)
|
|
322
368
|
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spikard
|
|
4
|
+
# Wrapper class for dependency providers
|
|
5
|
+
#
|
|
6
|
+
# This class wraps factory functions and configuration for dependency injection.
|
|
7
|
+
# It provides a consistent API across Python, Node.js, and Ruby bindings.
|
|
8
|
+
#
|
|
9
|
+
# @example Factory with caching
|
|
10
|
+
# app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
|
|
11
|
+
#
|
|
12
|
+
# @example Factory with dependencies
|
|
13
|
+
# app.provide("auth", Spikard::Provide.new(
|
|
14
|
+
# method("create_auth_service"),
|
|
15
|
+
# depends_on: ["db", "cache"],
|
|
16
|
+
# singleton: true
|
|
17
|
+
# ))
|
|
18
|
+
class Provide
|
|
19
|
+
attr_reader :factory, :depends_on, :singleton, :cacheable
|
|
20
|
+
|
|
21
|
+
# Create a new dependency provider
|
|
22
|
+
#
|
|
23
|
+
# @param factory [Proc, Method] The factory function that creates the dependency value
|
|
24
|
+
# @param depends_on [Array<String, Symbol>] List of dependency keys this factory depends on
|
|
25
|
+
# @param singleton [Boolean] Whether to cache the value globally (default: false)
|
|
26
|
+
# @param cacheable [Boolean] Whether to cache the value per-request (default: true)
|
|
27
|
+
def initialize(factory, depends_on: [], singleton: false, cacheable: true)
|
|
28
|
+
@factory = factory
|
|
29
|
+
@depends_on = Array(depends_on).map(&:to_s)
|
|
30
|
+
@singleton = singleton
|
|
31
|
+
@cacheable = cacheable
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check if the factory is async (based on method arity or other heuristics)
|
|
35
|
+
#
|
|
36
|
+
# @return [Boolean] True if the factory appears to be async
|
|
37
|
+
def async?
|
|
38
|
+
# Ruby doesn't have explicit async/await like Python/JS
|
|
39
|
+
# We could check if it returns a Thread or uses Fiber
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if the factory is an async generator
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] True if the factory is an async generator
|
|
46
|
+
def async_generator?
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Dependency Injection support for Spikard applications
|
|
52
|
+
#
|
|
53
|
+
# Provides methods for registering and managing dependencies that can be
|
|
54
|
+
# automatically injected into route handlers.
|
|
55
|
+
#
|
|
56
|
+
# @example Registering a value dependency
|
|
57
|
+
# app.provide("database_url", "postgresql://localhost/mydb")
|
|
58
|
+
#
|
|
59
|
+
# @example Registering a factory dependency
|
|
60
|
+
# app.provide("db_pool", depends_on: ["database_url"]) do |database_url:|
|
|
61
|
+
# ConnectionPool.new(database_url)
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# @example Singleton dependency (shared across all requests)
|
|
65
|
+
# app.provide("config", singleton: true) do
|
|
66
|
+
# Config.load_from_file("config.yml")
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# @example Using Provide wrapper
|
|
70
|
+
# app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
|
|
71
|
+
module ProvideSupport
|
|
72
|
+
# Register a dependency in the DI container
|
|
73
|
+
#
|
|
74
|
+
# This method supports three patterns:
|
|
75
|
+
# 1. **Value dependency**: Pass a value directly (e.g., string, number, object)
|
|
76
|
+
# 2. **Factory dependency**: Pass a block that computes the value
|
|
77
|
+
# 3. **Provide wrapper**: Pass a Spikard::Provide instance
|
|
78
|
+
#
|
|
79
|
+
# @param key [String, Symbol] Unique identifier for the dependency
|
|
80
|
+
# @param value [Object, Provide, nil] Static value, Provide instance, or nil
|
|
81
|
+
# @param depends_on [Array<String, Symbol>] List of dependency keys this factory depends on
|
|
82
|
+
# @param singleton [Boolean] Whether to cache the value globally (default: false)
|
|
83
|
+
# @param cacheable [Boolean] Whether to cache the value per-request (default: true)
|
|
84
|
+
# @yield Optional factory block that receives dependencies as keyword arguments
|
|
85
|
+
# @yieldparam **deps [Hash] Resolved dependencies as keyword arguments
|
|
86
|
+
# @yieldreturn [Object] The computed dependency value
|
|
87
|
+
# @return [self] Returns self for method chaining
|
|
88
|
+
#
|
|
89
|
+
# @example Value dependency
|
|
90
|
+
# app.provide("app_name", "MyApp")
|
|
91
|
+
# app.provide("port", 8080)
|
|
92
|
+
#
|
|
93
|
+
# @example Factory with dependencies
|
|
94
|
+
# app.provide("database", depends_on: ["config"]) do |config:|
|
|
95
|
+
# Database.connect(config["db_url"])
|
|
96
|
+
# end
|
|
97
|
+
#
|
|
98
|
+
# @example Singleton factory
|
|
99
|
+
# app.provide("thread_pool", singleton: true) do
|
|
100
|
+
# ThreadPool.new(size: 10)
|
|
101
|
+
# end
|
|
102
|
+
#
|
|
103
|
+
# @example Non-cacheable factory (resolves every time)
|
|
104
|
+
# app.provide("request_id", cacheable: false) do
|
|
105
|
+
# SecureRandom.uuid
|
|
106
|
+
# end
|
|
107
|
+
#
|
|
108
|
+
# @example Using Provide wrapper
|
|
109
|
+
# app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
|
|
110
|
+
# rubocop:disable Metrics/MethodLength
|
|
111
|
+
def provide(key, value = nil, depends_on: [], singleton: false, cacheable: true, &block)
|
|
112
|
+
key_str = key.to_s
|
|
113
|
+
@dependencies ||= {}
|
|
114
|
+
|
|
115
|
+
# Handle Provide wrapper instances
|
|
116
|
+
if value.is_a?(Provide)
|
|
117
|
+
provider = value
|
|
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
|
+
}
|
|
125
|
+
elsif block
|
|
126
|
+
# Factory dependency (block form)
|
|
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
|
+
}
|
|
134
|
+
else
|
|
135
|
+
# Value dependency
|
|
136
|
+
raise ArgumentError, 'Either provide a value or a block, not both' if value.nil?
|
|
137
|
+
|
|
138
|
+
@dependencies[key_str] = {
|
|
139
|
+
type: :value,
|
|
140
|
+
value: value,
|
|
141
|
+
singleton: true, # Values are always singleton
|
|
142
|
+
cacheable: true
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
self
|
|
147
|
+
end
|
|
148
|
+
# rubocop:enable Metrics/MethodLength
|
|
149
|
+
|
|
150
|
+
# Get all registered dependencies
|
|
151
|
+
#
|
|
152
|
+
# @return [Hash] Dictionary mapping dependency keys to their definitions
|
|
153
|
+
# @api private
|
|
154
|
+
def dependencies
|
|
155
|
+
@dependencies ||= {}
|
|
156
|
+
@dependencies.dup
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Dependency injection handler wrapper
|
|
161
|
+
#
|
|
162
|
+
# Wraps a route handler to inject dependencies based on parameter names.
|
|
163
|
+
# Dependencies are resolved from the DI container and passed as keyword arguments.
|
|
164
|
+
#
|
|
165
|
+
# @api private
|
|
166
|
+
module DIHandlerWrapper
|
|
167
|
+
# Wrap a handler to inject dependencies
|
|
168
|
+
#
|
|
169
|
+
# @param handler [Proc] The original route handler
|
|
170
|
+
# @param dependencies [Hash] Available dependencies from the app
|
|
171
|
+
# @return [Proc] Wrapped handler with DI support
|
|
172
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
173
|
+
def self.wrap_handler(handler, dependencies)
|
|
174
|
+
# Extract parameter names from the handler
|
|
175
|
+
params = handler.parameters.map { |_type, name| name.to_s }
|
|
176
|
+
|
|
177
|
+
# Find which parameters match registered dependencies
|
|
178
|
+
injectable_params = params & dependencies.keys
|
|
179
|
+
|
|
180
|
+
if injectable_params.empty?
|
|
181
|
+
# No DI needed, return original handler
|
|
182
|
+
return handler
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Create wrapped handler that injects dependencies
|
|
186
|
+
lambda do |request|
|
|
187
|
+
# Build kwargs with injected dependencies
|
|
188
|
+
kwargs = {}
|
|
189
|
+
|
|
190
|
+
injectable_params.each do |param_name|
|
|
191
|
+
dep_def = dependencies[param_name]
|
|
192
|
+
kwargs[param_name.to_sym] = resolve_dependency(dep_def, request)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Call original handler with injected dependencies
|
|
196
|
+
if handler.arity.zero?
|
|
197
|
+
# Handler takes no arguments (dependencies injected via closure or instance vars)
|
|
198
|
+
handler.call
|
|
199
|
+
elsif injectable_params.length == params.length
|
|
200
|
+
# All parameters are dependencies
|
|
201
|
+
handler.call(**kwargs)
|
|
202
|
+
else
|
|
203
|
+
# Mix of request data and dependencies
|
|
204
|
+
handler.call(request, **kwargs)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
209
|
+
|
|
210
|
+
# Resolve a dependency definition
|
|
211
|
+
#
|
|
212
|
+
# @param dep_def [Hash] Dependency definition
|
|
213
|
+
# @param request [Hash] Request context (unused for now, future: per-request deps)
|
|
214
|
+
# @return [Object] Resolved dependency value
|
|
215
|
+
# @api private
|
|
216
|
+
def self.resolve_dependency(dep_def, _request)
|
|
217
|
+
case dep_def[:type]
|
|
218
|
+
when :value
|
|
219
|
+
dep_def[:value]
|
|
220
|
+
when :factory
|
|
221
|
+
factory = dep_def[:factory]
|
|
222
|
+
dep_def[:depends_on]
|
|
223
|
+
# TODO: Implement nested dependency resolution when dependencies are provided
|
|
224
|
+
factory.call
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
data/lib/spikard/testing.rb
CHANGED
|
@@ -24,7 +24,8 @@ module Spikard
|
|
|
24
24
|
handlers = app.handler_map.transform_keys(&:to_sym)
|
|
25
25
|
ws_handlers = app.websocket_handlers || {}
|
|
26
26
|
sse_producers = app.sse_producers || {}
|
|
27
|
-
|
|
27
|
+
dependencies = app.dependencies || {}
|
|
28
|
+
native = Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers, dependencies)
|
|
28
29
|
TestClient.new(native)
|
|
29
30
|
end
|
|
30
31
|
|
data/lib/spikard/version.rb
CHANGED
data/lib/spikard.rb
CHANGED
|
@@ -20,6 +20,7 @@ require_relative 'spikard/websocket'
|
|
|
20
20
|
require_relative 'spikard/sse'
|
|
21
21
|
require_relative 'spikard/upload_file'
|
|
22
22
|
require_relative 'spikard/converters'
|
|
23
|
+
require_relative 'spikard/provide'
|
|
23
24
|
require_relative 'spikard/handler_wrapper'
|
|
24
25
|
require_relative 'spikard/app'
|
|
25
26
|
require_relative 'spikard/testing'
|
data/sig/spikard.rbs
CHANGED
|
@@ -187,8 +187,20 @@ module Spikard
|
|
|
187
187
|
def lifecycle_hooks: () -> Hash[Symbol, Array[Proc]]
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
+
module ProvideSupport
|
|
191
|
+
def provide: (
|
|
192
|
+
String | Symbol,
|
|
193
|
+
?untyped,
|
|
194
|
+
?depends_on: Array[String | Symbol],
|
|
195
|
+
?singleton: bool,
|
|
196
|
+
?cacheable: bool
|
|
197
|
+
) ?{ (**untyped) -> untyped } -> self
|
|
198
|
+
def dependencies: () -> Hash[String, untyped]
|
|
199
|
+
end
|
|
200
|
+
|
|
190
201
|
class App
|
|
191
202
|
include LifecycleHooks
|
|
203
|
+
include ProvideSupport
|
|
192
204
|
|
|
193
205
|
attr_reader routes: Array[untyped]
|
|
194
206
|
|
|
@@ -242,7 +254,8 @@ module Spikard
|
|
|
242
254
|
ServerConfig,
|
|
243
255
|
Hash[Symbol, Array[Proc]],
|
|
244
256
|
Hash[String, Proc],
|
|
245
|
-
Hash[String, Proc]
|
|
257
|
+
Hash[String, Proc],
|
|
258
|
+
Hash[String, untyped]
|
|
246
259
|
) -> void
|
|
247
260
|
|
|
248
261
|
class TestClient
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spikard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Na'aman Hirschfeld
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: websocket-client-simple
|
|
@@ -25,8 +25,21 @@ dependencies:
|
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0.8'
|
|
27
27
|
description: |
|
|
28
|
-
Spikard
|
|
29
|
-
|
|
28
|
+
Spikard is a Rust-centric multi-language HTTP toolkit providing a high-performance core library
|
|
29
|
+
and language bindings (Python, Node.js, Ruby, PHP, WebAssembly) to build and validate typed web services.
|
|
30
|
+
|
|
31
|
+
The Ruby binding uses Magnus for zero-overhead FFI, providing Sinatra-style routing, full async/await support,
|
|
32
|
+
WebSockets, Server-Sent Events, request validation with JSON Schema and dry-schema, lifecycle hooks,
|
|
33
|
+
dependency injection, and comprehensive middleware stack (compression, rate limiting, authentication).
|
|
34
|
+
|
|
35
|
+
Features:
|
|
36
|
+
- Zero-copy Rust-to-Ruby serialization via Magnus
|
|
37
|
+
- Async-first with Tokio and Axum backing
|
|
38
|
+
- Type-safe RBS type definitions for Steep
|
|
39
|
+
- Tower-HTTP middleware stack
|
|
40
|
+
- Lifecycle hooks (onRequest, preValidation, preHandler, onResponse, onError)
|
|
41
|
+
- Built-in WebSocket and SSE support
|
|
42
|
+
- Request validation with JSON Schema
|
|
30
43
|
email:
|
|
31
44
|
- nhirschfeld@gmail.com
|
|
32
45
|
executables: []
|
|
@@ -45,6 +58,7 @@ files:
|
|
|
45
58
|
- lib/spikard/config.rb
|
|
46
59
|
- lib/spikard/converters.rb
|
|
47
60
|
- lib/spikard/handler_wrapper.rb
|
|
61
|
+
- lib/spikard/provide.rb
|
|
48
62
|
- lib/spikard/response.rb
|
|
49
63
|
- lib/spikard/schema.rb
|
|
50
64
|
- lib/spikard/sse.rb
|
|
@@ -61,6 +75,9 @@ metadata:
|
|
|
61
75
|
homepage_uri: https://github.com/Goldziher/spikard
|
|
62
76
|
source_code_uri: https://github.com/Goldziher/spikard
|
|
63
77
|
changelog_uri: https://github.com/Goldziher/spikard/blob/main/CHANGELOG.md
|
|
78
|
+
documentation_uri: https://github.com/Goldziher/spikard/tree/main/packages/ruby#documentation
|
|
79
|
+
bug_tracker_uri: https://github.com/Goldziher/spikard/issues
|
|
80
|
+
funding_uri: https://github.com/Goldziher/spikard
|
|
64
81
|
rubygems_mfa_required: 'true'
|
|
65
82
|
post_install_message:
|
|
66
83
|
rdoc_options: []
|
|
@@ -80,5 +97,5 @@ requirements: []
|
|
|
80
97
|
rubygems_version: 3.5.22
|
|
81
98
|
signing_key:
|
|
82
99
|
specification_version: 4
|
|
83
|
-
summary:
|
|
100
|
+
summary: High-performance HTTP toolkit with Rust core and Ruby bindings
|
|
84
101
|
test_files: []
|