spikard 0.6.2 → 0.7.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +90 -508
  3. data/ext/spikard_rb/Cargo.lock +3287 -0
  4. data/ext/spikard_rb/Cargo.toml +1 -1
  5. data/ext/spikard_rb/extconf.rb +3 -3
  6. data/lib/spikard/app.rb +72 -49
  7. data/lib/spikard/background.rb +38 -7
  8. data/lib/spikard/testing.rb +42 -4
  9. data/lib/spikard/version.rb +1 -1
  10. data/sig/spikard.rbs +4 -0
  11. data/vendor/crates/spikard-bindings-shared/Cargo.toml +2 -2
  12. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +191 -0
  13. data/vendor/crates/spikard-core/Cargo.toml +1 -1
  14. data/vendor/crates/spikard-core/src/http.rs +1 -0
  15. data/vendor/crates/spikard-core/src/lifecycle.rs +63 -0
  16. data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +136 -0
  17. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +37 -0
  18. data/vendor/crates/spikard-core/tests/error_mapper.rs +761 -0
  19. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +106 -0
  20. data/vendor/crates/spikard-core/tests/parameters_full.rs +701 -0
  21. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +301 -0
  22. data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +67 -0
  23. data/vendor/crates/spikard-core/tests/validation_coverage.rs +250 -0
  24. data/vendor/crates/spikard-core/tests/validation_error_paths.rs +45 -0
  25. data/vendor/crates/spikard-http/Cargo.toml +1 -1
  26. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +502 -0
  27. data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +648 -0
  28. data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +58 -0
  29. data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +1207 -0
  30. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2262 -0
  31. data/vendor/crates/spikard-http/src/testing/test_client.rs +155 -2
  32. data/vendor/crates/spikard-http/src/testing.rs +171 -0
  33. data/vendor/crates/spikard-http/src/websocket.rs +79 -6
  34. data/vendor/crates/spikard-http/tests/auth_integration.rs +647 -0
  35. data/vendor/crates/spikard-http/tests/common/test_builders.rs +633 -0
  36. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +162 -0
  37. data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +389 -0
  38. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +513 -0
  39. data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +244 -0
  40. data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +200 -0
  41. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +82 -0
  42. data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +464 -0
  43. data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +286 -0
  44. data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +118 -0
  45. data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +99 -0
  46. data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +206 -0
  47. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +281 -0
  48. data/vendor/crates/spikard-http/tests/server_router_behavior.rs +121 -0
  49. data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +584 -0
  50. data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +130 -0
  51. data/vendor/crates/spikard-http/tests/test_client_requests.rs +167 -0
  52. data/vendor/crates/spikard-http/tests/testing_helpers.rs +87 -0
  53. data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +156 -0
  54. data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +82 -0
  55. data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +440 -0
  56. data/vendor/crates/spikard-http/tests/websocket_integration.rs +152 -0
  57. data/vendor/crates/spikard-rb/Cargo.toml +1 -1
  58. data/vendor/crates/spikard-rb/src/gvl.rs +80 -0
  59. data/vendor/crates/spikard-rb/src/handler.rs +12 -9
  60. data/vendor/crates/spikard-rb/src/lib.rs +137 -124
  61. data/vendor/crates/spikard-rb/src/request.rs +342 -0
  62. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +1 -8
  63. data/vendor/crates/spikard-rb/src/server.rs +1 -8
  64. data/vendor/crates/spikard-rb/src/testing/client.rs +168 -9
  65. data/vendor/crates/spikard-rb/src/websocket.rs +119 -30
  66. data/vendor/crates/spikard-rb-macros/Cargo.toml +14 -0
  67. data/vendor/crates/spikard-rb-macros/src/lib.rs +52 -0
  68. metadata +44 -1
