spikard 0.4.0-x86_64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +1 -0
- data/README.md +659 -0
- data/ext/spikard_rb/Cargo.toml +17 -0
- data/ext/spikard_rb/extconf.rb +10 -0
- data/ext/spikard_rb/src/lib.rs +6 -0
- data/lib/spikard/app.rb +405 -0
- data/lib/spikard/background.rb +27 -0
- data/lib/spikard/config.rb +396 -0
- data/lib/spikard/converters.rb +13 -0
- data/lib/spikard/handler_wrapper.rb +113 -0
- data/lib/spikard/provide.rb +214 -0
- data/lib/spikard/response.rb +173 -0
- data/lib/spikard/schema.rb +243 -0
- data/lib/spikard/sse.rb +111 -0
- data/lib/spikard/streaming_response.rb +44 -0
- data/lib/spikard/testing.rb +221 -0
- data/lib/spikard/upload_file.rb +131 -0
- data/lib/spikard/version.rb +5 -0
- data/lib/spikard/websocket.rb +59 -0
- data/lib/spikard.rb +43 -0
- data/sig/spikard.rbs +366 -0
- data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +5 -0
- data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +139 -0
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +561 -0
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +403 -0
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +274 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +25 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +298 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +637 -0
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +309 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +355 -0
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +502 -0
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +389 -0
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +413 -0
- data/vendor/crates/spikard-core/Cargo.toml +40 -0
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
- data/vendor/crates/spikard-core/src/debug.rs +63 -0
- data/vendor/crates/spikard-core/src/di/container.rs +726 -0
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
- data/vendor/crates/spikard-core/src/di/error.rs +118 -0
- data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
- data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
- data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
- data/vendor/crates/spikard-core/src/di/value.rs +283 -0
- data/vendor/crates/spikard-core/src/errors.rs +39 -0
- data/vendor/crates/spikard-core/src/http.rs +153 -0
- data/vendor/crates/spikard-core/src/lib.rs +29 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
- data/vendor/crates/spikard-core/src/metadata.rs +397 -0
- data/vendor/crates/spikard-core/src/parameters.rs +723 -0
- data/vendor/crates/spikard-core/src/problem.rs +310 -0
- data/vendor/crates/spikard-core/src/request_data.rs +189 -0
- data/vendor/crates/spikard-core/src/router.rs +249 -0
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +689 -0
- data/vendor/crates/spikard-core/src/validation/mod.rs +459 -0
- data/vendor/crates/spikard-http/Cargo.toml +58 -0
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +147 -0
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +91 -0
- data/vendor/crates/spikard-http/src/auth.rs +247 -0
- data/vendor/crates/spikard-http/src/background.rs +1562 -0
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
- data/vendor/crates/spikard-http/src/cors.rs +490 -0
- data/vendor/crates/spikard-http/src/debug.rs +63 -0
- data/vendor/crates/spikard-http/src/di_handler.rs +1878 -0
- data/vendor/crates/spikard-http/src/handler_response.rs +532 -0
- data/vendor/crates/spikard-http/src/handler_trait.rs +861 -0
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
- data/vendor/crates/spikard-http/src/lib.rs +524 -0
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
- data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
- data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +930 -0
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +541 -0
- data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -0
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +867 -0
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +678 -0
- data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
- data/vendor/crates/spikard-http/src/response.rs +399 -0
- data/vendor/crates/spikard-http/src/server/handler.rs +1557 -0
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
- data/vendor/crates/spikard-http/src/server/mod.rs +806 -0
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +630 -0
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +497 -0
- data/vendor/crates/spikard-http/src/sse.rs +961 -0
- data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
- data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
- data/vendor/crates/spikard-http/src/testing.rs +377 -0
- data/vendor/crates/spikard-http/src/websocket.rs +831 -0
- data/vendor/crates/spikard-http/tests/background_behavior.rs +918 -0
- data/vendor/crates/spikard-http/tests/common/handlers.rs +308 -0
- data/vendor/crates/spikard-http/tests/common/mod.rs +21 -0
- data/vendor/crates/spikard-http/tests/di_integration.rs +202 -0
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +4 -0
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1135 -0
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +688 -0
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +324 -0
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +728 -0
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +724 -0
- data/vendor/crates/spikard-rb/Cargo.toml +43 -0
- data/vendor/crates/spikard-rb/build.rs +199 -0
- data/vendor/crates/spikard-rb/src/background.rs +63 -0
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/config/server_config.rs +283 -0
- data/vendor/crates/spikard-rb/src/conversion.rs +459 -0
- data/vendor/crates/spikard-rb/src/di/builder.rs +105 -0
- data/vendor/crates/spikard-rb/src/di/mod.rs +413 -0
- data/vendor/crates/spikard-rb/src/handler.rs +612 -0
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
- data/vendor/crates/spikard-rb/src/lib.rs +1857 -0
- data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -0
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +427 -0
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +326 -0
- data/vendor/crates/spikard-rb/src/server.rs +283 -0
- data/vendor/crates/spikard-rb/src/sse.rs +231 -0
- data/vendor/crates/spikard-rb/src/testing/client.rs +404 -0
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
- data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -0
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +221 -0
- data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
- data/vendor/crates/spikard-rb/tests/magnus_ffi_tests.rs +14 -0
- metadata +213 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 456c14e0b98d776f632e10f5c154cacd4dc6e886ede14eb68e8a7f06e49d80b9
|
|
4
|
+
data.tar.gz: f0d630b5bd83b1bc653da774b4fdfb950fad568dae7b07a90b4bcf8772dde81f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 52fef672b90dea8986266e474a40558fa8c38b92c4aa4a79fdb3806bf279d07fc5ae5b170bdf57800a5c3f0d6330273c1ea66f6003213ef6799258b7dcdf6c84
|
|
7
|
+
data.tar.gz: 6a9479972f513edc4ec0d7ad990cff9f0a1044f1b4716fee86aa6b98845d4cd264d969766b9611610182ebe4d8324c197a171f1908052f5c06e91f74c5128b43
|
data/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
See ../../LICENSE
|
data/README.md
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
# Spikard Ruby
|
|
2
|
+
|
|
3
|
+
[](https://spikard.dev)
|
|
4
|
+
[](https://rubygems.org/gems/spikard)
|
|
5
|
+
[](https://rubygems.org/gems/spikard)
|
|
6
|
+
[](https://www.ruby-lang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://codecov.io/gh/Goldziher/spikard)
|
|
9
|
+
[](https://pypi.org/project/spikard/)
|
|
10
|
+
[](https://www.npmjs.com/package/spikard)
|
|
11
|
+
[](https://crates.io/crates/spikard)
|
|
12
|
+
[](https://packagist.org/packages/spikard/spikard)
|
|
13
|
+
|
|
14
|
+
High-performance Ruby web framework with a Rust core. Build REST APIs with Sinatra-style routing and zero-overhead async handlers backed by Axum and Tower-HTTP.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Rust-powered performance**: High-throughput HTTP server backed by Tokio and Axum
|
|
19
|
+
- **Sinatra-style routing**: Familiar `get`, `post`, `put`, `patch`, `delete` DSL
|
|
20
|
+
- **Type-safe with RBS**: Full RBS type definitions for Steep type checking
|
|
21
|
+
- **Zero-copy serialization**: Direct Rust-to-Ruby object conversion via Magnus
|
|
22
|
+
- **Async-first**: Non-blocking handlers with full async/await support
|
|
23
|
+
- **Middleware stack**: Compression, rate limiting, request IDs, authentication
|
|
24
|
+
- **WebSockets & SSE**: Native real-time communication primitives
|
|
25
|
+
- **Request validation**: JSON Schema and dry-schema support
|
|
26
|
+
- **Lifecycle hooks**: onRequest, preValidation, preHandler, onResponse, onError
|
|
27
|
+
- **Dependency injection**: Built-in container for services and factories
|
|
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 |request, 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
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "spikard-rb-ext"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
|
|
7
|
+
|
|
8
|
+
[lib]
|
|
9
|
+
name = "spikard_rb"
|
|
10
|
+
crate-type = ["cdylib"]
|
|
11
|
+
|
|
12
|
+
[workspace]
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
magnus = { version = "0.8.2", features = ["rb-sys"] }
|
|
16
|
+
# Use vendored crates for packaged gem distribution
|
|
17
|
+
spikard_rb_core = { package = "spikard-rb", path = "../../vendor/crates/spikard-rb" }
|