whoosh 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +413 -0
- data/exe/whoosh +6 -0
- data/lib/whoosh/app.rb +655 -0
- data/lib/whoosh/auth/access_control.rb +26 -0
- data/lib/whoosh/auth/api_key.rb +30 -0
- data/lib/whoosh/auth/jwt.rb +88 -0
- data/lib/whoosh/auth/oauth2.rb +33 -0
- data/lib/whoosh/auth/rate_limiter.rb +86 -0
- data/lib/whoosh/auth/token_tracker.rb +40 -0
- data/lib/whoosh/cache/memory_store.rb +57 -0
- data/lib/whoosh/cache/redis_store.rb +72 -0
- data/lib/whoosh/cache.rb +26 -0
- data/lib/whoosh/cli/generators.rb +133 -0
- data/lib/whoosh/cli/main.rb +277 -0
- data/lib/whoosh/cli/project_generator.rb +172 -0
- data/lib/whoosh/config.rb +160 -0
- data/lib/whoosh/database.rb +47 -0
- data/lib/whoosh/dependency_injection.rb +103 -0
- data/lib/whoosh/endpoint.rb +79 -0
- data/lib/whoosh/env_loader.rb +46 -0
- data/lib/whoosh/errors.rb +68 -0
- data/lib/whoosh/http/response.rb +26 -0
- data/lib/whoosh/http.rb +73 -0
- data/lib/whoosh/instrumentation.rb +22 -0
- data/lib/whoosh/job.rb +24 -0
- data/lib/whoosh/jobs/memory_backend.rb +45 -0
- data/lib/whoosh/jobs/worker.rb +73 -0
- data/lib/whoosh/jobs.rb +50 -0
- data/lib/whoosh/logger.rb +62 -0
- data/lib/whoosh/mcp/client.rb +71 -0
- data/lib/whoosh/mcp/client_manager.rb +73 -0
- data/lib/whoosh/mcp/protocol.rb +39 -0
- data/lib/whoosh/mcp/server.rb +66 -0
- data/lib/whoosh/mcp/transport/sse.rb +26 -0
- data/lib/whoosh/mcp/transport/stdio.rb +33 -0
- data/lib/whoosh/metrics.rb +84 -0
- data/lib/whoosh/middleware/cors.rb +61 -0
- data/lib/whoosh/middleware/plugin_hooks.rb +27 -0
- data/lib/whoosh/middleware/request_limit.rb +28 -0
- data/lib/whoosh/middleware/request_logger.rb +39 -0
- data/lib/whoosh/middleware/security_headers.rb +28 -0
- data/lib/whoosh/middleware/stack.rb +25 -0
- data/lib/whoosh/openapi/generator.rb +50 -0
- data/lib/whoosh/openapi/schema_converter.rb +48 -0
- data/lib/whoosh/openapi/ui.rb +62 -0
- data/lib/whoosh/paginate.rb +64 -0
- data/lib/whoosh/performance.rb +20 -0
- data/lib/whoosh/plugins/base.rb +42 -0
- data/lib/whoosh/plugins/registry.rb +139 -0
- data/lib/whoosh/request.rb +93 -0
- data/lib/whoosh/response.rb +39 -0
- data/lib/whoosh/router.rb +112 -0
- data/lib/whoosh/schema.rb +194 -0
- data/lib/whoosh/serialization/json.rb +73 -0
- data/lib/whoosh/serialization/msgpack.rb +51 -0
- data/lib/whoosh/serialization/negotiator.rb +37 -0
- data/lib/whoosh/serialization/protobuf.rb +43 -0
- data/lib/whoosh/shutdown.rb +30 -0
- data/lib/whoosh/storage/local.rb +24 -0
- data/lib/whoosh/storage/s3.rb +31 -0
- data/lib/whoosh/storage.rb +20 -0
- data/lib/whoosh/streaming/llm_stream.rb +51 -0
- data/lib/whoosh/streaming/sse.rb +61 -0
- data/lib/whoosh/streaming/stream_body.rb +59 -0
- data/lib/whoosh/streaming/websocket.rb +51 -0
- data/lib/whoosh/test.rb +70 -0
- data/lib/whoosh/types.rb +11 -0
- data/lib/whoosh/uploaded_file.rb +47 -0
- data/lib/whoosh/version.rb +5 -0
- data/lib/whoosh.rb +86 -0
- metadata +265 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dcdd58c23ddbf6deaa47e53584d661eaad24ac247734aa7bf9cb3a3b3222aae3
|
|
4
|
+
data.tar.gz: cb40914a6bcc3ed0bd53108320278b8cb339d00593ec5a8c312024aacc78c13f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0f14ef114d78e7b220ad6afb8cbe90e2a5b9ff9493f1e549cb016c29f80c87b7e76bd281332f0244df04f79c7f0a63fc547e3892e00331f952a387fd008ca1db
|
|
7
|
+
data.tar.gz: 55b556be2c0d39472350653331af872f16afd3f34fa1535d252f5cb033fffea60a57542a88dfb6cfd49f13933f77b6c9c7dda3c500878dbe8a0d32ff596fbc13
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johannes Dwi Cahyo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/images/whoosh-banner.png" alt="Whoosh — AI-First Ruby API Framework" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Whoosh</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>AI-first Ruby API framework inspired by FastAPI</strong><br>
|
|
9
|
+
Schema validation, MCP, streaming, background jobs, and OpenAPI docs — out of the box.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://img.shields.io/badge/ruby-%3E%3D%203.4.0-red" alt="Ruby">
|
|
14
|
+
<img src="https://img.shields.io/badge/rack-3.0-blue" alt="Rack">
|
|
15
|
+
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
|
16
|
+
<img src="https://img.shields.io/badge/tests-509%20passing-brightgreen" alt="Tests">
|
|
17
|
+
<img src="https://img.shields.io/badge/overhead-2.5%C2%B5s-orange" alt="Performance">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Why Whoosh?
|
|
23
|
+
|
|
24
|
+
- **AI-first** — MCP server built-in, LLM streaming, token tracking, plugin auto-discovery for 18+ AI gems
|
|
25
|
+
- **Fast** — 2.5µs framework overhead, 406K req/s on simple JSON, YJIT + Oj auto-enabled
|
|
26
|
+
- **Batteries included** — Auth, rate limiting, caching, background jobs, file uploads, pagination, metrics
|
|
27
|
+
- **Zero config to start** — `whoosh new myapp && cd myapp && whoosh s`
|
|
28
|
+
- **OpenAPI 3.1** — Swagger UI + ReDoc auto-generated from your routes and schemas
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
gem install whoosh
|
|
34
|
+
whoosh new my_api
|
|
35
|
+
cd my_api
|
|
36
|
+
whoosh s
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Open http://localhost:9292/docs for Swagger UI.
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# app.rb
|
|
45
|
+
require "whoosh"
|
|
46
|
+
|
|
47
|
+
app = Whoosh::App.new
|
|
48
|
+
|
|
49
|
+
app.get "/health" do
|
|
50
|
+
{ status: "ok", version: Whoosh::VERSION }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
app.post "/chat", request: ChatRequest, mcp: true do |req|
|
|
54
|
+
stream_llm do |out|
|
|
55
|
+
llm.chat(req.body[:message]).each_chunk { |c| out << c }
|
|
56
|
+
out.finish
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
whoosh s # Start server
|
|
63
|
+
whoosh s --reload # Auto-reload on file changes
|
|
64
|
+
whoosh s -p 3000 # Custom port
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
### Routing
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# Inline
|
|
73
|
+
app.get("/users/:id") { |req| { id: req.params[:id] } }
|
|
74
|
+
|
|
75
|
+
# Class-based
|
|
76
|
+
class ChatEndpoint < Whoosh::Endpoint
|
|
77
|
+
post "/chat", request: ChatRequest, mcp: true
|
|
78
|
+
|
|
79
|
+
def call(req)
|
|
80
|
+
{ reply: "Hello!" }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
app.load_endpoints("endpoints/")
|
|
84
|
+
|
|
85
|
+
# Groups with shared middleware
|
|
86
|
+
app.group "/api/v1", mcp: true do
|
|
87
|
+
get("/status") { { ok: true } }
|
|
88
|
+
post("/analyze", auth: :api_key) { |req| analyze(req) }
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Schema Validation
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
class CreateUserRequest < Whoosh::Schema
|
|
96
|
+
field :name, String, required: true, desc: "User name"
|
|
97
|
+
field :email, String, required: true, desc: "Email address"
|
|
98
|
+
field :age, Integer, min: 0, max: 150
|
|
99
|
+
field :role, String, default: "user"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns 422 with field-level errors on invalid input
|
|
103
|
+
app.post "/users", request: CreateUserRequest do |req|
|
|
104
|
+
{ name: req.body[:name], created: true }
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Authentication & Security
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
app.auth do
|
|
112
|
+
api_key header: "X-Api-Key", keys: {
|
|
113
|
+
"sk-prod-123" => { role: :premium },
|
|
114
|
+
"sk-free-456" => { role: :free }
|
|
115
|
+
}
|
|
116
|
+
jwt secret: ENV["JWT_SECRET"], algorithm: :hs256
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
app.rate_limit do
|
|
120
|
+
default limit: 60, period: 60
|
|
121
|
+
rule "/chat", limit: 10, period: 60
|
|
122
|
+
tier :free, limit: 100, period: 3600
|
|
123
|
+
tier :premium, limit: 5000, period: 3600
|
|
124
|
+
on_store_failure :fail_open
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
app.access_control do
|
|
128
|
+
role :free, models: ["claude-haiku"]
|
|
129
|
+
role :premium, models: ["claude-haiku", "claude-sonnet", "claude-opus"]
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### LLM Streaming (OpenAI-compatible)
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
app.post "/chat/stream", auth: :api_key do |req|
|
|
137
|
+
stream_llm do |out|
|
|
138
|
+
# True chunked streaming via SizedQueue — tokens flow in real-time
|
|
139
|
+
out << "Hello "
|
|
140
|
+
out << "World!"
|
|
141
|
+
out.finish # sends data: [DONE]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# SSE events
|
|
146
|
+
app.get "/events" do
|
|
147
|
+
stream :sse do |out|
|
|
148
|
+
out.event("status", { connected: true })
|
|
149
|
+
out << { data: "hello" }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### MCP (Model Context Protocol)
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# Any route with mcp: true becomes an MCP tool automatically
|
|
158
|
+
app.post "/summarize", mcp: true, request: SummarizeRequest do |req|
|
|
159
|
+
{ summary: llm.summarize(req.body[:text]) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Groups propagate mcp: true to all child routes
|
|
163
|
+
app.group "/tools", mcp: true do
|
|
164
|
+
post("/translate") { |req| { result: translate(req.body[:text]) } }
|
|
165
|
+
post("/analyze") { |req| { result: analyze(req.body[:text]) } }
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
whoosh mcp # stdio transport (Claude Desktop, Cursor)
|
|
171
|
+
whoosh mcp --list # list registered MCP tools
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Background Jobs
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class AnalyzeJob < Whoosh::Job
|
|
178
|
+
inject :db, :llm # DI injection
|
|
179
|
+
|
|
180
|
+
def perform(document_id:)
|
|
181
|
+
doc = db[:documents].where(id: document_id).first
|
|
182
|
+
result = llm.complete("Analyze: #{doc[:text]}")
|
|
183
|
+
db[:documents].where(id: document_id).update(analysis: result)
|
|
184
|
+
{ analyzed: true }
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Fire and forget
|
|
189
|
+
app.post "/analyze" do |req|
|
|
190
|
+
job_id = AnalyzeJob.perform_async(document_id: req.body["id"])
|
|
191
|
+
{ job_id: job_id }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Check status
|
|
195
|
+
app.get "/jobs/:id" do |req|
|
|
196
|
+
job = Whoosh::Jobs.find(req.params[:id])
|
|
197
|
+
{ status: job[:status], result: job[:result] }
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
whoosh worker # dedicated worker process
|
|
203
|
+
whoosh worker -c 4 # 4 threads
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### File Upload
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
app.post "/upload" do |req|
|
|
210
|
+
file = req.files["document"]
|
|
211
|
+
|
|
212
|
+
file.filename # => "report.pdf"
|
|
213
|
+
file.content_type # => "application/pdf"
|
|
214
|
+
file.size # => 245760
|
|
215
|
+
file.read_text # => UTF-8 string (for RAG)
|
|
216
|
+
file.to_base64 # => base64 (for vision APIs)
|
|
217
|
+
file.validate!(types: ["application/pdf"], max_size: 10_000_000)
|
|
218
|
+
|
|
219
|
+
path = file.save("documents")
|
|
220
|
+
{ path: path }
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Cache
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
app.get "/users/:id" do |req, cache:|
|
|
228
|
+
cache.fetch("user:#{req.params[:id]}", ttl: 60) do
|
|
229
|
+
db[:users].where(id: req.params[:id]).first
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Pagination
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# Offset-based
|
|
238
|
+
app.get "/users" do |req|
|
|
239
|
+
paginate(db[:users].order(:id),
|
|
240
|
+
page: req.query_params["page"], per_page: 20)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Cursor-based (recommended for large datasets)
|
|
244
|
+
app.get "/messages" do |req|
|
|
245
|
+
paginate_cursor(db[:messages].order(:id),
|
|
246
|
+
cursor: req.query_params["cursor"], limit: 20)
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Plugins (18 AI Gems Auto-Discovered)
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
# Just add gems to Gemfile — they're auto-discovered from Gemfile.lock
|
|
254
|
+
gem "ruby_llm"
|
|
255
|
+
gem "lingua-ruby"
|
|
256
|
+
gem "ner-ruby"
|
|
257
|
+
gem "guardrails-ruby"
|
|
258
|
+
|
|
259
|
+
# Available as bare method calls in endpoints:
|
|
260
|
+
app.post "/analyze" do |req|
|
|
261
|
+
lang = lingua.detect(req.body["text"])
|
|
262
|
+
entities = ner.recognize(req.body["text"])
|
|
263
|
+
{ language: lang, entities: entities }
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### HTTP Client
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
app.post "/proxy" do |req, http:|
|
|
271
|
+
result = http.post("https://api.example.com/analyze",
|
|
272
|
+
json: req.body,
|
|
273
|
+
headers: { "Authorization" => "Bearer #{ENV["API_KEY"]}" },
|
|
274
|
+
timeout: 30
|
|
275
|
+
)
|
|
276
|
+
result.json # parsed response
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Prometheus Metrics
|
|
281
|
+
|
|
282
|
+
Auto-tracked at `/metrics`:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
whoosh_requests_total{method="GET",path="/health",status="200"} 1234
|
|
286
|
+
whoosh_request_duration_seconds_sum{path="/health"} 45.23
|
|
287
|
+
whoosh_request_duration_seconds_count{path="/health"} 1234
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### OpenAPI & Docs
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
app.openapi do
|
|
294
|
+
title "My AI API"
|
|
295
|
+
version "1.0.0"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
app.docs enabled: true, redoc: true
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- `/docs` — Swagger UI
|
|
302
|
+
- `/redoc` — ReDoc
|
|
303
|
+
- `/openapi.json` — Machine-readable spec
|
|
304
|
+
|
|
305
|
+
### Health Checks
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
app.health_check do
|
|
309
|
+
probe(:database) { db.test_connection }
|
|
310
|
+
probe(:cache) { cache.get("ping") || true }
|
|
311
|
+
end
|
|
312
|
+
# GET /healthz → { "status": "ok", "checks": { "database": "ok" } }
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## CLI
|
|
316
|
+
|
|
317
|
+
```sh
|
|
318
|
+
whoosh new my_api # scaffold project (with Dockerfile)
|
|
319
|
+
whoosh s # start server (like rails s)
|
|
320
|
+
whoosh s --reload # hot reload on file changes
|
|
321
|
+
whoosh routes # list all routes
|
|
322
|
+
whoosh console # IRB with app loaded
|
|
323
|
+
whoosh worker # background job worker
|
|
324
|
+
whoosh mcp # MCP stdio server
|
|
325
|
+
|
|
326
|
+
whoosh generate endpoint chat # endpoint + schema + test
|
|
327
|
+
whoosh generate schema User # schema file
|
|
328
|
+
whoosh generate model User name:string email:string
|
|
329
|
+
whoosh generate migration add_email_to_users
|
|
330
|
+
whoosh generate plugin my_tool # plugin boilerplate
|
|
331
|
+
whoosh generate proto ChatRequest # .proto file
|
|
332
|
+
|
|
333
|
+
whoosh db migrate # run migrations
|
|
334
|
+
whoosh db rollback # rollback
|
|
335
|
+
whoosh db status # migration status
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Performance
|
|
339
|
+
|
|
340
|
+
| Benchmark | Result |
|
|
341
|
+
|-----------|--------|
|
|
342
|
+
| Simple JSON endpoint | **406K req/s** |
|
|
343
|
+
| Schema-validated endpoint | **115K req/s** |
|
|
344
|
+
| Router lookup (static) | **6.1M lookups/s** |
|
|
345
|
+
| Framework overhead | **~2.5µs per request** |
|
|
346
|
+
|
|
347
|
+
Optimizations: YJIT auto-enabled, Oj JSON auto-detected (5-10x faster), O(1) static route cache, pre-frozen headers, compiled middleware chain.
|
|
348
|
+
|
|
349
|
+
## Configuration
|
|
350
|
+
|
|
351
|
+
```yaml
|
|
352
|
+
# config/app.yml
|
|
353
|
+
app:
|
|
354
|
+
name: My API
|
|
355
|
+
port: 9292
|
|
356
|
+
|
|
357
|
+
database:
|
|
358
|
+
url: <%= ENV.fetch("DATABASE_URL", "sqlite://db/dev.sqlite3") %>
|
|
359
|
+
max_connections: 10
|
|
360
|
+
|
|
361
|
+
cache:
|
|
362
|
+
store: memory # memory | redis
|
|
363
|
+
default_ttl: 300
|
|
364
|
+
|
|
365
|
+
jobs:
|
|
366
|
+
backend: memory # memory | database | redis
|
|
367
|
+
workers: 2
|
|
368
|
+
|
|
369
|
+
logging:
|
|
370
|
+
level: info
|
|
371
|
+
format: json
|
|
372
|
+
|
|
373
|
+
docs:
|
|
374
|
+
enabled: true
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
`.env` files loaded automatically (dotenv-compatible).
|
|
378
|
+
|
|
379
|
+
## Testing
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
require "whoosh/test"
|
|
383
|
+
|
|
384
|
+
RSpec.describe "My API" do
|
|
385
|
+
include Whoosh::Test
|
|
386
|
+
|
|
387
|
+
def app = MyApp.to_rack
|
|
388
|
+
|
|
389
|
+
it "creates a user" do
|
|
390
|
+
post_json "/users", { name: "Alice", email: "a@b.com" }
|
|
391
|
+
assert_response 200
|
|
392
|
+
assert_json(name: "Alice")
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
it "requires auth" do
|
|
396
|
+
get "/protected"
|
|
397
|
+
assert_response 401
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
it "works with auth" do
|
|
401
|
+
get_with_auth "/protected", key: "sk-test"
|
|
402
|
+
assert_response 200
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## License
|
|
408
|
+
|
|
409
|
+
MIT — see [LICENSE](LICENSE).
|
|
410
|
+
|
|
411
|
+
## Contributing
|
|
412
|
+
|
|
413
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|