data/README.md CHANGED
@@ -7,20 +7,18 @@
7
7
  [![codecov](https://codecov.io/gh/Goldziher/spikard/graph/badge.svg?token=H4ZXDZ4A69)](https://codecov.io/gh/Goldziher/spikard)
8
8
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
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.
10
+ Ruby bindings for Spikard: a Rust-centric web framework with type-safe code generation from OpenAPI, GraphQL, AsyncAPI, and OpenRPC specifications. Leverage Sinatra-style routing with zero-copy FFI performance.
11
11
 
12
- ## Features
12
+ ## Key Features
13
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
14
  - **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
15
+ - **Zero-copy FFI**: Magnus/rb-sys bindings eliminate serialization overhead
16
+ - **Sinatra-style routing**: Familiar `get`, `post`, `put`, `patch`, `delete` DSL
17
+ - **Code generation**: Generate type-safe handlers from OpenAPI, GraphQL, AsyncAPI, and OpenRPC specs
18
+ - **Full async support**: Non-blocking handlers with complete async/await integration
19
+ - **Tower-HTTP middleware**: Compression, rate limiting, authentication, CORS, request IDs
20
+ - **Real-time**: WebSockets and Server-Sent Events (SSE)
21
+ - **Production-ready**: Dependency injection, validation schemas, lifecycle hooks
24
22
 
25
23
  ## Installation
26
24
 
@@ -30,576 +28,203 @@ High-performance Ruby web framework with a Rust core. Build REST APIs with Sinat
30
28
  gem install spikard
31
29
  ```
32
30
 
33
- **From source (development):**
31
+ **From source:**
34
32
 
35
33
  ```bash
36
- cd packages/ruby
37
34
  bundle install
38
35
  bundle exec rake ext:build
39
36
  ```
40
37
 
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.
38
+ **Requirements:** Ruby 3.2+, Bundler, and Rust toolchain (for building from source). On Windows, use RubyInstaller with MSYS2 DevKit and the GNU Rust toolchain.
78
39
 
79
40
  ## Quick Start
80
41
 
81
42
  ```ruby
82
43
  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
44
 
90
45
  app = Spikard::App.new
91
46
 
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"] }
47
+ app.get "/hello" do |request|
48
+ { message: "Hello, World!" }
100
49
  end
101
50
 
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
51
  app.get "/users/:id" do |request|
123
52
  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
53
+ { id: user_id, name: "Alice" }
167
54
  end
168
- ```
169
-
170
- ### Path Parameters
171
55
 
172
- ```ruby
173
- app.get "/users/:user_id" do |request|
174
- { user_id: request[:path_params]["user_id"].to_i }
56
+ app.post "/users" do |request|
57
+ { id: 1, name: request[:body]["name"] }
175
58
  end
176
59
 
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
60
+ app.run(port: 8000)
183
61
  ```
184
62
 
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
- ```
63
+ The `request` hash provides access to:
64
+ - `request[:method]` - HTTP method
65
+ - `request[:path]` - URL path
66
+ - `request[:path_params]` - Path parameters
67
+ - `request[:query]` - Query parameters
68
+ - `request[:headers]` - Request headers
69
+ - `request[:cookies]` - Request cookies
70
+ - `request[:body]` - Parsed request body
194
71
 
195
72
  ## Validation
196
73
 
197
- Spikard supports **dry-schema** and **raw JSON Schema objects**.
198
-
199
- ### With dry-schema
74
+ Pass a `request_schema` to validate incoming JSON:
200
75
 
201
76
  ```ruby
202
77
  require "dry-schema"
203
- Dry::Schema.load_extensions(:json_schema)
204
78
 
205
79
  UserSchema = Dry::Schema.JSON do
206
80
  required(:name).filled(:str?)
207
81
  required(:email).filled(:str?)
208
- required(:age).filled(:int?)
209
82
  end
210
83
 
211
84
  app.post "/users", request_schema: UserSchema do |request|
212
- # request[:body] is validated against schema
213
85
  { id: 1, name: request[:body]["name"] }
214
86
  end
215
87
  ```
216
88
 
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
- ```
89
+ Also supports raw JSON Schema objects and dry-struct schemas.
233
90
 
234
91
  ## Dependency Injection
235
92
 
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
93
+ Inject dependencies as keyword parameters:
250
94
 
251
95
  ```ruby
252
- require "dry-struct"
253
- require "dry-types"
254
-
255
- module Types
256
- include Dry.Types()
257
- end
96
+ app.provide("config", { "db_url" => "postgresql://localhost" })
97
+ app.provide("db", depends_on: ["config"], singleton: true) { |config:| Pool.new(config) }
258
98
 
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"] }
99
+ app.get "/data" do |request, config:, db:|
100
+ { url: config["db_url"] }
268
101
  end
269
102
  ```
270
103
 
271
- ## Response Types
104
+ ## Responses
272
105
 
273
- ### Simple Hash Response
106
+ Return a Hash, String, or Response object:
274
107
 
275
108
  ```ruby
109
+ # Simple hash (auto-serialized to JSON)
276
110
  app.get "/hello" do
277
111
  { message: "Hello, World!" }
278
112
  end
279
- ```
280
-
281
- ### String Response
282
113
 
283
- ```ruby
284
- app.get "/text" do
285
- "Plain text response"
286
- end
287
- ```
288
-
289
- ### Full Response Object
290
-
291
- ```ruby
114
+ # Custom status and headers
292
115
  app.post "/users" do |request|
293
116
  Spikard::Response.new(
294
- content: { id: 1, name: request[:body]["name"] },
117
+ content: { id: 1 },
295
118
  status_code: 201,
296
119
  headers: { "X-Custom" => "value" }
297
120
  )
298
121
  end
299
- ```
300
-
301
- ### Streaming Response
302
122
 
303
- ```ruby
123
+ # Streaming response
304
124
  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
- )
125
+ stream = Enumerator.new { |y| y << "data" }
126
+ Spikard::StreamingResponse.new(stream)
317
127
  end
318
- ```
319
128
 
320
- ## File Uploads
321
-
322
- ```ruby
129
+ # File uploads
323
130
  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
- }
131
+ file = request[:body]["file"]
132
+ { filename: file.filename, size: file.size }
332
133
  end
