whoosh 1.0.1 → 1.0.2
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 +4 -4
- data/README.md +57 -6
- data/lib/whoosh/app.rb +76 -2
- data/lib/whoosh/cli/project_generator.rb +275 -42
- data/lib/whoosh/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d739583e7066d3f04675dbd6eb4e17809226c909f5c7d4d226b7672957e4a88
|
|
4
|
+
data.tar.gz: 5f12610ddf11479be668f3b0dc88de6f6c3f5640620299ca3512f48ffaf14922
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 305d2ea3aac7ba3a22be62c52c996aca50956cce5a0983a8d38cc0f76a55a817c25bdb3fe8877127752a0f3d1e3d81d894c80dc395ae4da39d562d2fad58098f
|
|
7
|
+
data.tar.gz: 2cd02f1ab2374bd1334d6dc6bab05fc460e108081eadb8119c9082af41f522c7fb5a8e7f63781b9451aeb319cd001522b7d6998290c662b3c8dbf0706ac03308
|
data/README.md
CHANGED
|
@@ -337,14 +337,65 @@ whoosh db status # migration status
|
|
|
337
337
|
|
|
338
338
|
## Performance
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
340
|
+
### HTTP Benchmark: `GET /health → {"status":"ok"}`
|
|
341
|
+
|
|
342
|
+
> Apple Silicon arm64, 12 cores. [Full benchmark suite](benchmarks/comparison/)
|
|
343
|
+
|
|
344
|
+
**Single process** (fair 1:1 comparison):
|
|
345
|
+
|
|
346
|
+
| Framework | Language | Server | Req/sec |
|
|
347
|
+
|-----------|----------|--------|---------|
|
|
348
|
+
| Fastify | Node.js 22 | built-in | 69,200 |
|
|
349
|
+
| **Whoosh** | Ruby 3.4 +YJIT | **Falcon** | **24,400** |
|
|
350
|
+
| **Whoosh** | Ruby 3.4 +YJIT | Puma (5 threads) | **15,500** |
|
|
351
|
+
| FastAPI | Python 3.13 | uvicorn | 8,900 |
|
|
352
|
+
| Sinatra | Ruby 3.4 | Puma (5 threads) | 7,100 |
|
|
353
|
+
| PHP (raw) | PHP 8.5 | built-in | 2,000 |
|
|
354
|
+
|
|
355
|
+
> Whoosh + Falcon is **2.7x faster** than FastAPI single-core. Whoosh + Puma is **1.7x faster** than FastAPI. Use Falcon (recommended) for best performance.
|
|
356
|
+
|
|
357
|
+
**Multi-worker** (production deployment):
|
|
358
|
+
|
|
359
|
+
| Framework | Language | Server | Req/sec |
|
|
360
|
+
|-----------|----------|--------|---------|
|
|
361
|
+
| **Whoosh** | Ruby 3.4 +YJIT | **Falcon (4 workers)** | **87,400** |
|
|
362
|
+
| Fastify | Node.js 22 | built-in (single thread) | 69,200 |
|
|
363
|
+
| **Whoosh** | Ruby 3.4 +YJIT | Puma (4w×4t) | **52,500** |
|
|
364
|
+
| Roda | Ruby 3.4 | Puma (4w×4t) | 14,700 |
|
|
365
|
+
|
|
366
|
+
> **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.
|
|
367
|
+
|
|
368
|
+
### Real-World Benchmark: `GET /users/:id` from PostgreSQL (1000 rows)
|
|
369
|
+
|
|
370
|
+
**Single process:**
|
|
371
|
+
|
|
372
|
+
| Framework | Language | Req/sec |
|
|
373
|
+
|-----------|----------|---------|
|
|
374
|
+
| Fastify + pg | Node.js 22 | 36,900 |
|
|
375
|
+
| **Whoosh + Falcon (fiber PG pool)** | Ruby 3.4 +YJIT | **13,400** |
|
|
376
|
+
| **Whoosh + Puma (Sequel)** | Ruby 3.4 +YJIT | **8,600** |
|
|
377
|
+
| Roda + Puma | Ruby 3.4 | 6,700 |
|
|
378
|
+
| Sinatra + Puma | Ruby 3.4 | 4,400 |
|
|
379
|
+
| FastAPI + uvicorn | Python 3.13 | 2,400 |
|
|
380
|
+
|
|
381
|
+
**Multi-worker (PostgreSQL):**
|
|
382
|
+
|
|
383
|
+
| Framework | Language | Req/sec |
|
|
384
|
+
|-----------|----------|---------|
|
|
385
|
+
| **Whoosh + Falcon (4 workers, fiber PG pool)** | Ruby 3.4 +YJIT | **45,900** |
|
|
386
|
+
| Fastify (single thread) | Node.js 22 | 36,900 |
|
|
387
|
+
|
|
388
|
+
> Whoosh + Falcon with fiber-aware PG pool is **5.6x faster** than FastAPI. Multi-worker Falcon **beats Fastify by 24%** on real PostgreSQL workloads.
|
|
389
|
+
|
|
390
|
+
### Micro-benchmarks
|
|
391
|
+
|
|
392
|
+
| Component | Throughput |
|
|
393
|
+
|-----------|-----------|
|
|
394
|
+
| Router lookup (static, cached) | **6.1M ops/s** |
|
|
395
|
+
| JSON encode (Oj) | **5.4M ops/s** |
|
|
345
396
|
| Framework overhead | **~2.5µs per request** |
|
|
346
397
|
|
|
347
|
-
Optimizations: YJIT auto-enabled, Oj JSON auto-detected
|
|
398
|
+
Optimizations: YJIT auto-enabled, Oj JSON auto-detected, O(1) static route cache, compiled middleware chain, pre-frozen headers.
|
|
348
399
|
|
|
349
400
|
## Configuration
|
|
350
401
|
|
data/lib/whoosh/app.rb
CHANGED
|
@@ -276,8 +276,10 @@ module Whoosh
|
|
|
276
276
|
register_doc_routes if @config.docs_enabled?
|
|
277
277
|
register_metrics_route
|
|
278
278
|
@router.freeze!
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
|
|
280
|
+
# Compile the entire middleware + handler into a single lambda
|
|
281
|
+
# This eliminates 4x nested method calls per request
|
|
282
|
+
app = build_compiled_handler
|
|
281
283
|
start_job_workers
|
|
282
284
|
@shutdown.register { @di.close_all }
|
|
283
285
|
@shutdown.register { @mcp_manager.shutdown_all }
|
|
@@ -368,6 +370,78 @@ module Whoosh
|
|
|
368
370
|
@middleware_stack.use(Middleware::RequestLogger, logger: @logger, metrics: @metrics)
|
|
369
371
|
end
|
|
370
372
|
|
|
373
|
+
# Compiled handler inlines all default middleware into a single lambda
|
|
374
|
+
# Eliminates nested method calls — ~3-4x faster than middleware stack
|
|
375
|
+
def build_compiled_handler
|
|
376
|
+
logger = @logger
|
|
377
|
+
metrics = @metrics
|
|
378
|
+
max_bytes = 1_048_576
|
|
379
|
+
security_headers = Middleware::SecurityHeaders::HEADERS
|
|
380
|
+
|
|
381
|
+
-> (env) {
|
|
382
|
+
# 1. RequestLimit — check content length
|
|
383
|
+
content_length = env["CONTENT_LENGTH"]&.to_i || 0
|
|
384
|
+
if content_length > max_bytes
|
|
385
|
+
return [413, { "content-type" => "application/json" },
|
|
386
|
+
[JSON.generate({ error: "request_too_large", max_bytes: max_bytes })]]
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# 2. Request ID (from RequestLogger)
|
|
390
|
+
request_id = env["HTTP_X_REQUEST_ID"] || SecureRandom.uuid
|
|
391
|
+
env["whoosh.request_id"] = request_id
|
|
392
|
+
|
|
393
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
394
|
+
|
|
395
|
+
# 3. CORS preflight
|
|
396
|
+
origin = env["HTTP_ORIGIN"]
|
|
397
|
+
if env["REQUEST_METHOD"] == "OPTIONS" && origin
|
|
398
|
+
cors_headers = {
|
|
399
|
+
"access-control-allow-methods" => "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
400
|
+
"access-control-allow-headers" => "Content-Type, Authorization, X-API-Key, X-Request-ID",
|
|
401
|
+
"access-control-max-age" => "86400",
|
|
402
|
+
"access-control-allow-origin" => "*",
|
|
403
|
+
"access-control-expose-headers" => "X-Request-ID",
|
|
404
|
+
"vary" => "Origin"
|
|
405
|
+
}
|
|
406
|
+
return [204, cors_headers, []]
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# 4. Handle request (core)
|
|
410
|
+
status, headers, body = handle_request(env)
|
|
411
|
+
|
|
412
|
+
# Ensure headers are mutable (streaming returns frozen headers)
|
|
413
|
+
headers = headers.dup if headers.frozen?
|
|
414
|
+
|
|
415
|
+
# 5. Security headers (inline, no allocation)
|
|
416
|
+
security_headers.each { |k, v| headers[k] ||= v }
|
|
417
|
+
|
|
418
|
+
# 6. CORS headers
|
|
419
|
+
if origin
|
|
420
|
+
headers["access-control-allow-origin"] = "*"
|
|
421
|
+
headers["access-control-expose-headers"] = "X-Request-ID"
|
|
422
|
+
headers["vary"] = "Origin"
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# 7. Request ID in response
|
|
426
|
+
headers["x-request-id"] = request_id
|
|
427
|
+
|
|
428
|
+
# 8. Logging + metrics
|
|
429
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
|
|
430
|
+
logger.info("request_complete",
|
|
431
|
+
method: env["REQUEST_METHOD"], path: env["PATH_INFO"],
|
|
432
|
+
status: status, duration_ms: duration_ms, request_id: request_id)
|
|
433
|
+
|
|
434
|
+
if metrics
|
|
435
|
+
metrics.increment("whoosh_requests_total",
|
|
436
|
+
labels: { method: env["REQUEST_METHOD"], path: env["PATH_INFO"], status: status.to_s })
|
|
437
|
+
metrics.observe("whoosh_request_duration_seconds",
|
|
438
|
+
duration_ms / 1000.0, labels: { path: env["PATH_INFO"] })
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
[status, headers, body]
|
|
442
|
+
}
|
|
443
|
+
end
|
|
444
|
+
|
|
371
445
|
def register_mcp_tools
|
|
372
446
|
@router.routes.each do |route|
|
|
373
447
|
next unless route[:metadata] && route[:metadata][:mcp]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "securerandom"
|
|
5
6
|
|
|
6
7
|
module Whoosh
|
|
7
8
|
module CLI
|
|
@@ -13,19 +14,47 @@ module Whoosh
|
|
|
13
14
|
FileUtils.mkdir_p(File.join(dir, d))
|
|
14
15
|
end
|
|
15
16
|
|
|
17
|
+
jwt_secret = SecureRandom.hex(32)
|
|
18
|
+
|
|
16
19
|
write(dir, "app.rb", app_rb(name))
|
|
17
20
|
write(dir, "config.ru", config_ru)
|
|
18
|
-
write(dir, "Gemfile", gemfile(full: full))
|
|
19
|
-
write(dir, "Rakefile",
|
|
21
|
+
write(dir, "Gemfile", gemfile(minimal: minimal, full: full))
|
|
22
|
+
write(dir, "Rakefile", rakefile)
|
|
20
23
|
write(dir, "config/app.yml", app_yml(name))
|
|
21
|
-
write(dir, "config/plugins.yml",
|
|
24
|
+
write(dir, "config/plugins.yml", plugins_yml)
|
|
22
25
|
write(dir, "endpoints/health.rb", health_endpoint)
|
|
23
26
|
write(dir, "schemas/health.rb", health_schema)
|
|
24
27
|
write(dir, "test/test_helper.rb", test_helper)
|
|
25
|
-
write(dir, ".env
|
|
26
|
-
write(dir, "
|
|
27
|
-
write(dir, ".
|
|
28
|
-
|
|
28
|
+
write(dir, ".env", env_file(jwt_secret))
|
|
29
|
+
write(dir, ".env.example", env_example)
|
|
30
|
+
write(dir, ".gitignore", gitignore)
|
|
31
|
+
write(dir, ".rspec", rspec_config)
|
|
32
|
+
write(dir, "Dockerfile", dockerfile)
|
|
33
|
+
write(dir, ".dockerignore", dockerignore)
|
|
34
|
+
write(dir, "README.md", readme(name))
|
|
35
|
+
|
|
36
|
+
# Create empty SQLite DB directory
|
|
37
|
+
FileUtils.mkdir_p(File.join(dir, "db"))
|
|
38
|
+
|
|
39
|
+
# Auto-run bundle install
|
|
40
|
+
puts "Created #{name}/"
|
|
41
|
+
puts ""
|
|
42
|
+
Dir.chdir(dir) do
|
|
43
|
+
puts "Installing dependencies..."
|
|
44
|
+
system("bundle install --quiet")
|
|
45
|
+
end
|
|
46
|
+
puts ""
|
|
47
|
+
puts " #{name}/ is ready!"
|
|
48
|
+
puts ""
|
|
49
|
+
puts " cd #{name}"
|
|
50
|
+
puts " whoosh s # start server at http://localhost:9292"
|
|
51
|
+
puts " whoosh s --reload # start with hot reload"
|
|
52
|
+
puts ""
|
|
53
|
+
puts " http://localhost:9292/health # health check"
|
|
54
|
+
puts " http://localhost:9292/healthz # health probes"
|
|
55
|
+
puts " http://localhost:9292/docs # Swagger UI"
|
|
56
|
+
puts " http://localhost:9292/metrics # Prometheus metrics"
|
|
57
|
+
puts ""
|
|
29
58
|
end
|
|
30
59
|
|
|
31
60
|
class << self
|
|
@@ -41,58 +70,147 @@ module Whoosh
|
|
|
41
70
|
|
|
42
71
|
require "whoosh"
|
|
43
72
|
|
|
73
|
+
# Auto-enable YJIT + Oj for best performance
|
|
74
|
+
Whoosh::Performance.optimize!
|
|
75
|
+
|
|
44
76
|
App = Whoosh::App.new
|
|
45
77
|
|
|
78
|
+
# --- API Documentation ---
|
|
46
79
|
App.openapi do
|
|
47
|
-
title "#{name.capitalize} API"
|
|
80
|
+
title "#{name.gsub(/[-_]/, " ").split.map(&:capitalize).join(" ")} API"
|
|
48
81
|
version "0.1.0"
|
|
49
82
|
end
|
|
50
83
|
|
|
84
|
+
App.docs enabled: true, redoc: true
|
|
85
|
+
|
|
86
|
+
# --- Security ---
|
|
87
|
+
App.auth do
|
|
88
|
+
jwt secret: ENV["JWT_SECRET"], algorithm: :hs256, expiry: 3600
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
App.rate_limit do
|
|
92
|
+
default limit: 60, period: 60
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# --- Health Check ---
|
|
96
|
+
App.health_check do
|
|
97
|
+
probe(:api) { true }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# --- Load Endpoints ---
|
|
51
101
|
App.load_endpoints(File.join(__dir__, "endpoints"))
|
|
52
102
|
RUBY
|
|
53
103
|
end
|
|
54
104
|
|
|
55
105
|
def config_ru
|
|
56
|
-
|
|
106
|
+
<<~RUBY
|
|
107
|
+
# frozen_string_literal: true
|
|
108
|
+
|
|
109
|
+
require_relative "app"
|
|
110
|
+
|
|
111
|
+
run App.to_rack
|
|
112
|
+
RUBY
|
|
57
113
|
end
|
|
58
114
|
|
|
59
|
-
def gemfile(full: false)
|
|
60
|
-
g =
|
|
115
|
+
def gemfile(minimal: false, full: false)
|
|
116
|
+
g = <<~GEM
|
|
117
|
+
source "https://rubygems.org"
|
|
118
|
+
|
|
119
|
+
gem "whoosh"
|
|
120
|
+
|
|
121
|
+
# Server (Falcon recommended for best performance)
|
|
122
|
+
gem "falcon"
|
|
123
|
+
|
|
124
|
+
# Fast JSON (5-10x faster than stdlib)
|
|
125
|
+
gem "oj"
|
|
126
|
+
|
|
127
|
+
# Database
|
|
128
|
+
gem "sequel"
|
|
129
|
+
gem "sqlite3"
|
|
130
|
+
GEM
|
|
131
|
+
|
|
61
132
|
if full
|
|
62
|
-
g +=
|
|
133
|
+
g += <<~GEM
|
|
134
|
+
|
|
135
|
+
# AI & NLP
|
|
136
|
+
gem "ruby_llm"
|
|
137
|
+
gem "lingua-ruby"
|
|
138
|
+
gem "ner-ruby"
|
|
139
|
+
gem "guardrails-ruby"
|
|
140
|
+
GEM
|
|
63
141
|
end
|
|
64
|
-
g += "\ngroup :development, :test do\n gem \"rspec\"\n gem \"rack-test\"\nend\n"
|
|
65
|
-
end
|
|
66
142
|
|
|
67
|
-
|
|
68
|
-
|
|
143
|
+
g += <<~GEM
|
|
144
|
+
|
|
145
|
+
group :development, :test do
|
|
146
|
+
gem "rspec"
|
|
147
|
+
gem "rack-test"
|
|
148
|
+
end
|
|
149
|
+
GEM
|
|
150
|
+
|
|
151
|
+
g
|
|
69
152
|
end
|
|
70
153
|
|
|
71
|
-
def
|
|
72
|
-
|
|
154
|
+
def rakefile
|
|
155
|
+
<<~RUBY
|
|
156
|
+
require "rspec/core/rake_task"
|
|
157
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
158
|
+
task default: :spec
|
|
159
|
+
RUBY
|
|
73
160
|
end
|
|
74
161
|
|
|
75
|
-
def
|
|
162
|
+
def app_yml(name)
|
|
76
163
|
<<~YAML
|
|
77
164
|
app:
|
|
78
|
-
name: "#{name.capitalize} API"
|
|
165
|
+
name: "#{name.gsub(/[-_]/, " ").split.map(&:capitalize).join(" ")} API"
|
|
79
166
|
port: 9292
|
|
80
167
|
host: localhost
|
|
81
168
|
|
|
169
|
+
server:
|
|
170
|
+
type: falcon
|
|
171
|
+
workers: auto
|
|
172
|
+
|
|
82
173
|
database:
|
|
83
174
|
url: <%= ENV.fetch("DATABASE_URL", "sqlite://db/development.sqlite3") %>
|
|
84
175
|
max_connections: 10
|
|
176
|
+
log_level: debug
|
|
85
177
|
|
|
86
178
|
cache:
|
|
87
179
|
store: memory
|
|
88
180
|
default_ttl: 300
|
|
89
181
|
|
|
182
|
+
jobs:
|
|
183
|
+
backend: memory
|
|
184
|
+
workers: 2
|
|
185
|
+
retry: 3
|
|
186
|
+
|
|
90
187
|
logging:
|
|
91
188
|
level: info
|
|
92
189
|
format: json
|
|
93
190
|
|
|
94
191
|
docs:
|
|
95
192
|
enabled: true
|
|
193
|
+
|
|
194
|
+
performance:
|
|
195
|
+
yjit: true
|
|
196
|
+
YAML
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def plugins_yml
|
|
200
|
+
<<~YAML
|
|
201
|
+
# Plugin configuration
|
|
202
|
+
# Gems are auto-discovered from Gemfile.lock
|
|
203
|
+
# Configure or disable here:
|
|
204
|
+
#
|
|
205
|
+
# lingua:
|
|
206
|
+
# languages: [en, id, ms]
|
|
207
|
+
#
|
|
208
|
+
# guardrails:
|
|
209
|
+
# language_check:
|
|
210
|
+
# enabled: true
|
|
211
|
+
#
|
|
212
|
+
# ner:
|
|
213
|
+
# enabled: false
|
|
96
214
|
YAML
|
|
97
215
|
end
|
|
98
216
|
|
|
@@ -111,61 +229,176 @@ module Whoosh
|
|
|
111
229
|
end
|
|
112
230
|
|
|
113
231
|
def health_schema
|
|
114
|
-
|
|
232
|
+
<<~RUBY
|
|
233
|
+
# frozen_string_literal: true
|
|
234
|
+
|
|
235
|
+
class HealthResponse < Whoosh::Schema
|
|
236
|
+
field :status, String, required: true, desc: "Health status"
|
|
237
|
+
field :version, String, desc: "API version"
|
|
238
|
+
end
|
|
239
|
+
RUBY
|
|
115
240
|
end
|
|
116
241
|
|
|
117
242
|
def test_helper
|
|
118
|
-
|
|
243
|
+
<<~RUBY
|
|
244
|
+
# frozen_string_literal: true
|
|
245
|
+
|
|
246
|
+
require "whoosh/test"
|
|
247
|
+
require_relative "../app"
|
|
248
|
+
|
|
249
|
+
RSpec.configure do |config|
|
|
250
|
+
config.include Whoosh::Test
|
|
251
|
+
|
|
252
|
+
def app
|
|
253
|
+
App.to_rack
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
RUBY
|
|
119
257
|
end
|
|
120
258
|
|
|
121
|
-
def
|
|
122
|
-
<<~
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
259
|
+
def env_file(jwt_secret)
|
|
260
|
+
<<~ENV
|
|
261
|
+
# Generated by whoosh new — do NOT commit this file
|
|
262
|
+
JWT_SECRET=#{jwt_secret}
|
|
263
|
+
WHOOSH_ENV=development
|
|
264
|
+
DATABASE_URL=sqlite://db/development.sqlite3
|
|
265
|
+
ENV
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def env_example
|
|
269
|
+
<<~ENV
|
|
270
|
+
# Copy to .env and fill in values
|
|
271
|
+
JWT_SECRET=change_me_to_a_random_64_char_hex
|
|
272
|
+
WHOOSH_ENV=development
|
|
273
|
+
DATABASE_URL=sqlite://db/development.sqlite3
|
|
274
|
+
# REDIS_URL=redis://localhost:6379
|
|
275
|
+
ENV
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def gitignore
|
|
279
|
+
<<~GIT
|
|
280
|
+
# Dependencies
|
|
281
|
+
/vendor/bundle
|
|
282
|
+
/.bundle
|
|
283
|
+
|
|
284
|
+
# Environment
|
|
285
|
+
.env
|
|
286
|
+
.env.local
|
|
287
|
+
.env.production
|
|
288
|
+
|
|
289
|
+
# Database
|
|
290
|
+
db/*.sqlite3
|
|
291
|
+
|
|
292
|
+
# Logs
|
|
293
|
+
/log/*
|
|
294
|
+
/tmp/*
|
|
295
|
+
|
|
296
|
+
# OS
|
|
297
|
+
.DS_Store
|
|
298
|
+
*.swp
|
|
299
|
+
*~
|
|
300
|
+
|
|
301
|
+
# IDE
|
|
302
|
+
.idea/
|
|
303
|
+
.vscode/
|
|
304
|
+
*.code-workspace
|
|
305
|
+
|
|
306
|
+
# Gems
|
|
307
|
+
*.gem
|
|
308
|
+
Gemfile.lock
|
|
309
|
+
GIT
|
|
136
310
|
end
|
|
137
311
|
|
|
138
|
-
def
|
|
312
|
+
def rspec_config
|
|
313
|
+
<<~RSPEC
|
|
314
|
+
--require spec_helper
|
|
315
|
+
--format documentation
|
|
316
|
+
--color
|
|
317
|
+
--order random
|
|
318
|
+
RSPEC
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def dockerfile
|
|
139
322
|
<<~DOCKERFILE
|
|
140
323
|
FROM ruby:3.4-slim
|
|
141
324
|
|
|
142
325
|
WORKDIR /app
|
|
143
326
|
|
|
144
|
-
# Install dependencies
|
|
327
|
+
# Install system dependencies
|
|
328
|
+
RUN apt-get update -qq && apt-get install -y build-essential libsqlite3-dev
|
|
329
|
+
|
|
330
|
+
# Install gems
|
|
145
331
|
COPY Gemfile Gemfile.lock ./
|
|
146
332
|
RUN bundle install --without development test
|
|
147
333
|
|
|
148
334
|
# Copy app
|
|
149
335
|
COPY . .
|
|
150
336
|
|
|
151
|
-
# Expose port
|
|
152
337
|
EXPOSE 9292
|
|
153
338
|
|
|
154
|
-
# Start
|
|
339
|
+
# Start with Falcon for best performance
|
|
155
340
|
CMD ["bundle", "exec", "whoosh", "s", "-p", "9292", "--host", "0.0.0.0"]
|
|
156
341
|
DOCKERFILE
|
|
157
342
|
end
|
|
158
343
|
|
|
159
|
-
def
|
|
344
|
+
def dockerignore
|
|
160
345
|
<<~IGNORE
|
|
161
346
|
.git
|
|
162
347
|
.env
|
|
348
|
+
.env.*
|
|
163
349
|
node_modules
|
|
164
350
|
tmp
|
|
165
351
|
log
|
|
166
352
|
db/*.sqlite3
|
|
353
|
+
*.gem
|
|
354
|
+
.DS_Store
|
|
167
355
|
IGNORE
|
|
168
356
|
end
|
|
357
|
+
|
|
358
|
+
def readme(name)
|
|
359
|
+
title = name.gsub(/[-_]/, " ").split.map(&:capitalize).join(" ")
|
|
360
|
+
<<~MD
|
|
361
|
+
# #{title} API
|
|
362
|
+
|
|
363
|
+
Built with [Whoosh](https://github.com/johannesdwicahyo/whoosh) — AI-first Ruby API framework.
|
|
364
|
+
|
|
365
|
+
## Quick Start
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
whoosh s # http://localhost:9292
|
|
369
|
+
whoosh s --reload # hot reload
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Endpoints
|
|
373
|
+
|
|
374
|
+
| Method | Path | Description |
|
|
375
|
+
|--------|------|-------------|
|
|
376
|
+
| GET | /health | Health check |
|
|
377
|
+
| GET | /healthz | Health probes |
|
|
378
|
+
| GET | /docs | Swagger UI |
|
|
379
|
+
| GET | /redoc | ReDoc |
|
|
380
|
+
| GET | /openapi.json | OpenAPI spec |
|
|
381
|
+
| GET | /metrics | Prometheus metrics |
|
|
382
|
+
|
|
383
|
+
## Development
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
whoosh generate endpoint users # create endpoint + schema + test
|
|
387
|
+
whoosh generate schema CreateUser # create schema
|
|
388
|
+
whoosh generate model User name:string email:string
|
|
389
|
+
whoosh routes # list all routes
|
|
390
|
+
whoosh console # IRB with app loaded
|
|
391
|
+
bundle exec rspec # run tests
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Deploy
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
docker build -t #{name} .
|
|
398
|
+
docker run -p 9292:9292 -e JWT_SECRET=your_secret #{name}
|
|
399
|
+
```
|
|
400
|
+
MD
|
|
401
|
+
end
|
|
169
402
|
end
|
|
170
403
|
end
|
|
171
404
|
end
|
data/lib/whoosh/version.rb
CHANGED