funapi 0.1.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 +7 -0
- data/.claude/25-09-01-OPENAPI_IMPLEMENTATION.md +233 -0
- data/.claude/25-09-05-RESPONSE_SCHEMA.md +383 -0
- data/.claude/25-09-10-OPENAPI_PLAN.md +219 -0
- data/.claude/25-10-26-MIDDLEWARE_IMPLEMENTATION.md +230 -0
- data/.claude/25-10-26-MIDDLEWARE_PLAN.md +353 -0
- data/.claude/25-10-27-BACKGROUND_TASKS_ANALYSIS.md +325 -0
- data/.claude/25-10-27-DEPENDENCY_IMPLEMENTATION_SUMMARY.md +325 -0
- data/.claude/25-10-27-DEPENDENCY_INJECTION_PLAN.md +753 -0
- data/.claude/25-12-24-LIFECYCLE_HOOKS_PLAN.md +421 -0
- data/.claude/25-12-24-PUBLISHING_AND_DOGFOODING_PLAN.md +327 -0
- data/.claude/25-12-24-TEMPLATE_RENDERING_PLAN.md +704 -0
- data/.claude/DECISIONS.md +397 -0
- data/.claude/PROJECT_PLAN.md +80 -0
- data/.claude/TESTING_PLAN.md +285 -0
- data/.claude/TESTING_STATUS.md +157 -0
- data/.tool-versions +1 -0
- data/AGENTS.md +416 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +660 -0
- data/Rakefile +10 -0
- data/docs +8 -0
- data/docs-site/.gitignore +3 -0
- data/docs-site/Gemfile +9 -0
- data/docs-site/app.rb +138 -0
- data/docs-site/content/essential/handler.md +156 -0
- data/docs-site/content/essential/lifecycle.md +161 -0
- data/docs-site/content/essential/middleware.md +201 -0
- data/docs-site/content/essential/openapi.md +155 -0
- data/docs-site/content/essential/routing.md +123 -0
- data/docs-site/content/essential/validation.md +166 -0
- data/docs-site/content/getting-started/at-glance.md +82 -0
- data/docs-site/content/getting-started/key-concepts.md +150 -0
- data/docs-site/content/getting-started/quick-start.md +127 -0
- data/docs-site/content/index.md +81 -0
- data/docs-site/content/patterns/async-operations.md +137 -0
- data/docs-site/content/patterns/background-tasks.md +143 -0
- data/docs-site/content/patterns/database.md +175 -0
- data/docs-site/content/patterns/dependencies.md +141 -0
- data/docs-site/content/patterns/deployment.md +212 -0
- data/docs-site/content/patterns/error-handling.md +184 -0
- data/docs-site/content/patterns/response-schema.md +159 -0
- data/docs-site/content/patterns/templates.md +193 -0
- data/docs-site/content/patterns/testing.md +218 -0
- data/docs-site/mise.toml +2 -0
- data/docs-site/public/css/style.css +234 -0
- data/docs-site/templates/layouts/docs.html.erb +28 -0
- data/docs-site/templates/page.html.erb +3 -0
- data/docs-site/templates/partials/_nav.html.erb +19 -0
- data/examples/background_tasks_demo.rb +159 -0
- data/examples/demo_middleware.rb +55 -0
- data/examples/demo_openapi.rb +63 -0
- data/examples/dependency_block_demo.rb +150 -0
- data/examples/dependency_cleanup_demo.rb +146 -0
- data/examples/dependency_injection_demo.rb +200 -0
- data/examples/lifecycle_demo.rb +57 -0
- data/examples/middleware_demo.rb +74 -0
- data/examples/templates/layouts/application.html.erb +66 -0
- data/examples/templates/todos/_todo.html.erb +15 -0
- data/examples/templates/todos/index.html.erb +12 -0
- data/examples/templates_demo.rb +87 -0
- data/lib/funapi/application.rb +521 -0
- data/lib/funapi/async.rb +57 -0
- data/lib/funapi/background_tasks.rb +52 -0
- data/lib/funapi/config.rb +23 -0
- data/lib/funapi/database/sequel/fibered_connection_pool.rb +87 -0
- data/lib/funapi/dependency_wrapper.rb +66 -0
- data/lib/funapi/depends.rb +138 -0
- data/lib/funapi/exceptions.rb +72 -0
- data/lib/funapi/middleware/base.rb +13 -0
- data/lib/funapi/middleware/cors.rb +23 -0
- data/lib/funapi/middleware/request_logger.rb +32 -0
- data/lib/funapi/middleware/trusted_host.rb +34 -0
- data/lib/funapi/middleware.rb +4 -0
- data/lib/funapi/openapi/schema_converter.rb +85 -0
- data/lib/funapi/openapi/spec_generator.rb +179 -0
- data/lib/funapi/router.rb +43 -0
- data/lib/funapi/schema.rb +65 -0
- data/lib/funapi/server/falcon.rb +38 -0
- data/lib/funapi/template_response.rb +17 -0
- data/lib/funapi/templates.rb +111 -0
- data/lib/funapi/version.rb +5 -0
- data/lib/funapi.rb +14 -0
- data/sig/fun_api.rbs +499 -0
- metadata +220 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# Middleware Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement a comprehensive middleware system for FunApi that leverages the battle-tested Rack middleware ecosystem while providing FastAPI-like convenience methods.
|
|
6
|
+
|
|
7
|
+
## Architecture Decision: Hybrid Approach
|
|
8
|
+
|
|
9
|
+
Support both standard Rack middleware AND FunApi-specific convenience wrappers:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# 1. Standard Rack middleware (existing ecosystem)
|
|
13
|
+
app = FunApi::App.new do |api|
|
|
14
|
+
api.use Rack::Cors do |config|
|
|
15
|
+
config.allow do |allow|
|
|
16
|
+
allow.origins '*'
|
|
17
|
+
allow.resource '*', headers: :any, methods: [:get, :post]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# 2. FunApi-specific middleware (FastAPI-style convenience)
|
|
23
|
+
app = FunApi::App.new do |api|
|
|
24
|
+
api.add_cors(
|
|
25
|
+
allow_origins: ['*'],
|
|
26
|
+
allow_methods: ['*'],
|
|
27
|
+
allow_headers: ['*']
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Or custom FunApi middleware with async support
|
|
31
|
+
api.use FunApi::Middleware::RateLimit, max_requests: 100, window: 60
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Implementation Phases
|
|
36
|
+
|
|
37
|
+
### Phase 1: Core Middleware System ⭐⭐⭐⭐⭐
|
|
38
|
+
|
|
39
|
+
**File**: `lib/fun_api/application.rb`
|
|
40
|
+
|
|
41
|
+
**Changes**:
|
|
42
|
+
1. Uncomment and fix `build_middleware_chain` method (lines 144-152)
|
|
43
|
+
2. Add `use(middleware, *args, &block)` method
|
|
44
|
+
3. Update `call(env)` to use middleware chain instead of router directly
|
|
45
|
+
4. Ensure middleware is applied in correct order (LIFO - Last In, First Out)
|
|
46
|
+
|
|
47
|
+
**Why First**: This unblocks everything else and enables the entire Rack ecosystem.
|
|
48
|
+
|
|
49
|
+
**Implementation**:
|
|
50
|
+
```ruby
|
|
51
|
+
class App
|
|
52
|
+
def use(middleware, *args, &block)
|
|
53
|
+
@middleware_stack << [middleware, args, block]
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def call(env)
|
|
58
|
+
app = build_middleware_chain
|
|
59
|
+
app.call(env)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def build_middleware_chain
|
|
65
|
+
# Start with router as the innermost app
|
|
66
|
+
app = @router
|
|
67
|
+
|
|
68
|
+
# Wrap with middleware in reverse order (LIFO)
|
|
69
|
+
@middleware_stack.reverse_each do |middleware, args, block|
|
|
70
|
+
app = middleware.new(app, *args, &block)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
app
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Phase 2: Built-in Middleware ⭐⭐⭐⭐
|
|
79
|
+
|
|
80
|
+
Create FunApi-specific middleware with async awareness and FastAPI-style convenience.
|
|
81
|
+
|
|
82
|
+
#### A. Base Middleware Class
|
|
83
|
+
**File**: `lib/fun_api/middleware/base.rb`
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
module FunApi
|
|
87
|
+
module Middleware
|
|
88
|
+
class Base
|
|
89
|
+
def initialize(app)
|
|
90
|
+
@app = app
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def call(env)
|
|
94
|
+
@app.call(env)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### B. CORS Middleware (Delegate to rack-cors)
|
|
102
|
+
**File**: `lib/fun_api/middleware/cors.rb`
|
|
103
|
+
|
|
104
|
+
Wrapper around the battle-tested `rack-cors` gem.
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
module FunApi
|
|
108
|
+
module Middleware
|
|
109
|
+
class Cors
|
|
110
|
+
def self.new(app, allow_origins: ['*'], allow_methods: ['*'],
|
|
111
|
+
allow_headers: ['*'], expose_headers: [],
|
|
112
|
+
max_age: 600, allow_credentials: false)
|
|
113
|
+
require 'rack/cors'
|
|
114
|
+
|
|
115
|
+
Rack::Cors.new(app) do |config|
|
|
116
|
+
config.allow do |allow|
|
|
117
|
+
allow.origins(*allow_origins)
|
|
118
|
+
allow.resource '*',
|
|
119
|
+
methods: allow_methods,
|
|
120
|
+
headers: allow_headers,
|
|
121
|
+
expose: expose_headers,
|
|
122
|
+
max_age: max_age,
|
|
123
|
+
credentials: allow_credentials
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class App
|
|
131
|
+
def add_cors(**options)
|
|
132
|
+
use FunApi::Middleware::Cors, **options
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Dependencies**: Add `rack-cors` to gemspec
|
|
139
|
+
|
|
140
|
+
#### C. Trusted Host Middleware
|
|
141
|
+
**File**: `lib/fun_api/middleware/trusted_host.rb`
|
|
142
|
+
|
|
143
|
+
Validates the `Host` header to prevent host header attacks.
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
module FunApi
|
|
147
|
+
module Middleware
|
|
148
|
+
class TrustedHost < Base
|
|
149
|
+
def initialize(app, allowed_hosts: [])
|
|
150
|
+
super(app)
|
|
151
|
+
@allowed_hosts = Array(allowed_hosts)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def call(env)
|
|
155
|
+
host = env['HTTP_HOST']&.split(':')&.first
|
|
156
|
+
|
|
157
|
+
unless host_allowed?(host)
|
|
158
|
+
return [
|
|
159
|
+
400,
|
|
160
|
+
{'content-type' => 'application/json'},
|
|
161
|
+
[JSON.dump(detail: 'Invalid host header')]
|
|
162
|
+
]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
@app.call(env)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def host_allowed?(host)
|
|
171
|
+
return true if @allowed_hosts.empty?
|
|
172
|
+
@allowed_hosts.any? { |pattern|
|
|
173
|
+
pattern.is_a?(Regexp) ? pattern.match?(host) : pattern == host
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
class App
|
|
180
|
+
def add_trusted_host(allowed_hosts:)
|
|
181
|
+
use FunApi::Middleware::TrustedHost, allowed_hosts: allowed_hosts
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### D. Request Logger Middleware
|
|
188
|
+
**File**: `lib/fun_api/middleware/request_logger.rb`
|
|
189
|
+
|
|
190
|
+
Logs incoming requests with timing information.
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
module FunApi
|
|
194
|
+
module Middleware
|
|
195
|
+
class RequestLogger < Base
|
|
196
|
+
def initialize(app, logger: nil, level: :info)
|
|
197
|
+
super(app)
|
|
198
|
+
@logger = logger || Logger.new($stdout)
|
|
199
|
+
@level = level
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def call(env)
|
|
203
|
+
start = Time.now
|
|
204
|
+
status, headers, body = @app.call(env)
|
|
205
|
+
duration = Time.now - start
|
|
206
|
+
|
|
207
|
+
log_request(env, status, duration)
|
|
208
|
+
|
|
209
|
+
[status, headers, body]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
def log_request(env, status, duration)
|
|
215
|
+
request = Rack::Request.new(env)
|
|
216
|
+
@logger.send(@level,
|
|
217
|
+
"#{request.request_method} #{request.path} " \
|
|
218
|
+
"#{status} #{(duration * 1000).round(2)}ms"
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
class App
|
|
225
|
+
def add_request_logger(logger: nil, level: :info)
|
|
226
|
+
use FunApi::Middleware::RequestLogger, logger: logger, level: level
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### E. Gzip Compression (Delegate to Rack::Deflater)
|
|
233
|
+
**File**: Convenience method only
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
class App
|
|
237
|
+
def add_gzip
|
|
238
|
+
use Rack::Deflater, if: ->(env, status, headers, body) {
|
|
239
|
+
headers['content-type']&.start_with?('application/json')
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Phase 3: Documentation & Examples ⭐⭐⭐
|
|
246
|
+
|
|
247
|
+
#### Update README.md
|
|
248
|
+
Add middleware section with examples:
|
|
249
|
+
- Basic middleware usage
|
|
250
|
+
- Built-in middleware
|
|
251
|
+
- Compatible Rack middleware ecosystem
|
|
252
|
+
- Custom middleware creation
|
|
253
|
+
|
|
254
|
+
#### Create Example App
|
|
255
|
+
**File**: `examples/middleware_demo.rb`
|
|
256
|
+
|
|
257
|
+
Demonstrate all middleware features.
|
|
258
|
+
|
|
259
|
+
### Phase 4: Testing ⭐⭐⭐⭐
|
|
260
|
+
|
|
261
|
+
**File**: `test/test_middleware.rb`
|
|
262
|
+
|
|
263
|
+
Test:
|
|
264
|
+
- Middleware ordering (LIFO)
|
|
265
|
+
- Built-in middleware functionality
|
|
266
|
+
- Async compatibility
|
|
267
|
+
- Integration with Rack middleware
|
|
268
|
+
- Edge cases (no middleware, single middleware, multiple middleware)
|
|
269
|
+
|
|
270
|
+
## Benefits
|
|
271
|
+
|
|
272
|
+
1. ✅ **Leverage Ruby ecosystem** - All existing Rack middleware works out of the box
|
|
273
|
+
2. ✅ **Battle-tested** - Rack middleware pattern is 15+ years proven
|
|
274
|
+
3. ✅ **FastAPI-like DX** - Convenience methods (`add_cors`, `add_gzip`) for common needs
|
|
275
|
+
4. ✅ **Flexibility** - Support both Rack standard and custom middleware
|
|
276
|
+
5. ✅ **Async-compatible** - Rack 3.0+ supports async natively
|
|
277
|
+
6. ✅ **Zero reinvention** - Delegate to proven gems (rack-cors, rack-deflater)
|
|
278
|
+
|
|
279
|
+
## Compatible Rack Middleware (to document)
|
|
280
|
+
|
|
281
|
+
Popular Rack middleware that works immediately:
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# Rate limiting
|
|
285
|
+
gem 'rack-attack'
|
|
286
|
+
app.use Rack::Attack
|
|
287
|
+
|
|
288
|
+
# Authentication
|
|
289
|
+
gem 'warden'
|
|
290
|
+
app.use Warden::Manager
|
|
291
|
+
|
|
292
|
+
# Caching
|
|
293
|
+
app.use Rack::Cache
|
|
294
|
+
|
|
295
|
+
# ETags
|
|
296
|
+
app.use Rack::ETag
|
|
297
|
+
|
|
298
|
+
# Conditional GET
|
|
299
|
+
app.use Rack::ConditionalGet
|
|
300
|
+
|
|
301
|
+
# Static files
|
|
302
|
+
app.use Rack::Static, urls: ['/public']
|
|
303
|
+
|
|
304
|
+
# Session
|
|
305
|
+
app.use Rack::Session::Cookie, secret: 'your_secret'
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Implementation Checklist
|
|
309
|
+
|
|
310
|
+
### Phase 1: Core System
|
|
311
|
+
- [ ] Add `use` method to `App` class
|
|
312
|
+
- [ ] Implement `build_middleware_chain` method
|
|
313
|
+
- [ ] Update `call` method to use middleware chain
|
|
314
|
+
- [ ] Test with existing Rack middleware (Rack::Cors)
|
|
315
|
+
- [ ] Ensure middleware ordering is correct (LIFO)
|
|
316
|
+
|
|
317
|
+
### Phase 2: Built-in Middleware
|
|
318
|
+
- [ ] Create `lib/fun_api/middleware/base.rb`
|
|
319
|
+
- [ ] Create `lib/fun_api/middleware/cors.rb` (wrapper)
|
|
320
|
+
- [ ] Create `lib/fun_api/middleware/trusted_host.rb`
|
|
321
|
+
- [ ] Create `lib/fun_api/middleware/request_logger.rb`
|
|
322
|
+
- [ ] Add convenience methods: `add_cors`, `add_gzip`, etc.
|
|
323
|
+
- [ ] Add `rack-cors` to gemspec dependencies
|
|
324
|
+
|
|
325
|
+
### Phase 3: Documentation
|
|
326
|
+
- [ ] Update README with middleware section
|
|
327
|
+
- [ ] Document built-in middleware
|
|
328
|
+
- [ ] List compatible Rack middleware
|
|
329
|
+
- [ ] Create middleware guide
|
|
330
|
+
- [ ] Add examples for common use cases (auth, CORS, logging)
|
|
331
|
+
- [ ] Create `examples/middleware_demo.rb`
|
|
332
|
+
|
|
333
|
+
### Phase 4: Testing
|
|
334
|
+
- [ ] Create `test/test_middleware.rb`
|
|
335
|
+
- [ ] Test middleware ordering
|
|
336
|
+
- [ ] Test async compatibility
|
|
337
|
+
- [ ] Test with popular Rack middleware
|
|
338
|
+
- [ ] Integration tests for built-in middleware
|
|
339
|
+
- [ ] Edge case testing
|
|
340
|
+
|
|
341
|
+
## Questions Resolved
|
|
342
|
+
|
|
343
|
+
**Q: Should we leverage Rack for middleware?**
|
|
344
|
+
A: YES! Rack middleware is battle-tested and gives us access to the entire Ruby ecosystem. We should support standard Rack middleware while providing FastAPI-style convenience wrappers.
|
|
345
|
+
|
|
346
|
+
**Q: Which middleware should be built-in vs ecosystem?**
|
|
347
|
+
A:
|
|
348
|
+
- **Built-in**: CORS (wrapped), TrustedHost, RequestLogger (most common needs)
|
|
349
|
+
- **Ecosystem**: Rate limiting (rack-attack), Auth (warden), Caching (rack-cache)
|
|
350
|
+
- **Delegate to Rack**: Gzip (Rack::Deflater), ETag (Rack::ETag)
|
|
351
|
+
|
|
352
|
+
**Q: How to maintain async compatibility?**
|
|
353
|
+
A: Rack 3.0+ already supports async via Fiber/Async. Our middleware just needs to follow standard Rack interface (`call(env)` returns `[status, headers, body]`).
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# Background Tasks Analysis for FunAPI
|
|
2
|
+
|
|
3
|
+
## Executive Summary
|
|
4
|
+
|
|
5
|
+
After extensive testing of Ruby's Async gem, **we can implement background tasks with ZERO abstraction** - users can simply use `Async(task) do ... end` to spawn fire-and-forget tasks. However, there's a critical **dependency cleanup issue** that makes a lightweight `BackgroundTasks` abstraction valuable.
|
|
6
|
+
|
|
7
|
+
## Key Findings
|
|
8
|
+
|
|
9
|
+
### Python vs Ruby: Task Lifecycle
|
|
10
|
+
|
|
11
|
+
**Python (asyncio):**
|
|
12
|
+
- `asyncio.create_task()` returns immediately without waiting
|
|
13
|
+
- Tasks can be "orphaned" if not explicitly awaited
|
|
14
|
+
- This is WHY FastAPI needs `BackgroundTasks` - to ensure tasks complete before process exits
|
|
15
|
+
|
|
16
|
+
**Ruby (Async gem):**
|
|
17
|
+
- `task.async { }` creates a child task that IS tracked by parent
|
|
18
|
+
- Parent waits for children when block ends (via `Async do ... end.wait`)
|
|
19
|
+
- `Async(parent) { }` creates a DETACHED task (truly fire-and-forget)
|
|
20
|
+
|
|
21
|
+
### The Critical Problem: Dependency Cleanup
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
Async do |task|
|
|
25
|
+
begin
|
|
26
|
+
db = Database.connect
|
|
27
|
+
|
|
28
|
+
# User spawns background task
|
|
29
|
+
Async(task) do
|
|
30
|
+
sleep 0.1
|
|
31
|
+
send_email(db) # DB is captured in closure
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
[response, 200]
|
|
35
|
+
|
|
36
|
+
ensure
|
|
37
|
+
db.close # RUNS BEFORE background task!
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Timeline:**
|
|
43
|
+
1. Request handler starts
|
|
44
|
+
2. Response returned
|
|
45
|
+
3. **ensure block runs → DB closed**
|
|
46
|
+
4. Background task tries to use DB (might work due to closure, but semantically wrong)
|
|
47
|
+
|
|
48
|
+
### Solution Options
|
|
49
|
+
|
|
50
|
+
#### Option 1: No Abstraction (Pure Async)
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
api.post '/notify' do |input, req, task|
|
|
54
|
+
email = input[:body][:email]
|
|
55
|
+
|
|
56
|
+
# Detached background task
|
|
57
|
+
Async(task) do
|
|
58
|
+
send_welcome_email(email)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[{ message: "Sent" }, 200]
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Pros:**
|
|
66
|
+
- Zero magic, pure Ruby Async
|
|
67
|
+
- No new API to learn
|
|
68
|
+
- Explicit and clear
|
|
69
|
+
|
|
70
|
+
**Cons:**
|
|
71
|
+
- ❌ Runs AFTER dependency cleanup
|
|
72
|
+
- ❌ Dependencies might be closed when task runs
|
|
73
|
+
- ❌ No FastAPI parity
|
|
74
|
+
- ❌ No task collection/management
|
|
75
|
+
|
|
76
|
+
#### Option 2: BackgroundTasks Object (Recommended)
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
api.post '/notify' do |input, req, task, background:|
|
|
80
|
+
email = input[:body][:email]
|
|
81
|
+
|
|
82
|
+
background.add_task(:send_welcome_email, email)
|
|
83
|
+
|
|
84
|
+
[{ message: "Sent" }, 200]
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Implementation ensures correct execution order:**
|
|
89
|
+
1. Handler returns response tuple
|
|
90
|
+
2. **Background tasks execute** (with dependencies still available)
|
|
91
|
+
3. Dependency cleanup in ensure block
|
|
92
|
+
4. Response sent to client
|
|
93
|
+
|
|
94
|
+
**Pros:**
|
|
95
|
+
- ✅ Tasks run BEFORE dependency cleanup
|
|
96
|
+
- ✅ FastAPI API parity
|
|
97
|
+
- ✅ Dependencies guaranteed available
|
|
98
|
+
- ✅ Can inject dependencies into background tasks
|
|
99
|
+
- ✅ Error handling for task failures
|
|
100
|
+
- ✅ Testing/introspection support
|
|
101
|
+
|
|
102
|
+
**Cons:**
|
|
103
|
+
- Small abstraction needed (~50 lines)
|
|
104
|
+
|
|
105
|
+
## Recommended Implementation
|
|
106
|
+
|
|
107
|
+
### BackgroundTasks Class
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
module FunApi
|
|
111
|
+
class BackgroundTasks
|
|
112
|
+
def initialize(task, context)
|
|
113
|
+
@task = task
|
|
114
|
+
@context = context
|
|
115
|
+
@tasks = []
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def add_task(callable, *args, **kwargs)
|
|
119
|
+
@tasks << { callable: callable, args: args, kwargs: kwargs }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def execute
|
|
123
|
+
@tasks.each do |task_def|
|
|
124
|
+
callable = task_def[:callable]
|
|
125
|
+
args = task_def[:args]
|
|
126
|
+
kwargs = task_def[:kwargs]
|
|
127
|
+
|
|
128
|
+
# Spawn as child task (not detached)
|
|
129
|
+
@task.async do
|
|
130
|
+
if callable.respond_to?(:call)
|
|
131
|
+
callable.call(*args, **kwargs)
|
|
132
|
+
elsif callable.is_a?(Symbol) && @context.respond_to?(callable)
|
|
133
|
+
@context.public_send(callable, *args, **kwargs)
|
|
134
|
+
end
|
|
135
|
+
rescue => e
|
|
136
|
+
warn "Background task failed: #{e.message}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Wait for all background tasks to complete
|
|
141
|
+
@task.children.each(&:wait)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Modified Request Flow
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
def handle_async_route(req, path_params, body_schema, query_schema, response_schema, dependencies, &blk)
|
|
151
|
+
current_task = Async::Task.current
|
|
152
|
+
cleanup_objects = []
|
|
153
|
+
background_tasks = BackgroundTasks.new(current_task, self)
|
|
154
|
+
|
|
155
|
+
begin
|
|
156
|
+
# ... input validation ...
|
|
157
|
+
# ... dependency resolution ...
|
|
158
|
+
|
|
159
|
+
resolved_deps[:background] = background_tasks
|
|
160
|
+
|
|
161
|
+
payload, status = blk.call(input, req, current_task, **resolved_deps)
|
|
162
|
+
|
|
163
|
+
# Execute background tasks BEFORE ensure
|
|
164
|
+
background_tasks.execute
|
|
165
|
+
|
|
166
|
+
# ... response building ...
|
|
167
|
+
|
|
168
|
+
ensure
|
|
169
|
+
cleanup_objects.each(&:cleanup)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Execution Order
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
1. Request arrives
|
|
178
|
+
2. Dependencies resolved (DB connect, etc.)
|
|
179
|
+
3. Route handler executes
|
|
180
|
+
4. Handler returns [payload, status]
|
|
181
|
+
5. ⭐ Background tasks execute (deps still available)
|
|
182
|
+
6. Background tasks complete
|
|
183
|
+
7. Dependencies cleaned up (DB close, etc.)
|
|
184
|
+
8. Response tuple returned
|
|
185
|
+
9. HTTP layer sends response to client
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Use Cases
|
|
189
|
+
|
|
190
|
+
### Perfect For:
|
|
191
|
+
- Email notifications after signup
|
|
192
|
+
- Logging/analytics after request
|
|
193
|
+
- Cache warming
|
|
194
|
+
- Simple webhook calls
|
|
195
|
+
- Audit trail recording
|
|
196
|
+
|
|
197
|
+
### NOT For:
|
|
198
|
+
- Long-running jobs (> 30 seconds)
|
|
199
|
+
- Jobs requiring persistence/retries
|
|
200
|
+
- Jobs that must survive server restart
|
|
201
|
+
- Distributed job processing
|
|
202
|
+
→ Use Sidekiq, GoodJob, or Que instead
|
|
203
|
+
|
|
204
|
+
## API Design
|
|
205
|
+
|
|
206
|
+
### As Dependency (Recommended)
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
api.post '/signup', body: UserSchema do |input, req, task, background:|
|
|
210
|
+
user = create_user(input[:body])
|
|
211
|
+
|
|
212
|
+
background.add_task(:send_welcome_email, user[:email])
|
|
213
|
+
background.add_task(:notify_admin, user)
|
|
214
|
+
|
|
215
|
+
[user, 201]
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### With Callable Objects
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
background.add_task(method(:send_email), to: user[:email])
|
|
223
|
+
background.add_task(lambda { |id| log_event(id) }, user[:id])
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### With Dependencies
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
api.register(:mailer) { Mailer.new }
|
|
230
|
+
|
|
231
|
+
api.post '/signup', depends: [:mailer] do |input, req, task, mailer:, background:|
|
|
232
|
+
user = create_user(input[:body])
|
|
233
|
+
|
|
234
|
+
# Background task can use mailer (captured in closure)
|
|
235
|
+
background.add_task(lambda { mailer.send_welcome(user[:email]) })
|
|
236
|
+
|
|
237
|
+
[user, 201]
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Comparison to FastAPI
|
|
242
|
+
|
|
243
|
+
### FastAPI
|
|
244
|
+
```python
|
|
245
|
+
from fastapi import BackgroundTasks
|
|
246
|
+
|
|
247
|
+
@app.post("/send-notification/{email}")
|
|
248
|
+
async def notify(email: str, background_tasks: BackgroundTasks):
|
|
249
|
+
background_tasks.add_task(send_email, email, message="Hi")
|
|
250
|
+
return {"message": "Sent"}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### FunAPI
|
|
254
|
+
```ruby
|
|
255
|
+
api.post '/send-notification/:email' do |input, req, task, background:|
|
|
256
|
+
email = input[:path]['email']
|
|
257
|
+
background.add_task(:send_email, email, message: "Hi")
|
|
258
|
+
[{ message: "Sent" }, 200]
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Nearly identical!** ✨
|
|
263
|
+
|
|
264
|
+
## Testing Strategy
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
class TestBackgroundTasks < Minitest::Test
|
|
268
|
+
def test_background_tasks_execute_after_response
|
|
269
|
+
execution_order = []
|
|
270
|
+
|
|
271
|
+
app = FunApi::App.new do |api|
|
|
272
|
+
api.post '/test' do |input, req, task, background:|
|
|
273
|
+
execution_order << :handler
|
|
274
|
+
|
|
275
|
+
background.add_task(lambda { execution_order << :background })
|
|
276
|
+
|
|
277
|
+
[{ ok: true }, 200]
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
res = async_request(app, :post, '/test')
|
|
282
|
+
|
|
283
|
+
assert_equal [:handler, :background], execution_order
|
|
284
|
+
assert_equal 200, res.status
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_background_tasks_can_access_dependencies
|
|
288
|
+
# Test that DB is still available in background task
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def test_background_task_errors_are_handled
|
|
292
|
+
# Test that task errors don't crash app
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Recommendation: Implement BackgroundTasks
|
|
298
|
+
|
|
299
|
+
**Why:**
|
|
300
|
+
1. ✅ Correct execution order (before dependency cleanup)
|
|
301
|
+
2. ✅ FastAPI API parity
|
|
302
|
+
3. ✅ Safe dependency access
|
|
303
|
+
4. ✅ Better error handling
|
|
304
|
+
5. ✅ Testable and introspectable
|
|
305
|
+
6. ✅ Small implementation (~50-80 lines)
|
|
306
|
+
|
|
307
|
+
**Why not "no API":**
|
|
308
|
+
1. ❌ `Async(task) { }` runs AFTER cleanup
|
|
309
|
+
2. ❌ Dependencies might be closed
|
|
310
|
+
3. ❌ No error handling
|
|
311
|
+
4. ❌ Harder to test
|
|
312
|
+
5. ❌ Less discoverable
|
|
313
|
+
|
|
314
|
+
## Next Steps
|
|
315
|
+
|
|
316
|
+
1. Implement `BackgroundTasks` class
|
|
317
|
+
2. Modify `handle_async_route` to inject and execute
|
|
318
|
+
3. Write comprehensive tests
|
|
319
|
+
4. Create examples (email, logging, webhooks)
|
|
320
|
+
5. Update docs (README, AGENTS.md)
|
|
321
|
+
6. Consider dependency injection into background tasks
|
|
322
|
+
|
|
323
|
+
**Estimated effort:** 2-3 hours
|
|
324
|
+
**Impact:** High - production-critical feature
|
|
325
|
+
**Complexity:** Low - leverages existing Async infrastructure
|