333
134
  ```
334
135
 
335
136
  ## Configuration
336
137
 
138
+ Configure the server with middleware options:
139
+
337
140
  ```ruby
338
141
  config = Spikard::ServerConfig.new(
339
142
  host: "0.0.0.0",
340
143
  port: 8080,
341
144
  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
- )
145
+ compression: Spikard::CompressionConfig.new(gzip: true, brotli: true),
146
+ rate_limit: Spikard::RateLimitConfig.new(per_second: 100),
147
+ jwt: Spikard::JwtConfig.new(secret: "key", algorithm: "HS256"),
148
+ static_files: Spikard::StaticFilesConfig.new(directory: "./public"),
149
+ max_body_size: 10 * 1024 * 1024,
150
+ request_timeout: 30
354
151
  )
355
152
 
356
153
  app.run(config: config)
357
154
  ```
358
155
 
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:**
156
+ See [Configuration Reference](lib/spikard/config.rb) for full options.
383
157
 
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
- ```
158
+ ## Lifecycle Hooks
393
159
 
394
- **Static Files:**
160
+ Execute logic at key points in the request lifecycle:
395
161
 
396
162
  ```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
- )
163
+ app.on_request { |req| puts "#{req[:method]} #{req[:path]}" }
164
+ app.pre_validation { |req| req }
165
+ app.pre_handler { |req| req }
166
+ app.on_response { |res| res }
167
+ app.on_error { |res| res }
403
168
  ```
404
169
 
405
- **OpenAPI Documentation:**
170
+ Return a Request/Response object to continue, or a Response to short-circuit.
406
171
 
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
- ```
172
+ ## Real-Time Communication
417
173
 
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
174
+ **WebSockets:**
460
175
 
461
176
  ```ruby
462
177
  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
178
+ def handle_message(message) = { echo: message }
475
179
  end
