spikard 0.3.6 → 0.6.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.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +674 -659
  4. data/ext/spikard_rb/Cargo.toml +17 -17
  5. data/ext/spikard_rb/extconf.rb +13 -10
  6. data/ext/spikard_rb/src/lib.rs +6 -6
  7. data/lib/spikard/app.rb +405 -386
  8. data/lib/spikard/background.rb +27 -27
  9. data/lib/spikard/config.rb +396 -396
  10. data/lib/spikard/converters.rb +13 -13
  11. data/lib/spikard/handler_wrapper.rb +113 -113
  12. data/lib/spikard/provide.rb +214 -214
  13. data/lib/spikard/response.rb +173 -173
  14. data/lib/spikard/schema.rb +243 -243
  15. data/lib/spikard/sse.rb +111 -111
  16. data/lib/spikard/streaming_response.rb +44 -44
  17. data/lib/spikard/testing.rb +256 -221
  18. data/lib/spikard/upload_file.rb +131 -131
  19. data/lib/spikard/version.rb +5 -5
  20. data/lib/spikard/websocket.rb +59 -59
  21. data/lib/spikard.rb +43 -43
  22. data/sig/spikard.rbs +366 -366
  23. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  24. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  25. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  26. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  27. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  28. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  29. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  30. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  31. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  32. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  33. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  34. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  35. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  36. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  37. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  38. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  39. data/vendor/crates/spikard-core/Cargo.toml +40 -40
  40. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
  41. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
  42. data/vendor/crates/spikard-core/src/debug.rs +127 -63
  43. data/vendor/crates/spikard-core/src/di/container.rs +702 -726
  44. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
  45. data/vendor/crates/spikard-core/src/di/error.rs +118 -118
  46. data/vendor/crates/spikard-core/src/di/factory.rs +534 -538
  47. data/vendor/crates/spikard-core/src/di/graph.rs +506 -545
  48. data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
  49. data/vendor/crates/spikard-core/src/di/resolved.rs +405 -411
  50. data/vendor/crates/spikard-core/src/di/value.rs +281 -283
  51. data/vendor/crates/spikard-core/src/errors.rs +69 -39
  52. data/vendor/crates/spikard-core/src/http.rs +415 -153
  53. data/vendor/crates/spikard-core/src/lib.rs +29 -29
  54. data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -422
  55. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  56. data/vendor/crates/spikard-core/src/parameters.rs +2525 -722
  57. data/vendor/crates/spikard-core/src/problem.rs +344 -310
  58. data/vendor/crates/spikard-core/src/request_data.rs +1154 -189
  59. data/vendor/crates/spikard-core/src/router.rs +510 -249
  60. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
  61. data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
  62. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +696 -0
  63. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +457 -699
  64. data/vendor/crates/spikard-http/Cargo.toml +62 -68
  65. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  66. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  67. data/vendor/crates/spikard-http/src/auth.rs +296 -247
  68. data/vendor/crates/spikard-http/src/background.rs +1860 -249
  69. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
  70. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
  71. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
  72. data/vendor/crates/spikard-http/src/cors.rs +1005 -490
  73. data/vendor/crates/spikard-http/src/debug.rs +128 -63
  74. data/vendor/crates/spikard-http/src/di_handler.rs +1668 -423
  75. data/vendor/crates/spikard-http/src/handler_response.rs +901 -190
  76. data/vendor/crates/spikard-http/src/handler_trait.rs +838 -228
  77. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -284
  78. data/vendor/crates/spikard-http/src/lib.rs +534 -529
  79. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -149
  80. data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -428
  81. data/vendor/crates/spikard-http/src/middleware/mod.rs +560 -285
  82. data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -86
  83. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -147
  84. data/vendor/crates/spikard-http/src/middleware/validation.rs +768 -287
  85. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
  86. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -190
  87. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -308
  88. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -195
  89. data/vendor/crates/spikard-http/src/query_parser.rs +793 -369
  90. data/vendor/crates/spikard-http/src/response.rs +720 -399
  91. data/vendor/crates/spikard-http/src/server/handler.rs +1650 -87
  92. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -98
  93. data/vendor/crates/spikard-http/src/server/mod.rs +1593 -805
  94. data/vendor/crates/spikard-http/src/server/request_extraction.rs +789 -119
  95. data/vendor/crates/spikard-http/src/server/routing_factory.rs +629 -0
  96. data/vendor/crates/spikard-http/src/sse.rs +1409 -447
  97. data/vendor/crates/spikard-http/src/testing/form.rs +52 -14
  98. data/vendor/crates/spikard-http/src/testing/multipart.rs +64 -60
  99. data/vendor/crates/spikard-http/src/testing/test_client.rs +311 -285
  100. data/vendor/crates/spikard-http/src/testing.rs +406 -377
  101. data/vendor/crates/spikard-http/src/websocket.rs +1404 -324
  102. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  103. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  104. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  105. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  106. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  107. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  108. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  109. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  110. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  111. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  112. data/vendor/crates/spikard-rb/Cargo.toml +48 -42
  113. data/vendor/crates/spikard-rb/build.rs +199 -8
  114. data/vendor/crates/spikard-rb/src/background.rs +63 -63
  115. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  116. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +285 -294
  117. data/vendor/crates/spikard-rb/src/conversion.rs +554 -453
  118. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  119. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +375 -409
  120. data/vendor/crates/spikard-rb/src/handler.rs +618 -625
  121. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  122. data/vendor/crates/spikard-rb/src/lib.rs +1806 -2771
  123. data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -274
  124. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  125. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +442 -0
  126. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  127. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  128. data/vendor/crates/spikard-rb/src/server.rs +305 -283
  129. data/vendor/crates/spikard-rb/src/sse.rs +231 -231
  130. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +538 -404
  131. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  132. data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +143 -143
  133. data/vendor/crates/spikard-rb/src/testing/websocket.rs +608 -0
  134. data/vendor/crates/spikard-rb/src/websocket.rs +377 -233
  135. metadata +60 -13
  136. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  137. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  138. data/vendor/crates/spikard-http/src/router.rs +0 -1
  139. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  140. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  141. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  142. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
