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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +413 -0
  4. data/exe/whoosh +6 -0
  5. data/lib/whoosh/app.rb +655 -0
  6. data/lib/whoosh/auth/access_control.rb +26 -0
  7. data/lib/whoosh/auth/api_key.rb +30 -0
  8. data/lib/whoosh/auth/jwt.rb +88 -0
  9. data/lib/whoosh/auth/oauth2.rb +33 -0
  10. data/lib/whoosh/auth/rate_limiter.rb +86 -0
  11. data/lib/whoosh/auth/token_tracker.rb +40 -0
  12. data/lib/whoosh/cache/memory_store.rb +57 -0
  13. data/lib/whoosh/cache/redis_store.rb +72 -0
  14. data/lib/whoosh/cache.rb +26 -0
  15. data/lib/whoosh/cli/generators.rb +133 -0
  16. data/lib/whoosh/cli/main.rb +277 -0
  17. data/lib/whoosh/cli/project_generator.rb +172 -0
  18. data/lib/whoosh/config.rb +160 -0
  19. data/lib/whoosh/database.rb +47 -0
  20. data/lib/whoosh/dependency_injection.rb +103 -0
  21. data/lib/whoosh/endpoint.rb +79 -0
  22. data/lib/whoosh/env_loader.rb +46 -0
  23. data/lib/whoosh/errors.rb +68 -0
  24. data/lib/whoosh/http/response.rb +26 -0
  25. data/lib/whoosh/http.rb +73 -0
  26. data/lib/whoosh/instrumentation.rb +22 -0
  27. data/lib/whoosh/job.rb +24 -0
  28. data/lib/whoosh/jobs/memory_backend.rb +45 -0
  29. data/lib/whoosh/jobs/worker.rb +73 -0
  30. data/lib/whoosh/jobs.rb +50 -0
  31. data/lib/whoosh/logger.rb +62 -0
  32. data/lib/whoosh/mcp/client.rb +71 -0
  33. data/lib/whoosh/mcp/client_manager.rb +73 -0
  34. data/lib/whoosh/mcp/protocol.rb +39 -0
  35. data/lib/whoosh/mcp/server.rb +66 -0
  36. data/lib/whoosh/mcp/transport/sse.rb +26 -0
  37. data/lib/whoosh/mcp/transport/stdio.rb +33 -0
  38. data/lib/whoosh/metrics.rb +84 -0
  39. data/lib/whoosh/middleware/cors.rb +61 -0
  40. data/lib/whoosh/middleware/plugin_hooks.rb +27 -0
  41. data/lib/whoosh/middleware/request_limit.rb +28 -0
  42. data/lib/whoosh/middleware/request_logger.rb +39 -0
  43. data/lib/whoosh/middleware/security_headers.rb +28 -0
  44. data/lib/whoosh/middleware/stack.rb +25 -0
  45. data/lib/whoosh/openapi/generator.rb +50 -0
  46. data/lib/whoosh/openapi/schema_converter.rb +48 -0
  47. data/lib/whoosh/openapi/ui.rb +62 -0
  48. data/lib/whoosh/paginate.rb +64 -0
  49. data/lib/whoosh/performance.rb +20 -0
  50. data/lib/whoosh/plugins/base.rb +42 -0
  51. data/lib/whoosh/plugins/registry.rb +139 -0
  52. data/lib/whoosh/request.rb +93 -0
  53. data/lib/whoosh/response.rb +39 -0
  54. data/lib/whoosh/router.rb +112 -0
  55. data/lib/whoosh/schema.rb +194 -0
  56. data/lib/whoosh/serialization/json.rb +73 -0
  57. data/lib/whoosh/serialization/msgpack.rb +51 -0
  58. data/lib/whoosh/serialization/negotiator.rb +37 -0
  59. data/lib/whoosh/serialization/protobuf.rb +43 -0
  60. data/lib/whoosh/shutdown.rb +30 -0
  61. data/lib/whoosh/storage/local.rb +24 -0
  62. data/lib/whoosh/storage/s3.rb +31 -0
  63. data/lib/whoosh/storage.rb +20 -0
  64. data/lib/whoosh/streaming/llm_stream.rb +51 -0
  65. data/lib/whoosh/streaming/sse.rb +61 -0
  66. data/lib/whoosh/streaming/stream_body.rb +59 -0
  67. data/lib/whoosh/streaming/websocket.rb +51 -0
  68. data/lib/whoosh/test.rb +70 -0
  69. data/lib/whoosh/types.rb +11 -0
  70. data/lib/whoosh/uploaded_file.rb +47 -0
  71. data/lib/whoosh/version.rb +5 -0
  72. data/lib/whoosh.rb +86 -0
  73. 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).
data/exe/whoosh ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "whoosh/cli/main"
5
+
6
+ Whoosh::CLI::Main.start(ARGV)