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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfc01cd2450315beba3567c74c86a3a8a05077f7fd92444b8831dd03fcfa608b
4
- data.tar.gz: b4d4261fc98b7a911e667c3653d1137109aa3442fa8bf929180ea74034a171ec
3
+ metadata.gz: e1f75c9c00309eba12ce9edad11b8469746dcb1eba392f2987e522816dbdf9a0
4
+ data.tar.gz: cad1676a6d180be90cbf412d54470eb0a582eb0f65e7a880cdc546c3ab14a52e
5
5
  SHA512:
6
- metadata.gz: 3ccd8095e1cbb20162673f215cd19c8109e396b8e77c20290cc6695c64e74f63c2c45624b90924a1dd388ee3466283c3862fb581f3c630e0eb6e2e0c1307ba09
7
- data.tar.gz: fcd2b7a2ed7416458326a1e2024e1ad6de2fef41179f6f6c4a560a702a7693bc3bed1ca408b7eccc5f552fc6e8e8c1d70a22e6d28abd15a281eaff8a7bbdc368
6
+ metadata.gz: a8af5d8195a604c05fc4b5f3c9e20bce22f92f31db1b5a025a30aa971f94cb4cf8c5ecec2bfa2f7626aff0267ad21b4b4a0e12ee1659e856ca4277dea93451e1
7
+ data.tar.gz: d4ccbff9878b866119b20800223dc82e6cb744fe2a06fccc8c6017826c3ea1ec1810618c0143e513e6304a655dae536d23b6856777749be2053603e1a19b2f4d
data/README.md CHANGED
@@ -1,16 +1,40 @@
1
1
  # Spikard Ruby
2
2
 
3
- [![Discord](https://img.shields.io/badge/Discord-Join%20our%20community-7289da)](https://discord.gg/pXxagNK2zN)
4
- [![RubyGems](https://badge.fury.io/rb/spikard.svg)](https://rubygems.org/gems/spikard)
5
- [![npm](https://img.shields.io/npm/v/spikard)](https://www.npmjs.com/package/spikard)
6
- [![npm (WASM)](https://img.shields.io/npm/v/spikard-wasm?label=npm%20%28wasm%29)](https://www.npmjs.com/package/spikard-wasm)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
-
9
- High-performance Ruby web framework with a Rust core. Build REST APIs with Sinatra-style blocks backed by Axum and Tower-HTTP.
3
+ [![Documentation](https://img.shields.io/badge/docs-spikard.dev-58FBDA)](https://spikard.dev)
4
+ [![Gem Version](https://img.shields.io/gem/v/spikard.svg)](https://rubygems.org/gems/spikard)
5
+ [![Gem Downloads](https://img.shields.io/gem/dt/spikard.svg)](https://rubygems.org/gems/spikard)
6
+ [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg)](https://www.ruby-lang.org/)
7
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+ [![CI Status](https://img.shields.io/github/actions/workflow/status/Goldziher/spikard/ci.yml?branch=main)](https://github.com/Goldziher/spikard/actions)
9
+ [![PyPI](https://img.shields.io/pypi/v/spikard.svg)](https://pypi.org/project/spikard/)
10
+ [![npm](https://img.shields.io/npm/v/@spikard/node.svg)](https://www.npmjs.com/package/@spikard/node)
11
+ [![Crates.io](https://img.shields.io/crates/v/spikard.svg)](https://crates.io/crates/spikard)
12
+ [![Packagist](https://img.shields.io/packagist/v/spikard/spikard.svg)](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
- **From source (currently):**
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 native extension)
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
- { id: 1, name: request[:body]["name"] }
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
- See `/examples/ruby/` for more examples.
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
- - [Main Project README](../../README.md)
548
- - [Contributing Guide](../../CONTRIBUTING.md)
549
- - [RBS Type Signatures](sig/spikard.rbs)
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
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-rb-ext"
3
- version = "0.1.1"
3
+ version = "0.2.0"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
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
- metadata = build_metadata(method, path, handler_name, options)
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
- @routes.map(&: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
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 build_metadata(method, path, handler_name, options)
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
@@ -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
- native = Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spikard
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
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.1.1
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-23 00:00:00.000000000 Z
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 provides a high-performance HTTP toolkit with a Rust core and thin language bindings.
29
- This gem bundles the Ruby bridge implemented with Magnus.
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: Ruby bindings for the Spikard HTTP toolkit
100
+ summary: High-performance HTTP toolkit with Rust core and Ruby bindings
84
101
  test_files: []