whoosh 1.6.0 → 1.8.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f93acb5a2f85aecd9f3feafc165962aa721a762dad30caa2d2550ed500afbec8
4
- data.tar.gz: e44f3b1217d9177417b21820abd6d1788969556603c2d9aff7f26d7351c703d2
3
+ metadata.gz: 0ff8bb40124a953ebe2ed2524d79fa08961b19aa533efc4430e7f8011f3cfc82
4
+ data.tar.gz: 6dd2e3f3d79fed980cb56352f463fceb0d3f6569556b9f2dc03986d4e1c09d2d
5
5
  SHA512:
6
- metadata.gz: 77dd95d3cefc8053ff9c183207273adc8b99e4df806ad1374b277dfde6a64ffb31f82765c502c05d81eb4d4a9b4f6d681a2ffb0ebaf75353369e5179eeb17758
7
- data.tar.gz: ea988031ce4f4dd47959bd84782caa4ad019e7570f5cd2d17cba754b3851955a4520c7d5ee76815b53da7e381a9fde0a833895a7cc59386e2354dfc6f7604f74
6
+ metadata.gz: e1dc05a5db34f320c44271054c1d1070b2be879018933a6837700fd8a62f60971028f61638e4dac7ed91eba45b21898fe9aa9bbcf35dc42698843e2717f08235
7
+ data.tar.gz: dacb25be02806eff8256280ee0199a01877234646c7229bb1e3d1428b0eb1d895c39ea5599c06ffe6a618d222fc4fb1344c7958876c48f805dd889fd2fb7bd50
data/README.md CHANGED
@@ -5,8 +5,8 @@
5
5
  <h1 align="center">Whoosh</h1>
6
6
 
7
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.
8
+ <strong>The fastest way to ship a production MCP server in Ruby.</strong><br>
9
+ A FastAPI-style framework with MCP, schema validation, auth, streaming, and OpenAPI — built in.
10
10
  </p>
11
11
 
12
12
  <p align="center">
@@ -14,18 +14,29 @@
14
14
  <img src="https://img.shields.io/badge/rack-3.0-blue" alt="Rack">
15
15
  <img src="https://img.shields.io/badge/license-MIT-green" alt="License">
16
16
  <img src="https://img.shields.io/badge/tests-659%20passing-brightgreen" alt="Tests">
17
- <img src="https://img.shields.io/badge/overhead-2.5%C2%B5s-orange" alt="Performance">
17
+ <img src="https://img.shields.io/badge/stability-evolving-yellow" alt="Stability">
18
18
  </p>
19
19
 
20
20
  ---
21
21
 
22
22
  ## Why Whoosh?
23
23
 