data/README.md CHANGED
@@ -1,659 +1,674 @@
1
- # Spikard Ruby
2
-
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
- [![codecov](https://codecov.io/gh/Goldziher/spikard/graph/badge.svg?token=H4ZXDZ4A69)](https://codecov.io/gh/Goldziher/spikard)
9
- [![PyPI](https://img.shields.io/pypi/v/spikard.svg)](https://pypi.org/project/spikard/)
10
- [![npm](https://img.shields.io/npm/v/spikard.svg)](https://www.npmjs.com/package/spikard)
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
28
-
29
- ## Installation
30
-
31
- **Via RubyGems (recommended):**
32
-
33
- ```bash
34
- gem install spikard
35
- ```
36
-
37
- **From source (development):**
38
-
39
- ```bash
40
- cd packages/ruby
41
- bundle install
42
- bundle exec rake ext:build
43
- ```
44
-
45
- **Requirements:**
46
- - Ruby 3.2 or later
47
- - Bundler
48
- - Rust toolchain (for building from source)
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
-
83
- ## Quick Start
84
-
85
- ```ruby
86
- require "spikard"
87
- require "dry-schema"
88
-
89
- UserSchema = Dry::Schema.JSON do
90
- required(:name).filled(:str?)
91
- required(:email).filled(:str?)
92
- end
93
-
94
- app = Spikard::App.new
95
-
96
- app.get "/users/:id" do |request|
97
- user_id = request[:path_params]["id"].to_i
98
- { id: user_id, name: "Alice" }
99
- end
100
-
101
- app.post "/users", request_schema: UserSchema do |request|
102
- body = request[:body]
103
- { id: 1, name: body["name"], email: body["email"] }
104
- end
105
-
106
- app.run(port: 8000)
107
- ```
108
-
109
- ## Request Hash Structure
110
-
111
- Handlers receive a single `request` hash argument with the following keys:
112
-
113
- - `:method` - HTTP method (String): `"GET"`, `"POST"`, etc.
114
- - `:path` - URL path (String): `"/users/123"`
115
- - `:path_params` - Path parameters (Hash): `{"id" => "123"}`
116
- - `:query` - Query parameters (Hash): `{"search" => "ruby"}`
117
- - `:raw_query` - Raw query multimap (Hash of Arrays)
118
- - `:headers` - Request headers (Hash): `{"Authorization" => "Bearer..."}`
119
- - `:cookies` - Request cookies (Hash): `{"session_id" => "..."}`
120
- - `:body` - Parsed request body (Hash or nil)
121
- - `:params` - Merged params from path, query, headers, and cookies
122
-
123
- **Example:**
124
-
125
- ```ruby
126
- app.get "/users/:id" do |request|
127
- user_id = request[:path_params]["id"]
128
- search = request[:query]["search"]
129
- auth = request[:headers]["Authorization"]
130
-
131
- { id: user_id, search: search }
132
- end
133
- ```
134
-
135
- ## Route Registration
136
-
137
- ### HTTP Methods
138
-
139
- ```ruby
140
- app.get "/path" do |request|
141
- # Handler code
142
- { method: request[:method] }
143
- end
144
-
145
- app.post "/path" do |request|
146
- { created: true }
147
- end
148
-
149
- app.put "/path" do |request|
150
- { updated: true }
151
- end
152
-
153
- app.patch "/path" do |request|
154
- { patched: true }
155
- end
156
-
157
- app.delete "/path" do |request|
158
- { deleted: true }
159
- end
160
-
161
- app.options "/path" do |request|
162
- { options: [] }
163
- end
164
-
165
- app.head "/path" do |request|
166
- # HEAD request
167
- end
168
-
169
- app.trace "/path" do |request|
170
- # TRACE request
171
- end
172
- ```
173
-
174
- ### Path Parameters
175
-
176
- ```ruby
177
- app.get "/users/:user_id" do |request|
178
- { user_id: request[:path_params]["user_id"].to_i }
179
- end
180
-
181
- app.get "/posts/:post_id/comments/:comment_id" do |request|
182
- {
183
- post_id: request[:path_params]["post_id"].to_i,
184
- comment_id: request[:path_params]["comment_id"].to_i
185
- }
186
- end
187
- ```
188
-
189
- ### Query Parameters
190
-
191
- ```ruby
192
- app.get "/search" do |request|
193
- q = request[:query]["q"]
194
- limit = (request[:query]["limit"] || "10").to_i
195
- { query: q, limit: limit }
196
- end
197
- ```
198
-
199
- ## Validation
200
-
201
- Spikard supports **dry-schema** and **raw JSON Schema objects**.
202
-
203
- ### With dry-schema
204
-
205
- ```ruby
206
- require "dry-schema"
207
- Dry::Schema.load_extensions(:json_schema)
208
-
209
- UserSchema = Dry::Schema.JSON do
210
- required(:name).filled(:str?)
211
- required(:email).filled(:str?)
212
- required(:age).filled(:int?)
213
- end
214
-
215
- app.post "/users", request_schema: UserSchema do |request|
216
- # request[:body] is validated against schema
217
- { id: 1, name: request[:body]["name"] }
218
- end
219
- ```
220
-
221
- ### With raw JSON Schema
222
-
223
- ```ruby
224
- user_schema = {
225
- "type" => "object",
226
- "properties" => {
227
- "name" => { "type" => "string" },
228
- "email" => { "type" => "string", "format" => "email" }
229
- },
230
- "required" => ["name", "email"]
231
- }
232
-
233
- app.post "/users", request_schema: user_schema do |request|
234
- { id: 1, name: request[:body]["name"], email: request[:body]["email"] }
235
- end
236
- ```
237
-
238
- ## Dependency Injection
239
-
240
- Register values or factories and inject them as keyword parameters:
241
-
242
- ```ruby
243
- app.provide("config", { "db_url" => "postgresql://localhost/app" })
244
- app.provide("db_pool", depends_on: ["config"], singleton: true) do |config:|
245
- { url: config["db_url"], driver: "pool" }
246
- end
247
-
248
- app.get "/stats" do |_params, _query, _body, config:, db_pool:|
249
- { db: db_pool[:url], env: config["db_url"] }
250
- end
251
- ```
252
-
253
- ### With dry-struct
254
-
255
- ```ruby
256
- require "dry-struct"
257
- require "dry-types"
258
-
259
- module Types
260
- include Dry.Types()
261
- end
262
-
263
- class User < Dry::Struct
264
- attribute :name, Types::String
265
- attribute :email, Types::String
266
- attribute :age, Types::Integer
267
- end
268
-
269
- app.post "/users", request_schema: User do |request|
270
- # request[:body] validated as User
271
- { id: 1, name: request[:body]["name"] }
272
- end
273
- ```
274
-
275
- ## Response Types
276
-
277
- ### Simple Hash Response
278
-
279
- ```ruby
280
- app.get "/hello" do
281
- { message: "Hello, World!" }
282
- end
283
- ```
284
-
285
- ### String Response
286
-
287
- ```ruby
288
- app.get "/text" do
289
- "Plain text response"
290
- end
291
- ```
292
-
293
- ### Full Response Object
294
-
295
- ```ruby
296
- app.post "/users" do |request|
297
- Spikard::Response.new(
298
- content: { id: 1, name: request[:body]["name"] },
299
- status_code: 201,
300
- headers: { "X-Custom" => "value" }
301
- )
302
- end
303
- ```
304
-
305
- ### Streaming Response
306
-
307
- ```ruby
308
- app.get "/stream" do
309
- stream = Enumerator.new do |yielder|
310
- 10.times do |i|
311
- yielder << "Chunk #{i}\n"
312
- sleep 0.1
313
- end
314
- end
315
-
316
- Spikard::StreamingResponse.new(
317
- stream,
318
- status_code: 200,
319
- headers: { "Content-Type" => "text/plain" }
320
- )
321
- end
322
- ```
323
-
324
- ## File Uploads
325
-
326
- ```ruby
327
- app.post "/upload", file_params: true do |request|
328
- file = request[:body]["file"] # UploadFile instance
329
-
330
- {
331
- filename: file.filename,
332
- size: file.size,
333
- content_type: file.content_type,
334
- content: file.read
335
- }
336
- end
337
- ```
338
-
339
- ## Configuration
340
-
341
- ```ruby
342
- config = Spikard::ServerConfig.new(
343
- host: "0.0.0.0",
344
- port: 8080,
345
- workers: 4,
346
- enable_request_id: true,
347
- max_body_size: 10 * 1024 * 1024, # 10 MB
348
- request_timeout: 30,
349
- compression: Spikard::CompressionConfig.new(
350
- gzip: true,
351
- brotli: true,
352
- quality: 6
353
- ),
354
- rate_limit: Spikard::RateLimitConfig.new(
355
- per_second: 100,
356
- burst: 200
357
- )
358
- )
359
-
360
- app.run(config: config)
361
- ```
362
-
363
- ### Middleware Configuration
364
-
365
- **Compression:**
366
-
367
- ```ruby
368
- compression = Spikard::CompressionConfig.new(
369
- gzip: true,
370
- brotli: true,
371
- min_size: 1024,
372
- quality: 6
373
- )
374
- ```
375
-
376
- **Rate Limiting:**
377
-
378
- ```ruby
379
- rate_limit = Spikard::RateLimitConfig.new(
380
- per_second: 100,
381
- burst: 200,
382
- ip_based: true
383
- )
384
- ```
385
-
386
- **JWT Authentication:**
387
-
388
- ```ruby
389
- jwt = Spikard::JwtConfig.new(
390
- secret: "your-secret-key",
391
- algorithm: "HS256",
392
- audience: ["api.example.com"],
393
- issuer: "auth.example.com",
394
- leeway: 30
395
- )
396
- ```
397
-
398
- **Static Files:**
399
-
400
- ```ruby
401
- static = Spikard::StaticFilesConfig.new(
402
- directory: "./public",
403
- route_prefix: "/static",
404
- index_file: true,
405
- cache_control: "public, max-age=3600"
406
- )
407
- ```
408
-
409
- **OpenAPI Documentation:**
410
-
411
- ```ruby
412
- openapi = Spikard::OpenApiConfig.new(
413
- enabled: true,
414
- title: "My API",
415
- version: "1.0.0",
416
- description: "API docs",
417
- swagger_ui_path: "/docs",
418
- redoc_path: "/redoc"
419
- )
420
- ```
421
-
422
- ## Lifecycle Hooks
423
-
424
- ```ruby
425
- app.on_request do |request|
426
- puts "#{request[:method]} #{request[:path]}"
427
- request
428
- end
429
-
430
- app.pre_validation do |request|
431
- if too_many_requests?
432
- Spikard::Response.new(
433
- content: { error: "Rate limit exceeded" },
434
- status_code: 429
435
- )
436
- else
437
- request
438
- end
439
- end
440
-
441
- app.pre_handler do |request|
442
- if invalid_token?(request[:headers]["Authorization"])
443
- Spikard::Response.new(
444
- content: { error: "Unauthorized" },
445
- status_code: 401
446
- )
447
- else
448
- request
449
- end
450
- end
451
-
452
- app.on_response do |response|
453
- response.headers["X-Frame-Options"] = "DENY"
454
- response
455
- end
456
-
457
- app.on_error do |response|
458
- puts "Error: #{response.status_code}"
459
- response
460
- end
461
- ```
462
-
463
- ## WebSockets
464
-
465
- ```ruby
466
- class ChatHandler < Spikard::WebSocketHandler
467
- def on_connect
468
- puts "Client connected"
469
- end
470
-
471
- def handle_message(message)
472
- # message is a Hash (parsed JSON)
473
- { echo: message, timestamp: Time.now.to_i }
474
- end
475
-
476
- def on_disconnect
477
- puts "Client disconnected"
478
- end
479
- end
480
-
481
- app.websocket("/chat") { ChatHandler.new }
482
- ```
483
-
484
- ## Server-Sent Events (SSE)
485
-
486
- ```ruby
487
- class NotificationProducer < Spikard::SseEventProducer
488
- def initialize
489
- @count = 0
490
- end
491
-
492
- def on_connect
493
- puts "Client connected to SSE stream"
494
- end
495
-
496
- def next_event
497
- sleep 1
498
-
499
- return nil if @count >= 10 # End stream
500
-
501
- event = Spikard::SseEvent.new(
502
- data: { message: "Notification #{@count}" },
503
- event_type: "notification",
504
- id: @count.to_s,
505
- retry_ms: 3000
506
- )
507
- @count += 1
508
- event
509
- end
510
-
511
- def on_disconnect
512
- puts "Client disconnected from SSE"
513
- end
514
- end
515
-
516
- app.sse("/notifications") { NotificationProducer.new }
517
- ```
518
-
519
- ## Background Tasks
520
-
521
- ```ruby
522
- app.post "/process" do |request|
523
- Spikard::Background.run do
524
- # Heavy processing after response
525
- ProcessData.perform(request[:path_params]["id"])
526
- end
527
-
528
- { status: "processing" }
529
- end
530
- ```
531
-
532
- ## Testing
533
-
534
- ```ruby
535
- require "spikard"
536
-
537
- app = Spikard::App.new
538
- app.get "/hello" do
539
- { message: "Hello, World!" }
540
- end
541
-
542
- client = Spikard::TestClient.new(app)
543
-
544
- # HTTP requests
545
- response = client.get("/hello", query: { name: "Alice" })
546
- puts response.status_code # => 200
547
- puts response.json # => { "message" => "Hello, World!" }
548
-
549
- # POST with JSON
550
- response = client.post("/users", json: { name: "Bob" })
551
-
552
- # File upload
553
- response = client.post("/upload", files: {
554
- file: ["test.txt", "content", "text/plain"]
555
- })
556
-
557
- # WebSocket
558
- ws = client.websocket("/chat")
559
- ws.send_json({ message: "hello" })
560
- message = ws.receive_json
561
- ws.close
562
-
563
- # SSE
564
- sse = client.sse("/events")
565
- events = sse.events_as_json
566
- puts events.length
567
-
568
- # Cleanup
569
- client.close
570
- ```
571
-
572
- ## Running the Server
573
-
574
- ```ruby
575
- # Development
576
- app.run(port: 8000)
577
-
578
- # Production
579
- config = Spikard::ServerConfig.new(
580
- host: "0.0.0.0",
581
- port: 8080,
582
- workers: 4
583
- )
584
- app.run(config: config)
585
- ```
586
-
587
- ## Type Safety with RBS
588
-
589
- RBS type signatures are provided in `sig/spikard.rbs`:
590
-
591
- ```ruby
592
- module Spikard
593
- class App
594
- def initialize: () -> void
595
- def get: (String, ?handler_name: String?, **untyped) { (untyped) -> untyped } -> Proc
596
- def post: (String, ?handler_name: String?, **untyped) { (untyped) -> untyped } -> Proc
597
- def run: (?config: ServerConfig | Hash[Symbol, untyped]?) -> void
598
- end
599
-
600
- class ServerConfig
601
- def initialize: (?host: String, ?port: Integer, **untyped) -> void
602
- end
603
- end
604
- ```
605
-
606
- Use with Steep for type checking:
607
-
608
- ```bash
609
- bundle exec steep check
610
- ```
611
-
612
- ## Performance
613
-
614
- Ruby bindings use:
615
- - **Magnus** for zero-overhead FFI
616
- - **rb-sys** for modern Ruby 3.2+ integration
617
- - Idiomatic Ruby blocks and procs
618
- - GC-safe handler storage
619
-
620
- ## Examples
621
-
622
- The [examples directory](../../examples/) contains comprehensive demonstrations:
623
-
624
- **Ruby-specific examples:**
625
- - [Basic Ruby Example](../../examples/di/ruby_basic.rb) - Simple server with DI
626
- - [Database Integration](../../examples/di/ruby_database.rb) - DI with database pools
627
- - Additional examples in [examples/](../../examples/)
628
-
629
- **API Schemas** (language-agnostic, can be used with code generation):
630
- - [Todo API](../../examples/schemas/todo-api.openapi.yaml) - REST CRUD with validation
631
- - [File Service](../../examples/schemas/file-service.openapi.yaml) - File uploads/downloads
632
- - [Auth Service](../../examples/schemas/auth-service.openapi.yaml) - JWT, API keys, OAuth
633
- - [Chat Service](../../examples/schemas/chat-service.asyncapi.yaml) - WebSocket messaging
634
- - [Event Streams](../../examples/schemas/events-stream.asyncapi.yaml) - SSE streaming
635
-
636
- See [examples/README.md](../../examples/README.md) for code generation instructions.
637
-
638
- ## Documentation
639
-
640
- **API Reference & Guides:**
641
- - [Type Definitions (RBS)](sig/spikard.rbs) - Full type signatures for Steep
642
- - [Configuration Reference](lib/spikard/config.rb) - ServerConfig and middleware options
643
- - [Handler Documentation](lib/spikard/handler_wrapper.rb) - Request/response handling
644
-
645
- **Project Resources:**
646
- - [Main Project README](../../README.md) - Spikard overview and multi-language ecosystem
647
- - [Contributing Guide](../../CONTRIBUTING.md) - Development guidelines
648
- - [Architecture Decisions](../../docs/adr/) - ADRs on design choices
649
- - [Examples](../../examples/ruby/) - Runnable example applications
650
-
651
- **Cross-Language:**
652
- - [Python (PyPI)](https://pypi.org/project/spikard/)
653
- - [Node.js (npm)](https://www.npmjs.com/package/spikard)
654
- - [Rust (Crates.io)](https://crates.io/crates/spikard)
655
- - [PHP (Packagist)](https://packagist.org/packages/spikard/spikard)
656
-
657
- ## License
658
-
659
- MIT - See [LICENSE](../../LICENSE) for details
1
+ # Spikard Ruby
2
+
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
+ [![codecov](https://codecov.io/gh/Goldziher/spikard/graph/badge.svg?token=H4ZXDZ4A69)](https://codecov.io/gh/Goldziher/spikard)
8
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
+
10
+ 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.
11
+
12
+ ## Features
13
+
14
+ - **Rust-powered performance**: High-throughput HTTP server backed by Tokio and Axum
15
+ - **Sinatra-style routing**: Familiar `get`, `post`, `put`, `patch`, `delete` DSL
16
+ - **Type-safe with RBS**: Full RBS type definitions for Steep type checking
17
+ - **Zero-copy serialization**: Direct Rust-to-Ruby object conversion via Magnus
18
+ - **Async-first**: Non-blocking handlers with full async/await support
19
+ - **Middleware stack**: Compression, rate limiting, request IDs, authentication
20
+ - **WebSockets & SSE**: Native real-time communication primitives
21
+ - **Request validation**: JSON Schema and dry-schema support
22
+ - **Lifecycle hooks**: onRequest, preValidation, preHandler, onResponse, onError
23
+ - **Dependency injection**: Built-in container for services and factories
24
+
25
+ ## Installation
26
+
27
+ **Via RubyGems (recommended):**
28
+
29
+ ```bash
30
+ gem install spikard
31
+ ```
32
+
33
+ **From source (development):**
34
+
35
+ ```bash
36
+ cd packages/ruby
37
+ bundle install
38
+ bundle exec rake ext:build
39
+ ```
40
+
41
+ **Requirements:**
42
+ - Ruby 3.2 or later
43
+ - Bundler
44
+ - Rust toolchain (for building from source)
45
+
46
+ ## Windows Development
47
+
48
+ On Windows, Spikard uses the GNU toolchain (not MSVC) to match Ruby's official RubyInstaller distribution.
49
+
50
+ ### Prerequisites
51
+
52
+ 1. **Install RubyInstaller with DevKit:**
53
+ - Download from [RubyInstaller.org](https://rubyinstaller.org/downloads/)
54
+ - Choose Ruby+Devkit 3.2.x (x64)
55
+ - During installation, select "MSYS2 development toolchain"
56
+
57
+ 2. **Install Rust with GNU target:**
58
+ ```powershell
59
+ rustup toolchain install stable-x86_64-pc-windows-gnu
60
+ rustup default stable-x86_64-pc-windows-gnu
61
+ ```
62
+
63
+ 3. **Verify setup:**
64
+ ```powershell
65
+ ruby --version # Should show 3.2.x
66
+ rustup show # Should show *-pc-windows-gnu
67
+ ```
68
+
69
+ ### Building on Windows
70
+
71
+ ```bash
72
+ cd packages/ruby
73
+ bundle install
74
+ bundle exec rake compile
75
+ ```
76
+
77
+ The build uses the GNU toolchain automatically via RubyInstaller's MSYS2 DevKit. No MSVC configuration needed.
78
+
79
+ ## Quick Start
80
+
81
+ ```ruby
82
+ require "spikard"
83
+ require "dry-schema"
84
+
85
+ UserSchema = Dry::Schema.JSON do
86
+ required(:name).filled(:str?)
87
+ required(:email).filled(:str?)
88
+ end
89
+
90
+ app = Spikard::App.new
91
+
92
+ app.get "/users/:id" do |request|
93
+ user_id = request[:path_params]["id"].to_i
94
+ { id: user_id, name: "Alice" }
95
+ end
96
+
97
+ app.post "/users", request_schema: UserSchema do |request|
98
+ body = request[:body]
99
+ { id: 1, name: body["name"], email: body["email"] }
100
+ end
101
+
102
+ app.run(port: 8000)
103
+ ```
104
+
105
+ ## Request Hash Structure
106
+
107
+ Handlers receive a single `request` hash argument with the following keys:
108
+
109
+ - `:method` - HTTP method (String): `"GET"`, `"POST"`, etc.
110
+ - `:path` - URL path (String): `"/users/123"`
111
+ - `:path_params` - Path parameters (Hash): `{"id" => "123"}`
112
+ - `:query` - Query parameters (Hash): `{"search" => "ruby"}`
113
+ - `:raw_query` - Raw query multimap (Hash of Arrays)
114
+ - `:headers` - Request headers (Hash): `{"Authorization" => "Bearer..."}`
115
+ - `:cookies` - Request cookies (Hash): `{"session_id" => "..."}`
116
+ - `:body` - Parsed request body (Hash or nil)
117
+ - `:params` - Merged params from path, query, headers, and cookies
118
+
119
+ **Example:**
120
+
121
+ ```ruby
122
+ app.get "/users/:id" do |request|
123
+ user_id = request[:path_params]["id"]
124
+ search = request[:query]["search"]
125
+ auth = request[:headers]["Authorization"]
126
+
127
+ { id: user_id, search: search }
128
+ end
129
+ ```
130
+
131
+ ## Route Registration
132
+
133
+ ### HTTP Methods
134
+
135
+ ```ruby
136
+ app.get "/path" do |request|
137
+ # Handler code
138
+ { method: request[:method] }
139
+ end
140
+
141
+ app.post "/path" do |request|
142
+ { created: true }
143
+ end
144
+
145
+ app.put "/path" do |request|
146
+ { updated: true }
147
+ end
148
+
149
+ app.patch "/path" do |request|
150
+ { patched: true }
151
+ end
152
+
153
+ app.delete "/path" do |request|
154
+ { deleted: true }
155
+ end
156
+
157
+ app.options "/path" do |request|
158
+ { options: [] }
159
+ end
160
+
161
+ app.head "/path" do |request|
162
+ # HEAD request
163
+ end
164
+
165
+ app.trace "/path" do |request|
166
+ # TRACE request
167
+ end
168
+ ```
169
+
170
+ ### Path Parameters
171
+
172
+ ```ruby
173
+ app.get "/users/:user_id" do |request|
174
+ { user_id: request[:path_params]["user_id"].to_i }
175
+ end
176
+
177
+ app.get "/posts/:post_id/comments/:comment_id" do |request|
178
+ {
179
+ post_id: request[:path_params]["post_id"].to_i,
180
+ comment_id: request[:path_params]["comment_id"].to_i
181
+ }
182
+ end
183
+ ```
184
+
185
+ ### Query Parameters
186
+
187
+ ```ruby
188
+ app.get "/search" do |request|
189
+ q = request[:query]["q"]
190
+ limit = (request[:query]["limit"] || "10").to_i
191
+ { query: q, limit: limit }
192
+ end
193
+ ```
194
+
195
+ ## Validation
196
+
197
+ Spikard supports **dry-schema** and **raw JSON Schema objects**.
198
+
199
+ ### With dry-schema
200
+
201
+ ```ruby
202
+ require "dry-schema"
203
+ Dry::Schema.load_extensions(:json_schema)
204
+
205
+ UserSchema = Dry::Schema.JSON do
206
+ required(:name).filled(:str?)
207
+ required(:email).filled(:str?)
208
+ required(:age).filled(:int?)
209
+ end
210
+
211
+ app.post "/users", request_schema: UserSchema do |request|
212
+ # request[:body] is validated against schema
213
+ { id: 1, name: request[:body]["name"] }
214
+ end
215
+ ```
216
+
217
+ ### With raw JSON Schema
218
+
219
+ ```ruby
220
+ user_schema = {
221
+ "type" => "object",
222
+ "properties" => {
223
+ "name" => { "type" => "string" },
224
+ "email" => { "type" => "string", "format" => "email" }
225
+ },
226
+ "required" => ["name", "email"]
227
+ }
228
+
229
+ app.post "/users", request_schema: user_schema do |request|
230
+ { id: 1, name: request[:body]["name"], email: request[:body]["email"] }
231
+ end
232
+ ```
233
+
234
+ ## Dependency Injection
235
+
236
+ Register values or factories and inject them as keyword parameters:
237
+
238
+ ```ruby
239
+ app.provide("config", { "db_url" => "postgresql://localhost/app" })
240
+ app.provide("db_pool", depends_on: ["config"], singleton: true) do |config:|
241
+ { url: config["db_url"], driver: "pool" }
242
+ end
243
+
244
+ app.get "/stats" do |request, config:, db_pool:|
245
+ { db: db_pool[:url], env: config["db_url"] }
246
+ end
247
+ ```
248
+
249
+ ### With dry-struct
250
+
251
+ ```ruby
252
+ require "dry-struct"
253
+ require "dry-types"
254
+
255
+ module Types
256
+ include Dry.Types()
257
+ end
258
+
259
+ class User < Dry::Struct
260
+ attribute :name, Types::String
261
+ attribute :email, Types::String
262
+ attribute :age, Types::Integer
263
+ end
264
+
265
+ app.post "/users", request_schema: User do |request|
266
+ # request[:body] validated as User
267
+ { id: 1, name: request[:body]["name"] }
268
+ end
269
+ ```
270
+
271
+ ## Response Types
272
+
273
+ ### Simple Hash Response
274
+
275
+ ```ruby
276
+ app.get "/hello" do
277
+ { message: "Hello, World!" }
278
+ end
279
+ ```
280
+
281
+ ### String Response
282
+
283
+ ```ruby
284
+ app.get "/text" do
285
+ "Plain text response"
286
+ end
287
+ ```
288
+
289
+ ### Full Response Object
290
+
291
+ ```ruby
292
+ app.post "/users" do |request|
293
+ Spikard::Response.new(
294
+ content: { id: 1, name: request[:body]["name"] },
295
+ status_code: 201,
296
+ headers: { "X-Custom" => "value" }
297
+ )
298
+ end
299
+ ```
300
+
301
+ ### Streaming Response
302
+
303
+ ```ruby
304
+ app.get "/stream" do
305
+ stream = Enumerator.new do |yielder|
306
+ 10.times do |i|
307
+ yielder << "Chunk #{i}\n"
308
+ sleep 0.1
309
+ end
310
+ end
311
+
312
+ Spikard::StreamingResponse.new(
313
+ stream,
314
+ status_code: 200,
315
+ headers: { "Content-Type" => "text/plain" }
316
+ )
317
+ end
318
+ ```
319
+
320
+ ## File Uploads
321
+
322
+ ```ruby
323
+ app.post "/upload", file_params: true do |request|
324
+ file = request[:body]["file"] # UploadFile instance
325
+
326
+ {
327
+ filename: file.filename,
328
+ size: file.size,
329
+ content_type: file.content_type,
330
+ content: file.read
331
+ }
332
+ end
333
+ ```
334
+
335
+ ## Configuration
336
+
337
+ ```ruby
338
+ config = Spikard::ServerConfig.new(
339
+ host: "0.0.0.0",
340
+ port: 8080,
341
+ workers: 4,
342
+ enable_request_id: true,
343
+ max_body_size: 10 * 1024 * 1024, # 10 MB
344
+ request_timeout: 30,
345
+ compression: Spikard::CompressionConfig.new(
346
+ gzip: true,
347
+ brotli: true,
348
+ quality: 6
349
+ ),
350
+ rate_limit: Spikard::RateLimitConfig.new(
351
+ per_second: 100,
352
+ burst: 200
353
+ )
354
+ )
355
+
356
+ app.run(config: config)
357
+ ```
358
+
359
+ ### Middleware Configuration
360
+
361
+ **Compression:**
362
+
363
+ ```ruby
364
+ compression = Spikard::CompressionConfig.new(
365
+ gzip: true,
366
+ brotli: true,
367
+ min_size: 1024,
368
+ quality: 6
369
+ )
370
+ ```
371
+
372
+ **Rate Limiting:**
373
+
374
+ ```ruby
375
+ rate_limit = Spikard::RateLimitConfig.new(
376
+ per_second: 100,
377
+ burst: 200,
378
+ ip_based: true
379
+ )
380
+ ```
381
+
382
+ **JWT Authentication:**
383
+
384
+ ```ruby
385
+ jwt = Spikard::JwtConfig.new(
386
+ secret: "your-secret-key",
387
+ algorithm: "HS256",
388
+ audience: ["api.example.com"],
389
+ issuer: "auth.example.com",
390
+ leeway: 30
391
+ )
392
+ ```
393
+
394
+ **Static Files:**
395
+
396
+ ```ruby
397
+ static = Spikard::StaticFilesConfig.new(
398
+ directory: "./public",
399
+ route_prefix: "/static",
400
+ index_file: true,
401
+ cache_control: "public, max-age=3600"
402
+ )
403
+ ```
404
+
405
+ **OpenAPI Documentation:**
406
+
407
+ ```ruby
408
+ openapi = Spikard::OpenApiConfig.new(
409
+ enabled: true,
410
+ title: "My API",
411
+ version: "1.0.0",
412
+ description: "API docs",
413
+ swagger_ui_path: "/docs",
414
+ redoc_path: "/redoc"
415
+ )
416
+ ```
417
+
418
+ ## Lifecycle Hooks
419
+
420
+ ```ruby
421
+ app.on_request do |request|
422
+ puts "#{request[:method]} #{request[:path]}"
423
+ request
424
+ end
425
+
426
+ app.pre_validation do |request|
427
+ if too_many_requests?
428
+ Spikard::Response.new(
429
+ content: { error: "Rate limit exceeded" },
430
+ status_code: 429
431
+ )
432
+ else
433
+ request
434
+ end
435
+ end
436
+
437
+ app.pre_handler do |request|
438
+ if invalid_token?(request[:headers]["Authorization"])
439
+ Spikard::Response.new(
440
+ content: { error: "Unauthorized" },
441
+ status_code: 401
442
+ )
443
+ else
444
+ request
445
+ end
446
+ end
447
+
448
+ app.on_response do |response|
449
+ response.headers["X-Frame-Options"] = "DENY"
450
+ response
451
+ end
452
+
453
+ app.on_error do |response|
454
+ puts "Error: #{response.status_code}"
455
+ response
456
+ end
457
+ ```
458
+
459
+ ## WebSockets
460
+
461
+ ```ruby
462
+ class ChatHandler < Spikard::WebSocketHandler
463
+ def on_connect
464
+ puts "Client connected"
465
+ end
466
+
467
+ def handle_message(message)
468
+ # message is a Hash (parsed JSON)
469
+ { echo: message, timestamp: Time.now.to_i }
470
+ end
471
+
472
+ def on_disconnect
473
+ puts "Client disconnected"
474
+ end
475
+ end
476
+
477
+ app.websocket("/chat") { ChatHandler.new }
478
+ ```
479
+
480
+ ## Server-Sent Events (SSE)
481
+
482
+ ```ruby
483
+ class NotificationProducer < Spikard::SseEventProducer
484
+ def initialize
485
+ @count = 0
486
+ end
487
+
488
+ def on_connect
489
+ puts "Client connected to SSE stream"
490
+ end
491
+
492
+ def next_event
493
+ sleep 1
494
+
495
+ return nil if @count >= 10 # End stream
496
+
497
+ event = Spikard::SseEvent.new(
498
+ data: { message: "Notification #{@count}" },
499
+ event_type: "notification",
500
+ id: @count.to_s,
501
+ retry_ms: 3000
502
+ )
503
+ @count += 1
504
+ event
505
+ end
506
+
507
+ def on_disconnect
508
+ puts "Client disconnected from SSE"
509
+ end
510
+ end
511
+
512
+ app.sse("/notifications") { NotificationProducer.new }
513
+ ```
514
+
515
+ ## Background Tasks
516
+
517
+ ```ruby
518
+ app.post "/process" do |request|
519
+ Spikard::Background.run do
520
+ # Heavy processing after response
521
+ ProcessData.perform(request[:path_params]["id"])
522
+ end
523
+
524
+ { status: "processing" }
525
+ end
526
+ ```
527
+
528
+ ## Testing
529
+
530
+ ```ruby
531
+ require "spikard"
532
+
533
+ app = Spikard::App.new
534
+ app.get "/hello" do
535
+ { message: "Hello, World!" }
536
+ end
537
+
538
+ client = Spikard::TestClient.new(app)
539
+
540
+ # HTTP requests
541
+ response = client.get("/hello", query: { name: "Alice" })
542
+ puts response.status_code # => 200
543
+ puts response.json # => { "message" => "Hello, World!" }
544
+
545
+ # POST with JSON
546
+ response = client.post("/users", json: { name: "Bob" })
547
+
548
+ # File upload
549
+ response = client.post("/upload", files: {
550
+ file: ["test.txt", "content", "text/plain"]
551
+ })
552
+
553
+ # WebSocket
554
+ ws = client.websocket("/chat")
555
+ ws.send_json({ message: "hello" })
556
+ message = ws.receive_json
557
+ ws.close
558
+
559
+ # SSE
560
+ sse = client.sse("/events")
561
+ events = sse.events_as_json
562
+ puts events.length
563
+
564
+ # Cleanup
565
+ client.close
566
+ ```
567
+
568
+ ## Running the Server
569
+
570
+ ```ruby
571
+ # Development
572
+ app.run(port: 8000)
573
+
574
+ # Production
575
+ config = Spikard::ServerConfig.new(
576
+ host: "0.0.0.0",
577
+ port: 8080,
578
+ workers: 4
579
+ )
580
+ app.run(config: config)
581
+ ```
582
+
583
+ ## Type Safety with RBS
584
+
585
+ RBS type signatures are provided in `sig/spikard.rbs`:
586
+
587
+ ```ruby
588
+ module Spikard
589
+ class App
590
+ def initialize: () -> void
591
+ def get: (String, ?handler_name: String?, **untyped) { (untyped) -> untyped } -> Proc
592
+ def post: (String, ?handler_name: String?, **untyped) { (untyped) -> untyped } -> Proc
593
+ def run: (?config: ServerConfig | Hash[Symbol, untyped]?) -> void
594
+ end
595
+
596
+ class ServerConfig
597
+ def initialize: (?host: String, ?port: Integer, **untyped) -> void
598
+ end
599
+ end
600
+ ```
601
+
602
+ Use with Steep for type checking:
603
+
604
+ ```bash
605
+ bundle exec steep check
606
+ ```
607
+
608
+ ## Performance
609
+
610
+ Ruby bindings use:
611
+ - **Magnus** for zero-overhead FFI
612
+ - **rb-sys** for modern Ruby 3.2+ integration
613
+ - Idiomatic Ruby blocks and procs
614
+ - GC-safe handler storage
615
+
616
+ ### CI Benchmarks (2025-12-20)
617
+
618
+ Run: `snapshots/benchmarks/20397054933` (commit `25e4fdf`, oha, 50 concurrency, 10s, Linux x86_64).
619
+
620
+ | Metric | Value |
621
+ | --- | --- |
622
+ | Avg RPS (all workloads) | 8,271 |
623
+ | Avg latency (ms) | 6.50 |
624
+
625
+ Category breakdown:
626
+
627
+ | Category | Avg RPS | Avg latency (ms) |
628
+ | --- | --- | --- |
629
+ | path-params | 9,591 | 5.22 |
630
+ | json-bodies | 8,648 | 5.78 |
631
+ | forms | 7,989 | 6.27 |
632
+ | query-params | 7,984 | 6.33 |
633
+ | multipart | 5,604 | 10.36 |
634
+
635
+ ## Examples
636
+
637
+ The [examples directory](../../examples/) contains comprehensive demonstrations:
638
+
639
+ **Ruby-specific examples:**
640
+ - [Basic Ruby Example](../../examples/di/ruby_basic.rb) - Simple server with DI
641
+ - [Database Integration](../../examples/di/ruby_database.rb) - DI with database pools
642
+ - Additional examples in [examples/](../../examples/)
643
+
644
+ **API Schemas** (language-agnostic, can be used with code generation):
645
+ - [Todo API](../../examples/schemas/todo-api.openapi.yaml) - REST CRUD with validation
646
+ - [File Service](../../examples/schemas/file-service.openapi.yaml) - File uploads/downloads
647
+ - [Auth Service](../../examples/schemas/auth-service.openapi.yaml) - JWT, API keys, OAuth
648
+ - [Chat Service](../../examples/schemas/chat-service.asyncapi.yaml) - WebSocket messaging
649
+ - [Event Streams](../../examples/schemas/events-stream.asyncapi.yaml) - SSE streaming
650
+
651
+ See [examples/README.md](../../examples/README.md) for code generation instructions.
652
+
653
+ ## Documentation
654
+
655
+ **API Reference & Guides:**
656
+ - [Type Definitions (RBS)](sig/spikard.rbs) - Full type signatures for Steep
657
+ - [Configuration Reference](lib/spikard/config.rb) - ServerConfig and middleware options
658
+ - [Handler Documentation](lib/spikard/handler_wrapper.rb) - Request/response handling
659
+
660
+ **Project Resources:**
661
+ - [Main Project README](../../README.md) - Spikard overview and multi-language ecosystem
662
+ - [Contributing Guide](../../CONTRIBUTING.md) - Development guidelines
663
+ - [Architecture Decisions](../../docs/adr/) - ADRs on design choices
664
+ - [Examples](../../examples/ruby/) - Runnable example applications
665
+
666
+ **Cross-Language:**
667
+ - [Python (PyPI)](https://pypi.org/project/spikard/)
668
+ - [Node.js (npm)](https://www.npmjs.com/package/spikard)
669
+ - [Rust (Crates.io)](https://crates.io/crates/spikard)
670
+ - [PHP (Packagist)](https://packagist.org/packages/spikard/spikard)
671
+
672
+ ## License
673
+
674
+ MIT - See [LICENSE](../../LICENSE) for details