476
180
 
477
181
  app.websocket("/chat") { ChatHandler.new }
478
182
  ```
479
183
 
480
- ## Server-Sent Events (SSE)
184
+ **Server-Sent Events:**
481
185
 
482
186
  ```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
187
+ class Events < Spikard::SseEventProducer
188
+ def next_event = Spikard::SseEvent.new(data: { msg: "Hello" })
510
189
  end
511
190
 
512
- app.sse("/notifications") { NotificationProducer.new }
191
+ app.sse("/events") { Events.new }
513
192
  ```
514
193
 
515
194
  ## Background Tasks
516
195
 
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
196
+ Offload work after sending response:
523
197
 
198
+ ```ruby
199
+ app.post "/process" do
200
+ Spikard::Background.run { perform_long_task }
524
201
  { status: "processing" }
525
202
  end
526
203
  ```
527
204
 
528
205
  ## Testing
529
206
 
530
- ```ruby
531
- require "spikard"
532
-
533
- app = Spikard::App.new
534
- app.get "/hello" do
535
- { message: "Hello, World!" }
536
- end
207
+ Use the TestClient for integration tests:
537
208
 
209
+ ```ruby
538
210
  client = Spikard::TestClient.new(app)
539
211
 
540
212
  # HTTP requests
541
213
  response = client.get("/hello", query: { name: "Alice" })
542
- puts response.status_code # => 200
543
- puts response.json # => { "message" => "Hello, World!" }
214
+ puts response.status_code # 200
215
+ puts response.json # { "message" => "Hello, World!" }
544
216
 
545
- # POST with JSON
217
+ # POST, WebSocket, SSE all supported
546
218
  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
219
  ws = client.websocket("/chat")
555
- ws.send_json({ message: "hello" })
556
- message = ws.receive_json
557
- ws.close
558
-
559
- # SSE
560
220
  sse = client.sse("/events")
561
- events = sse.events_as_json
562
- puts events.length
563
221
 
564
- # Cleanup
565
222
  client.close
566
223
  ```
567
224
 
568
- ## Running the Server
225
+ ## Type Safety with RBS & Steep
569
226
 
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:
227
+ Full RBS type definitions are included in `sig/spikard.rbs`. Use Steep for type checking:
603
228
 
604
229
  ```bash
605
230
  bundle exec steep check
@@ -607,67 +232,24 @@ bundle exec steep check
607
232
 
608
233
  ## Performance
609
234
 
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).
235
+ Built with zero-overhead FFI via Magnus and rb-sys. Benchmark results: ~8,000 RPS, ~6.5ms latency at 50 concurrency. See [benchmarks](../../snapshots/benchmarks/) for full results.
619
236
 
620
- | Metric | Value |
621
- | --- | --- |
622
- | Avg RPS (all workloads) | 8,271 |
623
- | Avg latency (ms) | 6.50 |
237
+ ## Learn More
624
238
 
625
- Category breakdown:
239
+ **Examples & Code Generation:**
240
+ - [Runnable Examples](../../examples/) - Ruby, Python, TypeScript, PHP, and WASM
241
+ - [Code Generation Guide](../../examples/README.md) - Generate from OpenAPI, GraphQL, AsyncAPI, OpenRPC
626
242
 
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 |
243
+ **Documentation:**
244
+ - [Type Definitions (RBS)](sig/spikard.rbs) - Full Steep type signatures
245
+ - [Main README](../../README.md) - Multi-language ecosystem and feature overview
246
+ - [Architecture Decisions](../../docs/adr/) - Design choices and patterns
634
247
 
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:**
248
+ **Other Languages:**
667
249
  - [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)
250
+ - [TypeScript (npm)](https://www.npmjs.com/package/spikard)
670
251
  - [PHP (Packagist)](https://packagist.org/packages/spikard/spikard)
252
+ - [Rust (Crates.io)](https://crates.io/crates/spikard)
671
253
 
672
254
  ## License
673
255