24
- - **AI-first** — Every app is an MCP server automatically. LLM wrapper with structured output, caching, and streaming. Vector search built-in. 18+ AI gem auto-discovery.
25
- - **Fast** — 2.5µs framework overhead, 87K req/s with Falcon. Beats Fastify on multi-worker PostgreSQL benchmarks.
26
- - **Batteries included** — Auth, rate limiting, caching, background jobs, file uploads, vector search, pagination, metrics, CI pipeline.
27
- - **Zero config to start** — `whoosh new myapp && cd myapp && whoosh s` everything works.
28
- - **AI-agent friendly** — `whoosh describe` dumps your app as JSON for AI tools. Generated `CLAUDE.md` in every project. `whoosh check` catches mistakes before runtime.
24
+ - **MCP-native** — opt routes into MCP with `mcp: true` and they become typed tools over stdio/SSE. No glue code, no separate server.
25
+ - **FastAPI-style DSL in Ruby** — declarative schemas, typed request/response, auto-generated OpenAPI + Swagger UI, dependency injection.
26
+ - **Batteries included** — auth (JWT, API key, OAuth), rate limiting, caching, background jobs, file uploads, vector search, streaming, pagination.
27
+ - **Agent-friendly** — `whoosh describe` emits a JSON snapshot of your app; generated `CLAUDE.md` so coding agents understand it; `whoosh check` validates config before runtime.
28
+ - **Competitive Ruby performance** — YJIT + Falcon fibers + Oj, ~2.5µs framework overhead. See [Performance](#performance) for honest, per-core comparisons.
29
+
30
+ ## When NOT to use Whoosh
31
+
32
+ Whoosh is on 1.x but still evolving — solo-maintained, without a production track record yet, and breaking changes ship occasionally (always called out in `CHANGELOG.md`). Reach for something else when:
33
+
34
+ - **You need a managed backend.** Supabase, PocketBase, or Firebase give you DB + auth + realtime without hosting a framework. Whoosh is the app layer — use it *with* a managed DB if that fits.
35
+ - **You want maximum ecosystem depth.** Rails has more gems; FastAPI has the Python ML/AI library ecosystem (PyTorch, transformers, LangChain). If your core workload lives in those libraries, stay where they are.
36
+ - **You need a frozen API surface.** Being on 1.x doesn't mean the API is locked — breaking changes still ship when the design calls for it. If you need strict stability contracts today, wait a few releases.
37
+ - **Your team has no Ruby experience** and the project isn't specifically about AI/MCP. Hiring and ecosystem gravity usually beat framework features.
38
+
39
+ Whoosh's sweet spot: Ruby shops (or Ruby-curious teams) building AI / LLM / MCP-backed APIs who want typed schemas, OpenAPI, and MCP without wiring three libraries together.
29
40
 
30
41
  ## Install
31
42
 
@@ -192,21 +203,24 @@ end
192
203
 
193
204
  ### MCP (Model Context Protocol)
194
205
 
195
- **Every route is automatically an MCP tool.** No `mcp: true` needed.
206
+ **Routes are exposed as MCP tools only when you opt in with `mcp: true`.** This prevents internal or admin endpoints from being callable as tools by accident.
196
207
 
197
208
  ```ruby
198
- # These are all MCP tools automatically:
199
- app.post "/summarize", request: SummarizeRequest do |req|
209
+ # Opt in per route:
210
+ app.post "/summarize", request: SummarizeRequest, mcp: true do |req|
200
211
  { summary: llm.summarize(req.body[:text]) }
201
212
  end
202
213
 
203
- app.post "/translate" do |req|
204
- { result: translate(req.body["text"]) }
214
+ # Or opt in a whole group:
215
+ app.group "/tools", mcp: true do
216
+ post "/translate" do |req|
217
+ { result: translate(req.body["text"]) }
218
+ end
205
219
  end
206
220
 
207
- # Opt OUT with mcp: false for internal routes:
208
- app.get "/internal", mcp: false do
209
- { debug: "not exposed as MCP tool" }
221
+ # Default: not exposed as an MCP tool.
222
+ app.get "/internal" do
223
+ { debug: "not exposed" }
210
224
  end
211
225
  ```
212
226
 
@@ -440,55 +454,56 @@ whoosh check # validates config, auth, dependencies
440
454
 
441
455
  ## Performance
442
456
 
443
- ### HTTP Benchmark: `GET /health {"status":"ok"}`
457
+ > Apple Silicon arm64, 12 cores. Ruby 3.4 + YJIT. [Full benchmark suite & reproduction steps](benchmarks/comparison/)
458
+ >
459
+ > **How to read these numbers.** Benchmarks are selective by nature. A `GET /health` returning `{"status":"ok"}` tests the router + serializer, not your real app. A Postgres read tests one query pattern. We show single-process (per-core) numbers first because that's the fair cross-language comparison. Multi-worker numbers are included for deployment sizing, but scaling strategies differ per runtime (Node uses `cluster`, Python uses multiple workers, Ruby uses workers × threads or fibers) and mixing them isn't apples-to-apples.
444
460
 
445
- > Apple Silicon arm64, 12 cores. [Full benchmark suite](benchmarks/comparison/)
461
+ ### HTTP micro-benchmark `GET /health`
446
462
 
447
- **Single process** (fair 1:1 comparison):
463
+ **Single process (per-core, fair comparison):**
448
464
 
449
465
  | Framework | Language | Server | Req/sec |
450
466
  |-----------|----------|--------|---------|
451
467
  | Fastify | Node.js 22 | built-in | 69,200 |
452
- | **Whoosh** | Ruby 3.4 +YJIT | **Falcon** | **24,400** |
468
+ | **Whoosh** | Ruby 3.4 +YJIT | Falcon | **24,400** |
453
469
  | **Whoosh** | Ruby 3.4 +YJIT | Puma (5 threads) | **15,500** |
454
470
  | FastAPI | Python 3.13 | uvicorn | 8,900 |
455
471
  | Sinatra | Ruby 3.4 | Puma (5 threads) | 7,100 |
456
- | PHP (raw) | PHP 8.5 | built-in | 2,000 |
457
472
 
458
- > Whoosh + Falcon is **2.7x faster** than FastAPI single-core. Whoosh + Puma is **1.7x faster** than FastAPI. Use Falcon (recommended) for best performance.
473
+ On this microbenchmark, Fastify is ~2. Whoosh+Falcon per-core; that's the honest picture for trivial JSON. Against other Ruby frameworks and against FastAPI on CPython, Whoosh is competitive.
459
474
 
460
- **Multi-worker** (production deployment):
475
+ **Multi-worker (sizing reference, not apples-to-apples):**
461
476
 
462
- | Framework | Language | Server | Req/sec |
463
- |-----------|----------|--------|---------|
464
- | **Whoosh** | Ruby 3.4 +YJIT | **Falcon (4 workers)** | **87,400** |
465
- | Fastify | Node.js 22 | built-in (single thread) | 69,200 |
466
- | **Whoosh** | Ruby 3.4 +YJIT | Puma (4w×4t) | **52,500** |
467
- | Roda | Ruby 3.4 | Puma (4w×4t) | 14,700 |
477
+ | Framework | Server | Req/sec |
478
+ |-----------|--------|---------|
479
+ | Whoosh | Falcon (4 workers) | 87,400 |
480
+ | Fastify | built-in (single thread, no cluster) | 69,200 |
481
+ | Whoosh | Puma (4w × 4t) | 52,500 |
482
+ | Roda | Puma (4w × 4t) | 14,700 |
468
483
 
469
- > **Note:** Fastify is single-threaded by design (Node.js event loop). It can scale via `cluster` module but was not tested in that mode. Whoosh + Falcon with 4 workers uses 4 cores.
484
+ Fastify was not run under `cluster`; don't read this table as "Whoosh beats Fastify." Read it as "Whoosh on 4 cores handles ~87K req/s on trivial JSON."
470
485
 
471
- ### Real-World Benchmark: `GET /users/:id` from PostgreSQL (1000 rows)
486
+ ### Real-world benchmark `GET /users/:id` from PostgreSQL (1000-row table)
472
487
 
473
- **Single process:**
488
+ **Single process (per-core):**
474
489
 
475
- | Framework | Language | Req/sec |
476
- |-----------|----------|---------|
477
- | Fastify + pg | Node.js 22 | 36,900 |
478
- | **Whoosh + Falcon (fiber PG pool)** | Ruby 3.4 +YJIT | **13,400** |
479
- | **Whoosh + Puma (Sequel)** | Ruby 3.4 +YJIT | **8,600** |
480
- | Roda + Puma | Ruby 3.4 | 6,700 |
481
- | Sinatra + Puma | Ruby 3.4 | 4,400 |
482
- | FastAPI + uvicorn | Python 3.13 | 2,400 |
490
+ | Framework | Req/sec |
491
+ |-----------|---------|
492
+ | Fastify + pg | 36,900 |
493
+ | **Whoosh + Falcon (fiber PG pool)** | **13,400** |
494
+ | Whoosh + Puma (Sequel) | 8,600 |
495
+ | Roda + Puma | 6,700 |
496
+ | Sinatra + Puma | 4,400 |
497
+ | FastAPI + uvicorn | 2,400 |
483
498
 
484
- **Multi-worker (PostgreSQL):**
499
+ On realistic DB-bound work, Whoosh's fiber-aware PG pool closes a lot of the gap vs Fastify (~2.75×) and has a wide lead over FastAPI on CPython and over other Ruby frameworks.
485
500
 
486
- | Framework | Language | Req/sec |
487
- |-----------|----------|---------|
488
- | **Whoosh + Falcon (4 workers, fiber PG pool)** | Ruby 3.4 +YJIT | **45,900** |
489
- | Fastify (single thread) | Node.js 22 | 36,900 |
501
+ **Multi-worker (sizing reference):**
490
502
 
491
- > Whoosh + Falcon with fiber-aware PG pool is **5.6x faster** than FastAPI. Multi-worker Falcon **beats Fastify by 24%** on real PostgreSQL workloads.
503
+ | Framework | Req/sec |
504
+ |-----------|---------|
505
+ | Whoosh + Falcon (4 workers, fiber PG pool) | 45,900 |
506
+ | Fastify (single thread) | 36,900 |
492
507
 
493
508
  ### Micro-benchmarks
494
509
 
data/lib/whoosh/app.rb CHANGED
@@ -493,8 +493,9 @@ module Whoosh
493
493
  internal_paths = %w[/openapi.json /docs /redoc /metrics /healthz]
494
494
 
495
495
  @router.routes.each do |route|
496
- # Auto-expose all routes as MCP tools (opt-out with mcp: false)
497
- next if route[:metadata] && route[:metadata][:mcp] == false
496
+ # Routes are exposed as MCP tools only when opted in with mcp: true.
497
+ # Why: default-expose leaks internal/admin endpoints as callable tools.
498
+ next unless route[:metadata] && route[:metadata][:mcp] == true
498
499
  next if internal_paths.include?(route[:path])
499
500
 
500
501
  tool_name = "#{route[:method]} #{route[:path]}"
@@ -621,9 +622,16 @@ module Whoosh
621
622
  # Call handler
622
623
  if handler[:endpoint_class]
623
624
  # Class-based endpoint
624
- endpoint = handler[:endpoint_class].new
625
- context = Endpoint::Context.new(self, request)
626
- result = endpoint.call(context.request)
625
+ endpoint_class = handler[:endpoint_class]
626
+ endpoint = endpoint_class.new
627
+ if endpoint_class.respond_to?(:dependencies)
628
+ endpoint_class.dependencies.each do |dep|
629
+ value = @di.resolve(dep, request: request)
630
+ endpoint.instance_variable_set(:"@#{dep}", value)
631
+ endpoint.define_singleton_method(dep) { instance_variable_get(:"@#{dep}") }
632
+ end
633
+ end
634
+ result = endpoint.call(request)
627
635
  else
628
636
  # Inline block endpoint
629
637
  block = handler[:block]
@@ -35,6 +35,14 @@ module Whoosh
35
35
  @declared_routes
36
36
  end
37
37
 
38
+ def inject(*names)
39
+ @dependencies = names
40
+ end
41
+
42
+ def dependencies
43
+ @dependencies || []
44
+ end
45
+
38
46
  def get(path, **opts)
39
47
  declare_route("GET", path, **opts)
40
48
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Whoosh
4
- VERSION = "1.6.0"
4
+ VERSION = "1.8.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whoosh
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johannes Dwi Cahyo