spikard 0.1.2 → 0.2.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.
data/README.md CHANGED
@@ -1,553 +1,626 @@
1
- # Spikard Ruby
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.
10
-
11
- ## Installation
12
-
13
- **From source (currently):**
14
-
15
- ```bash
16
- cd packages/ruby
17
- bundle install
18
- bundle exec rake ext:build
19
- ```
20
-
21
- **Requirements:**
22
- - Ruby 3.2+
23
- - Bundler
24
- - Rust toolchain (for building native extension)
25
-
26
- ## Quick Start
27
-
28
- ```ruby
29
- require "spikard"
30
-
31
- app = Spikard::App.new
32
-
33
- app.get "/users/:id" do |request|
34
- user_id = request[:path_params]["id"].to_i
35
- { id: user_id, name: "Alice" }
36
- end
37
-
38
- app.post "/users" do |request|
39
- { id: 1, name: request[:body]["name"] }
40
- end
41
-
42
- app.run(port: 8000)
43
- ```
44
-
45
- ## Request Hash Structure
46
-
47
- Handlers receive a single `request` hash argument with the following keys:
48
-
49
- - `:method` - HTTP method (String): `"GET"`, `"POST"`, etc.
50
- - `:path` - URL path (String): `"/users/123"`
51
- - `:path_params` - Path parameters (Hash): `{"id" => "123"}`
52
- - `:query` - Query parameters (Hash): `{"search" => "ruby"}`
53
- - `:raw_query` - Raw query multimap (Hash of Arrays)
54
- - `:headers` - Request headers (Hash): `{"Authorization" => "Bearer..."}`
55
- - `:cookies` - Request cookies (Hash): `{"session_id" => "..."}`
56
- - `:body` - Parsed request body (Hash or nil)
57
- - `:params` - Merged params from path, query, headers, and cookies
58
-
59
- **Example:**
60
-
61
- ```ruby
62
- app.get "/users/:id" do |request|
63
- user_id = request[:path_params]["id"]
64
- search = request[:query]["search"]
65
- auth = request[:headers]["Authorization"]
66
-
67
- { id: user_id, search: search }
68
- end
69
- ```
70
-
71
- ## Route Registration
72
-
73
- ### HTTP Methods
74
-
75
- ```ruby
76
- app.get "/path" do |request|
77
- # Handler code
78
- { method: request[:method] }
79
- end
80
-
81
- app.post "/path" do |request|
82
- { created: true }
83
- end
84
-
85
- app.put "/path" do |request|
86
- { updated: true }
87
- end
88
-
89
- app.patch "/path" do |request|
90
- { patched: true }
91
- end
92
-
93
- app.delete "/path" do |request|
94
- { deleted: true }
95
- end
96
-
97
- app.options "/path" do |request|
98
- { options: [] }
99
- end
100
-
101
- app.head "/path" do |request|
102
- # HEAD request
103
- end
104
-
105
- app.trace "/path" do |request|
106
- # TRACE request
107
- end
108
- ```
109
-
110
- ### Path Parameters
111
-
112
- ```ruby
113
- app.get "/users/:user_id" do |request|
114
- { user_id: request[:path_params]["user_id"].to_i }
115
- end
116
-
117
- app.get "/posts/:post_id/comments/:comment_id" do |request|
118
- {
119
- post_id: request[:path_params]["post_id"].to_i,
120
- comment_id: request[:path_params]["comment_id"].to_i
121
- }
122
- end
123
- ```
124
-
125
- ### Query Parameters
126
-
127
- ```ruby
128
- app.get "/search" do |request|
129
- q = request[:query]["q"]
130
- limit = (request[:query]["limit"] || "10").to_i
131
- { query: q, limit: limit }
132
- end
133
- ```
134
-
135
- ## Validation
136
-
137
- Spikard supports **dry-schema** and **raw JSON Schema objects**.
138
-
139
- ### With dry-schema
140
-
141
- ```ruby
142
- require "dry-schema"
143
- Dry::Schema.load_extensions(:json_schema)
144
-
145
- UserSchema = Dry::Schema.JSON do
146
- required(:name).filled(:str?)
147
- required(:email).filled(:str?)
148
- required(:age).filled(:int?)
149
- end
150
-
151
- app.post "/users", request_schema: UserSchema do |request|
152
- # request[:body] is validated against schema
153
- { id: 1, name: request[:body]["name"] }
154
- end
155
- ```
156
-
157
- ### With raw JSON Schema
158
-
159
- ```ruby
160
- user_schema = {
161
- "type" => "object",
162
- "properties" => {
163
- "name" => { "type" => "string" },
164
- "email" => { "type" => "string", "format" => "email" }
165
- },
166
- "required" => ["name", "email"]
167
- }
168
-
169
- app.post "/users", request_schema: user_schema do |request|
170
- { id: 1, name: request[:body]["name"], email: request[:body]["email"] }
171
- end
172
- ```
173
-
174
- ### With dry-struct
175
-
176
- ```ruby
177
- require "dry-struct"
178
- require "dry-types"
179
-
180
- module Types
181
- include Dry.Types()
182
- end
183
-
184
- class User < Dry::Struct
185
- attribute :name, Types::String
186
- attribute :email, Types::String
187
- attribute :age, Types::Integer
188
- end
189
-
190
- app.post "/users", request_schema: User do |request|
191
- # request[:body] validated as User
192
- { id: 1, name: request[:body]["name"] }
193
- end
194
- ```
195
-
196
- ## Response Types
197
-
198
- ### Simple Hash Response
199
-
200
- ```ruby
201
- app.get "/hello" do
202
- { message: "Hello, World!" }
203
- end
204
- ```
205
-
206
- ### String Response
207
-
208
- ```ruby
209
- app.get "/text" do
210
- "Plain text response"
211
- end
212
- ```
213
-
214
- ### Full Response Object
215
-
216
- ```ruby
217
- app.post "/users" do |request|
218
- Spikard::Response.new(
219
- content: { id: 1, name: request[:body]["name"] },
220
- status_code: 201,
221
- headers: { "X-Custom" => "value" }
222
- )
223
- end
224
- ```
225
-
226
- ### Streaming Response
227
-
228
- ```ruby
229
- app.get "/stream" do
230
- stream = Enumerator.new do |yielder|
231
- 10.times do |i|
232
- yielder << "Chunk #{i}\n"
233
- sleep 0.1
234
- end
235
- end
236
-
237
- Spikard::StreamingResponse.new(
238
- stream,
239
- status_code: 200,
240
- headers: { "Content-Type" => "text/plain" }
241
- )
242
- end
243
- ```
244
-
245
- ## File Uploads
246
-
247
- ```ruby
248
- app.post "/upload", file_params: true do |request|
249
- file = request[:body]["file"] # UploadFile instance
250
-
251
- {
252
- filename: file.filename,
253
- size: file.size,
254
- content_type: file.content_type,
255
- content: file.read
256
- }
257
- end
258
- ```
259
-
260
- ## Configuration
261
-
262
- ```ruby
263
- config = Spikard::ServerConfig.new(
264
- host: "0.0.0.0",
265
- port: 8080,
266
- workers: 4,
267
- enable_request_id: true,
268
- max_body_size: 10 * 1024 * 1024, # 10 MB
269
- request_timeout: 30,
270
- compression: Spikard::CompressionConfig.new(
271
- gzip: true,
272
- brotli: true,
273
- quality: 6
274
- ),
275
- rate_limit: Spikard::RateLimitConfig.new(
276
- per_second: 100,
277
- burst: 200
278
- )
279
- )
280
-
281
- app.run(config: config)
282
- ```
283
-
284
- ### Middleware Configuration
285
-
286
- **Compression:**
287
-
288
- ```ruby
289
- compression = Spikard::CompressionConfig.new(
290
- gzip: true,
291
- brotli: true,
292
- min_size: 1024,
293
- quality: 6
294
- )
295
- ```
296
-
297
- **Rate Limiting:**
298
-
299
- ```ruby
300
- rate_limit = Spikard::RateLimitConfig.new(
301
- per_second: 100,
302
- burst: 200,
303
- ip_based: true
304
- )
305
- ```
306
-
307
- **JWT Authentication:**
308
-
309
- ```ruby
310
- jwt = Spikard::JwtConfig.new(
311
- secret: "your-secret-key",
312
- algorithm: "HS256",
313
- audience: ["api.example.com"],
314
- issuer: "auth.example.com",
315
- leeway: 30
316
- )
317
- ```
318
-
319
- **Static Files:**
320
-
321
- ```ruby
322
- static = Spikard::StaticFilesConfig.new(
323
- directory: "./public",
324
- route_prefix: "/static",
325
- index_file: true,
326
- cache_control: "public, max-age=3600"
327
- )
328
- ```
329
-
330
- **OpenAPI Documentation:**
331
-
332
- ```ruby
333
- openapi = Spikard::OpenApiConfig.new(
334
- enabled: true,
335
- title: "My API",
336
- version: "1.0.0",
337
- description: "API docs",
338
- swagger_ui_path: "/docs",
339
- redoc_path: "/redoc"
340
- )
341
- ```
342
-
343
- ## Lifecycle Hooks
344
-
345
- ```ruby
346
- app.on_request do |request|
347
- puts "#{request[:method]} #{request[:path]}"
348
- request
349
- end
350
-
351
- app.pre_validation do |request|
352
- if too_many_requests?
353
- Spikard::Response.new(
354
- content: { error: "Rate limit exceeded" },
355
- status_code: 429
356
- )
357
- else
358
- request
359
- end
360
- end
361
-
362
- app.pre_handler do |request|
363
- if invalid_token?(request[:headers]["Authorization"])
364
- Spikard::Response.new(
365
- content: { error: "Unauthorized" },
366
- status_code: 401
367
- )
368
- else
369
- request
370
- end
371
- end
372
-
373
- app.on_response do |response|
374
- response.headers["X-Frame-Options"] = "DENY"
375
- response
376
- end
377
-
378
- app.on_error do |response|
379
- puts "Error: #{response.status_code}"
380
- response
381
- end
382
- ```
383
-
384
- ## WebSockets
385
-
386
- ```ruby
387
- class ChatHandler < Spikard::WebSocketHandler
388
- def on_connect
389
- puts "Client connected"
390
- end
391
-
392
- def handle_message(message)
393
- # message is a Hash (parsed JSON)
394
- { echo: message, timestamp: Time.now.to_i }
395
- end
396
-
397
- def on_disconnect
398
- puts "Client disconnected"
399
- end
400
- end
401
-
402
- app.websocket("/chat") { ChatHandler.new }
403
- ```
404
-
405
- ## Server-Sent Events (SSE)
406
-
407
- ```ruby
408
- class NotificationProducer < Spikard::SseEventProducer
409
- def initialize
410
- @count = 0
411
- end
412
-
413
- def on_connect
414
- puts "Client connected to SSE stream"
415
- end
416
-
417
- def next_event
418
- sleep 1
419
-
420
- return nil if @count >= 10 # End stream
421
-
422
- event = Spikard::SseEvent.new(
423
- data: { message: "Notification #{@count}" },
424
- event_type: "notification",
425
- id: @count.to_s,
426
- retry_ms: 3000
427
- )
428
- @count += 1
429
- event
430
- end
431
-
432
- def on_disconnect
433
- puts "Client disconnected from SSE"
434
- end
435
- end
436
-
437
- app.sse("/notifications") { NotificationProducer.new }
438
- ```
439
-
440
- ## Background Tasks
441
-
442
- ```ruby
443
- app.post "/process" do |request|
444
- Spikard::Background.run do
445
- # Heavy processing after response
446
- ProcessData.perform(request[:path_params]["id"])
447
- end
448
-
449
- { status: "processing" }
450
- end
451
- ```
452
-
453
- ## Testing
454
-
455
- ```ruby
456
- require "spikard"
457
-
458
- app = Spikard::App.new
459
- app.get "/hello" do
460
- { message: "Hello, World!" }
461
- end
462
-
463
- client = Spikard::TestClient.new(app)
464
-
465
- # HTTP requests
466
- response = client.get("/hello", query: { name: "Alice" })
467
- puts response.status_code # => 200
468
- puts response.json # => { "message" => "Hello, World!" }
469
-
470
- # POST with JSON
471
- response = client.post("/users", json: { name: "Bob" })
472
-
473
- # File upload
474
- response = client.post("/upload", files: {
475
- file: ["test.txt", "content", "text/plain"]
476
- })
477
-
478
- # WebSocket
479
- ws = client.websocket("/chat")
480
- ws.send_json({ message: "hello" })
481
- message = ws.receive_json
482
- ws.close
483
-
484
- # SSE
485
- sse = client.sse("/events")
486
- events = sse.events_as_json
487
- puts events.length
488
-
489
- # Cleanup
490
- client.close
491
- ```
492
-
493
- ## Running the Server
494
-
495
- ```ruby
496
- # Development
497
- app.run(port: 8000)
498
-
499
- # Production
500
- config = Spikard::ServerConfig.new(
501
- host: "0.0.0.0",
502
- port: 8080,
503
- workers: 4
504
- )
505
- app.run(config: config)
506
- ```
507
-
508
- ## Type Safety with RBS
509
-
510
- RBS type signatures are provided in `sig/spikard.rbs`:
511
-
512
- ```ruby
513
- module Spikard
514
- class App
515
- def initialize: () -> void
516
- def get: (String, ?handler_name: String?, **untyped) { (untyped) -> untyped } -> Proc
517
- def post: (String, ?handler_name: String?, **untyped) { (untyped) -> untyped } -> Proc
518
- def run: (?config: ServerConfig | Hash[Symbol, untyped]?) -> void
519
- end
520
-
521
- class ServerConfig
522
- def initialize: (?host: String, ?port: Integer, **untyped) -> void
523
- end
524
- end
525
- ```
526
-
527
- Use with Steep for type checking:
528
-
529
- ```bash
530
- bundle exec steep check
531
- ```
532
-
533
- ## Performance
534
-
535
- Ruby bindings use:
536
- - **Magnus** for zero-overhead FFI
537
- - **rb-sys** for modern Ruby 3.2+ integration
538
- - Idiomatic Ruby blocks and procs
539
- - GC-safe handler storage
540
-
541
- ## Examples
542
-
543
- See `/examples/ruby/` for more examples.
544
-
545
- ## Documentation
546
-
547
- - [Main Project README](../../README.md)
548
- - [Contributing Guide](../../CONTRIBUTING.md)
549
- - [RBS Type Signatures](sig/spikard.rbs)
550
-
551
- ## License
552
-
553
- MIT
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
+ [![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.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
+ ## Quick Start
51
+
52
+ ```ruby
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
60
+
61
+ app = Spikard::App.new
62
+
63
+ app.get "/users/:id" do |request|
64
+ user_id = request[:path_params]["id"].to_i
65
+ { id: user_id, name: "Alice" }
66
+ end
67
+
68
+ app.post "/users", request_schema: UserSchema do |request|
69
+ body = request[:body]
70
+ { id: 1, name: body["name"], email: body["email"] }
71
+ end
72
+
73
+ app.run(port: 8000)
74
+ ```
75
+
76
+ ## Request Hash Structure
77
+
78
+ Handlers receive a single `request` hash argument with the following keys:
79
+
80
+ - `:method` - HTTP method (String): `"GET"`, `"POST"`, etc.
81
+ - `:path` - URL path (String): `"/users/123"`
82
+ - `:path_params` - Path parameters (Hash): `{"id" => "123"}`
83
+ - `:query` - Query parameters (Hash): `{"search" => "ruby"}`
84
+ - `:raw_query` - Raw query multimap (Hash of Arrays)
85
+ - `:headers` - Request headers (Hash): `{"Authorization" => "Bearer..."}`
86
+ - `:cookies` - Request cookies (Hash): `{"session_id" => "..."}`
87
+ - `:body` - Parsed request body (Hash or nil)
88
+ - `:params` - Merged params from path, query, headers, and cookies
89
+
90
+ **Example:**
91
+
92
+ ```ruby
93
+ app.get "/users/:id" do |request|
94
+ user_id = request[:path_params]["id"]
95
+ search = request[:query]["search"]
96
+ auth = request[:headers]["Authorization"]
97
+
98
+ { id: user_id, search: search }
99
+ end
100
+ ```
101
+
102
+ ## Route Registration
103
+
104
+ ### HTTP Methods
105
+
106
+ ```ruby
107
+ app.get "/path" do |request|
108
+ # Handler code
109
+ { method: request[:method] }
110
+ end
111
+
112
+ app.post "/path" do |request|
113
+ { created: true }
114
+ end
115
+
116
+ app.put "/path" do |request|
117
+ { updated: true }
118
+ end
119
+
120
+ app.patch "/path" do |request|
121
+ { patched: true }
122
+ end
123
+
124
+ app.delete "/path" do |request|
125
+ { deleted: true }
126
+ end
127
+
128
+ app.options "/path" do |request|
129
+ { options: [] }
130
+ end
131
+
132
+ app.head "/path" do |request|
133
+ # HEAD request
134
+ end
135
+
136
+ app.trace "/path" do |request|
137
+ # TRACE request
138
+ end
139
+ ```
140
+
141
+ ### Path Parameters
142
+
143
+ ```ruby
144
+ app.get "/users/:user_id" do |request|
145
+ { user_id: request[:path_params]["user_id"].to_i }
146
+ end
147
+
148
+ app.get "/posts/:post_id/comments/:comment_id" do |request|
149
+ {
150
+ post_id: request[:path_params]["post_id"].to_i,
151
+ comment_id: request[:path_params]["comment_id"].to_i
152
+ }
153
+ end
154
+ ```
155
+
156
+ ### Query Parameters
157
+
158
+ ```ruby
159
+ app.get "/search" do |request|
160
+ q = request[:query]["q"]
161
+ limit = (request[:query]["limit"] || "10").to_i
162
+ { query: q, limit: limit }
163
+ end
164
+ ```
165
+
166
+ ## Validation
167
+
168
+ Spikard supports **dry-schema** and **raw JSON Schema objects**.
169
+
170
+ ### With dry-schema
171
+
172
+ ```ruby
173
+ require "dry-schema"
174
+ Dry::Schema.load_extensions(:json_schema)
175
+
176
+ UserSchema = Dry::Schema.JSON do
177
+ required(:name).filled(:str?)
178
+ required(:email).filled(:str?)
179
+ required(:age).filled(:int?)
180
+ end
181
+
182
+ app.post "/users", request_schema: UserSchema do |request|
183
+ # request[:body] is validated against schema
184
+ { id: 1, name: request[:body]["name"] }
185
+ end
186
+ ```
187
+
188
+ ### With raw JSON Schema
189
+
190
+ ```ruby
191
+ user_schema = {
192
+ "type" => "object",
193
+ "properties" => {
194
+ "name" => { "type" => "string" },
195
+ "email" => { "type" => "string", "format" => "email" }
196
+ },
197
+ "required" => ["name", "email"]
198
+ }
199
+
200
+ app.post "/users", request_schema: user_schema do |request|
201
+ { id: 1, name: request[:body]["name"], email: request[:body]["email"] }
202
+ end
203
+ ```
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
+
220
+ ### With dry-struct
221
+
222
+ ```ruby
223
+ require "dry-struct"
224
+ require "dry-types"
225
+
226
+ module Types
227
+ include Dry.Types()
228
+ end
229
+
230
+ class User < Dry::Struct
231
+ attribute :name, Types::String
232
+ attribute :email, Types::String
233
+ attribute :age, Types::Integer
234
+ end
235
+
236
+ app.post "/users", request_schema: User do |request|
237
+ # request[:body] validated as User
238
+ { id: 1, name: request[:body]["name"] }
239
+ end
240
+ ```
241
+
242
+ ## Response Types
243
+
244
+ ### Simple Hash Response
245
+
246
+ ```ruby
247
+ app.get "/hello" do
248
+ { message: "Hello, World!" }
249
+ end
250
+ ```
251
+
252
+ ### String Response
253
+
254
+ ```ruby
255
+ app.get "/text" do
256
+ "Plain text response"
257
+ end
258
+ ```
259
+
260
+ ### Full Response Object
261
+
262
+ ```ruby
263
+ app.post "/users" do |request|
264
+ Spikard::Response.new(
265
+ content: { id: 1, name: request[:body]["name"] },
266
+ status_code: 201,
267
+ headers: { "X-Custom" => "value" }
268
+ )
269
+ end
270
+ ```
271
+
272
+ ### Streaming Response
273
+
274
+ ```ruby
275
+ app.get "/stream" do
276
+ stream = Enumerator.new do |yielder|
277
+ 10.times do |i|
278
+ yielder << "Chunk #{i}\n"
279
+ sleep 0.1
280
+ end
281
+ end
282
+
283
+ Spikard::StreamingResponse.new(
284
+ stream,
285
+ status_code: 200,
286
+ headers: { "Content-Type" => "text/plain" }
287
+ )
288
+ end
289
+ ```
290
+
291
+ ## File Uploads
292
+
293
+ ```ruby
294
+ app.post "/upload", file_params: true do |request|
295
+ file = request[:body]["file"] # UploadFile instance
296
+
297
+ {
298
+ filename: file.filename,
299
+ size: file.size,
300
+ content_type: file.content_type,
301
+ content: file.read
302
+ }
303
+ end
304
+ ```
305
+
306
+ ## Configuration
307
+
308
+ ```ruby
309
+ config = Spikard::ServerConfig.new(
310
+ host: "0.0.0.0",
311
+ port: 8080,
312
+ workers: 4,
313
+ enable_request_id: true,
314
+ max_body_size: 10 * 1024 * 1024, # 10 MB
315
+ request_timeout: 30,
316
+ compression: Spikard::CompressionConfig.new(
317
+ gzip: true,
318
+ brotli: true,
319
+ quality: 6
320
+ ),
321
+ rate_limit: Spikard::RateLimitConfig.new(
322
+ per_second: 100,
323
+ burst: 200
324
+ )
325
+ )
326
+
327
+ app.run(config: config)
328
+ ```
329
+
330
+ ### Middleware Configuration
331
+
332
+ **Compression:**
333
+
334
+ ```ruby
335
+ compression = Spikard::CompressionConfig.new(
336
+ gzip: true,
337
+ brotli: true,
338
+ min_size: 1024,
339
+ quality: 6
340
+ )
341
+ ```
342
+
343
+ **Rate Limiting:**
344
+
345
+ ```ruby
346
+ rate_limit = Spikard::RateLimitConfig.new(
347
+ per_second: 100,
348
+ burst: 200,
349
+ ip_based: true
350
+ )
351
+ ```
352
+
353
+ **JWT Authentication:**
354
+
355
+ ```ruby
356
+ jwt = Spikard::JwtConfig.new(
357
+ secret: "your-secret-key",
358
+ algorithm: "HS256",
359
+ audience: ["api.example.com"],
360
+ issuer: "auth.example.com",
361
+ leeway: 30
362
+ )
363
+ ```
364
+
365
+ **Static Files:**
366
+
367
+ ```ruby
368
+ static = Spikard::StaticFilesConfig.new(
369
+ directory: "./public",
370
+ route_prefix: "/static",
371
+ index_file: true,
372
+ cache_control: "public, max-age=3600"
373
+ )
374
+ ```
375
+
376
+ **OpenAPI Documentation:**
377
+
378
+ ```ruby
379
+ openapi = Spikard::OpenApiConfig.new(
380
+ enabled: true,
381
+ title: "My API",
382
+ version: "1.0.0",
383
+ description: "API docs",
384
+ swagger_ui_path: "/docs",
385
+ redoc_path: "/redoc"
386
+ )
387
+ ```
388
+
389
+ ## Lifecycle Hooks
390
+
391
+ ```ruby
392
+ app.on_request do |request|
393
+ puts "#{request[:method]} #{request[:path]}"
394
+ request
395
+ end
396
+
397
+ app.pre_validation do |request|
398
+ if too_many_requests?
399
+ Spikard::Response.new(
400
+ content: { error: "Rate limit exceeded" },
401
+ status_code: 429
402
+ )
403
+ else
404
+ request
405
+ end
406
+ end
407
+
408
+ app.pre_handler do |request|
409
+ if invalid_token?(request[:headers]["Authorization"])
410
+ Spikard::Response.new(
411
+ content: { error: "Unauthorized" },
412
+ status_code: 401
413
+ )
414
+ else
415
+ request
416
+ end
417
+ end
418
+
419
+ app.on_response do |response|
420
+ response.headers["X-Frame-Options"] = "DENY"
421
+ response
422
+ end
423
+
424
+ app.on_error do |response|
425
+ puts "Error: #{response.status_code}"
426
+ response
427
+ end
428
+ ```
429
+
430
+ ## WebSockets
431
+
432
+ ```ruby
433
+ class ChatHandler < Spikard::WebSocketHandler
434
+ def on_connect
435
+ puts "Client connected"
436
+ end
437
+
438
+ def handle_message(message)
439
+ # message is a Hash (parsed JSON)
440
+ { echo: message, timestamp: Time.now.to_i }
441
+ end
442
+
443
+ def on_disconnect
444
+ puts "Client disconnected"
445
+ end
446
+ end
447
+
448
+ app.websocket("/chat") { ChatHandler.new }
449
+ ```
450
+
451
+ ## Server-Sent Events (SSE)
452
+
453
+ ```ruby
454
+ class NotificationProducer < Spikard::SseEventProducer
455
+ def initialize
456
+ @count = 0
457
+ end
458
+
459
+ def on_connect
460
+ puts "Client connected to SSE stream"
461
+ end
462
+
463
+ def next_event
464
+ sleep 1
465
+
466
+ return nil if @count >= 10 # End stream
467
+
468
+ event = Spikard::SseEvent.new(
469
+ data: { message: "Notification #{@count}" },
470
+ event_type: "notification",
471
+ id: @count.to_s,
472
+ retry_ms: 3000
473
+ )
474
+ @count += 1
475
+ event
476
+ end
477
+
478
+ def on_disconnect
479
+ puts "Client disconnected from SSE"
480
+ end
481
+ end
482
+
483
+ app.sse("/notifications") { NotificationProducer.new }
484
+ ```
485
+
486
+ ## Background Tasks
487
+
488
+ ```ruby
489
+ app.post "/process" do |request|
490
+ Spikard::Background.run do
491
+ # Heavy processing after response
492
+ ProcessData.perform(request[:path_params]["id"])
493
+ end
494
+
495
+ { status: "processing" }
496
+ end
497
+ ```
498
+
499
+ ## Testing
500
+
501
+ ```ruby
502
+ require "spikard"
503
+
504
+ app = Spikard::App.new
505
+ app.get "/hello" do
506
+ { message: "Hello, World!" }
507
+ end
508
+
509
+ client = Spikard::TestClient.new(app)
510
+
511
+ # HTTP requests
512
+ response = client.get("/hello", query: { name: "Alice" })
513
+ puts response.status_code # => 200
514
+ puts response.json # => { "message" => "Hello, World!" }
515
+
516
+ # POST with JSON
517
+ response = client.post("/users", json: { name: "Bob" })
518
+
519
+ # File upload
520
+ response = client.post("/upload", files: {
521
+ file: ["test.txt", "content", "text/plain"]
522
+ })
523
+
524
+ # WebSocket
525
+ ws = client.websocket("/chat")
526
+ ws.send_json({ message: "hello" })
527
+ message = ws.receive_json
528
+ ws.close
529
+
530
+ # SSE
531
+ sse = client.sse("/events")
532
+ events = sse.events_as_json
533
+ puts events.length
534
+
535
+ # Cleanup
536
+ client.close
537
+ ```
538
+
539
+ ## Running the Server
540
+
541
+ ```ruby
542
+ # Development
543
+ app.run(port: 8000)
544
+
545
+ # Production
546
+ config = Spikard::ServerConfig.new(
547
+ host: "0.0.0.0",
548
+ port: 8080,
549
+ workers: 4
550
+ )
551
+ app.run(config: config)
552
+ ```
553
+
554
+ ## Type Safety with RBS
555
+
556
+ RBS type signatures are provided in `sig/spikard.rbs`:
557
+
558
+ ```ruby
559
+ module Spikard
560
+ class App
561
+ def initialize: () -> void
562
+ def get: (String, ?handler_name: String?, **untyped) { (untyped) -> untyped } -> Proc
563
+ def post: (String, ?handler_name: String?, **untyped) { (untyped) -> untyped } -> Proc
564
+ def run: (?config: ServerConfig | Hash[Symbol, untyped]?) -> void
565
+ end
566
+
567
+ class ServerConfig
568
+ def initialize: (?host: String, ?port: Integer, **untyped) -> void
569
+ end
570
+ end
571
+ ```
572
+
573
+ Use with Steep for type checking:
574
+
575
+ ```bash
576
+ bundle exec steep check
577
+ ```
578
+
579
+ ## Performance
580
+
581
+ Ruby bindings use:
582
+ - **Magnus** for zero-overhead FFI
583
+ - **rb-sys** for modern Ruby 3.2+ integration
584
+ - Idiomatic Ruby blocks and procs
585
+ - GC-safe handler storage
586
+
587
+ ## Examples
588
+
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.
604
+
605
+ ## Documentation
606
+
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)
621
+ - [Rust (Crates.io)](https://crates.io/crates/spikard)
622
+ - [PHP (Packagist)](https://packagist.org/packages/spikard/spikard)
623
+
624
+ ## License
625
+
626
+ MIT - See [LICENSE](../../LICENSE) for details