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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/25-09-01-OPENAPI_IMPLEMENTATION.md +233 -0
  3. data/.claude/25-09-05-RESPONSE_SCHEMA.md +383 -0
  4. data/.claude/25-09-10-OPENAPI_PLAN.md +219 -0
  5. data/.claude/25-10-26-MIDDLEWARE_IMPLEMENTATION.md +230 -0
  6. data/.claude/25-10-26-MIDDLEWARE_PLAN.md +353 -0
  7. data/.claude/25-10-27-BACKGROUND_TASKS_ANALYSIS.md +325 -0
  8. data/.claude/25-10-27-DEPENDENCY_IMPLEMENTATION_SUMMARY.md +325 -0
  9. data/.claude/25-10-27-DEPENDENCY_INJECTION_PLAN.md +753 -0
  10. data/.claude/25-12-24-LIFECYCLE_HOOKS_PLAN.md +421 -0
  11. data/.claude/25-12-24-PUBLISHING_AND_DOGFOODING_PLAN.md +327 -0
  12. data/.claude/25-12-24-TEMPLATE_RENDERING_PLAN.md +704 -0
  13. data/.claude/DECISIONS.md +397 -0
  14. data/.claude/PROJECT_PLAN.md +80 -0
  15. data/.claude/TESTING_PLAN.md +285 -0
  16. data/.claude/TESTING_STATUS.md +157 -0
  17. data/.tool-versions +1 -0
  18. data/AGENTS.md +416 -0
  19. data/CHANGELOG.md +5 -0
  20. data/CODE_OF_CONDUCT.md +132 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +660 -0
  23. data/Rakefile +10 -0
  24. data/docs +8 -0
  25. data/docs-site/.gitignore +3 -0
  26. data/docs-site/Gemfile +9 -0
  27. data/docs-site/app.rb +138 -0
  28. data/docs-site/content/essential/handler.md +156 -0
  29. data/docs-site/content/essential/lifecycle.md +161 -0
  30. data/docs-site/content/essential/middleware.md +201 -0
  31. data/docs-site/content/essential/openapi.md +155 -0
  32. data/docs-site/content/essential/routing.md +123 -0
  33. data/docs-site/content/essential/validation.md +166 -0
  34. data/docs-site/content/getting-started/at-glance.md +82 -0
  35. data/docs-site/content/getting-started/key-concepts.md +150 -0
  36. data/docs-site/content/getting-started/quick-start.md +127 -0
  37. data/docs-site/content/index.md +81 -0
  38. data/docs-site/content/patterns/async-operations.md +137 -0
  39. data/docs-site/content/patterns/background-tasks.md +143 -0
  40. data/docs-site/content/patterns/database.md +175 -0
  41. data/docs-site/content/patterns/dependencies.md +141 -0
  42. data/docs-site/content/patterns/deployment.md +212 -0
  43. data/docs-site/content/patterns/error-handling.md +184 -0
  44. data/docs-site/content/patterns/response-schema.md +159 -0
  45. data/docs-site/content/patterns/templates.md +193 -0
  46. data/docs-site/content/patterns/testing.md +218 -0
  47. data/docs-site/mise.toml +2 -0
  48. data/docs-site/public/css/style.css +234 -0
  49. data/docs-site/templates/layouts/docs.html.erb +28 -0
  50. data/docs-site/templates/page.html.erb +3 -0
  51. data/docs-site/templates/partials/_nav.html.erb +19 -0
  52. data/examples/background_tasks_demo.rb +159 -0
  53. data/examples/demo_middleware.rb +55 -0
  54. data/examples/demo_openapi.rb +63 -0
  55. data/examples/dependency_block_demo.rb +150 -0
  56. data/examples/dependency_cleanup_demo.rb +146 -0
  57. data/examples/dependency_injection_demo.rb +200 -0
  58. data/examples/lifecycle_demo.rb +57 -0
  59. data/examples/middleware_demo.rb +74 -0
  60. data/examples/templates/layouts/application.html.erb +66 -0
  61. data/examples/templates/todos/_todo.html.erb +15 -0
  62. data/examples/templates/todos/index.html.erb +12 -0
  63. data/examples/templates_demo.rb +87 -0
  64. data/lib/funapi/application.rb +521 -0
  65. data/lib/funapi/async.rb +57 -0
  66. data/lib/funapi/background_tasks.rb +52 -0
  67. data/lib/funapi/config.rb +23 -0
  68. data/lib/funapi/database/sequel/fibered_connection_pool.rb +87 -0
  69. data/lib/funapi/dependency_wrapper.rb +66 -0
  70. data/lib/funapi/depends.rb +138 -0
  71. data/lib/funapi/exceptions.rb +72 -0
  72. data/lib/funapi/middleware/base.rb +13 -0
  73. data/lib/funapi/middleware/cors.rb +23 -0
  74. data/lib/funapi/middleware/request_logger.rb +32 -0
  75. data/lib/funapi/middleware/trusted_host.rb +34 -0
  76. data/lib/funapi/middleware.rb +4 -0
  77. data/lib/funapi/openapi/schema_converter.rb +85 -0
  78. data/lib/funapi/openapi/spec_generator.rb +179 -0
  79. data/lib/funapi/router.rb +43 -0
  80. data/lib/funapi/schema.rb +65 -0
  81. data/lib/funapi/server/falcon.rb +38 -0
  82. data/lib/funapi/template_response.rb +17 -0
  83. data/lib/funapi/templates.rb +111 -0
  84. data/lib/funapi/version.rb +5 -0
  85. data/lib/funapi.rb +14 -0
  86. data/sig/fun_api.rbs +499 -0
  87. metadata +220 -0
@@ -0,0 +1,421 @@
1
+ # Lifecycle Hooks Implementation Plan
2
+
3
+ ## Date: 2024-12-24
4
+
5
+ ## Overview
6
+
7
+ Implement startup and shutdown lifecycle hooks for FunApi. These hooks allow users to run code before the server accepts requests and after it stops, useful for initializing/cleaning up resources like database connections, HTTP clients, and background task supervisors.
8
+
9
+ ## Design Decision
10
+
11
+ **Approach**: Separate callbacks (`on_startup`/`on_shutdown`)
12
+
13
+ **Why callbacks over block/yield pattern**:
14
+ - Simpler implementation (~10 lines vs ~20 lines with Fibers)
15
+ - Multiple hooks are natural (common use case: init DB, cache, metrics separately)
16
+ - Familiar to Ruby developers (Rails, Sinatra patterns)
17
+ - Block pattern can be added later if demand exists
18
+
19
+ **Why NOT shared state between lifecycle and routes**:
20
+ - Async gems (Sequel, HTTP clients) manage their own connection pools
21
+ - Long-lived resources are typically singletons/constants
22
+ - Routes already have dependency injection for per-request resources
23
+ - Keeps implementation simple
24
+
25
+ ## API Design
26
+
27
+ ```ruby
28
+ app = FunApi::App.new do |api|
29
+ api.on_startup do
30
+ puts "Connecting to database..."
31
+ DB.connect
32
+ end
33
+
34
+ api.on_startup do
35
+ puts "Warming cache..."
36
+ Cache.warm
37
+ end
38
+
39
+ api.on_shutdown do
40
+ puts "Closing database..."
41
+ DB.disconnect
42
+ end
43
+
44
+ api.get '/users' do |input, req, task|
45
+ [DB[:users].all, 200]
46
+ end
47
+ end
48
+ ```
49
+
50
+ **Key behaviors**:
51
+ - Multiple hooks allowed (executed in registration order)
52
+ - Startup hooks run inside Async context, before server accepts requests
53
+ - Shutdown hooks run inside Async context, after server stops accepting requests
54
+ - Hooks can be async (have access to current Async::Task if needed)
55
+ - Errors in startup hooks should prevent server from starting
56
+ - Errors in shutdown hooks should be logged but not prevent other hooks from running
57
+
58
+ ## Implementation
59
+
60
+ ### 1. Application Changes (`lib/fun_api/application.rb`)
61
+
62
+ Add to `App` class:
63
+
64
+ ```ruby
65
+ def initialize(...)
66
+ # ... existing code ...
67
+ @startup_hooks = []
68
+ @shutdown_hooks = []
69
+
70
+ yield self if block_given?
71
+ # ... rest of existing code ...
72
+ end
73
+
74
+ def on_startup(&block)
75
+ raise ArgumentError, "on_startup requires a block" unless block_given?
76
+ @startup_hooks << block
77
+ self
78
+ end
79
+
80
+ def on_shutdown(&block)
81
+ raise ArgumentError, "on_shutdown requires a block" unless block_given?
82
+ @shutdown_hooks << block
83
+ self
84
+ end
85
+
86
+ def run_startup_hooks
87
+ @startup_hooks.each(&:call)
88
+ end
89
+
90
+ def run_shutdown_hooks
91
+ @shutdown_hooks.each do |hook|
92
+ hook.call
93
+ rescue => e
94
+ warn "Shutdown hook failed: #{e.message}"
95
+ end
96
+ end
97
+ ```
98
+
99
+ ### 2. Falcon Server Changes (`lib/fun_api/server/falcon.rb`)
100
+
101
+ ```ruby
102
+ def self.start(app, host: "0.0.0.0", port: 3000)
103
+ Async do |task|
104
+ falcon_app = Protocol::Rack::Adapter.new(app)
105
+ endpoint = ::Async::HTTP::Endpoint.parse("http://#{host}:#{port}")
106
+ server = ::Falcon::Server.new(falcon_app, endpoint)
107
+
108
+ # Run startup hooks
109
+ app.run_startup_hooks if app.respond_to?(:run_startup_hooks)
110
+
111
+ puts "Falcon listening on #{host}:#{port}"
112
+
113
+ shutdown = -> {
114
+ puts "\nShutting down..."
115
+ app.run_shutdown_hooks if app.respond_to?(:run_shutdown_hooks)
116
+ task.stop
117
+ }
118
+
119
+ trap(:INT) { shutdown.call }
120
+ trap(:TERM) { shutdown.call }
121
+
122
+ server.run
123
+ end
124
+ end
125
+ ```
126
+
127
+ ### 3. Accessor for Hooks (for testing)
128
+
129
+ ```ruby
130
+ # In Application class
131
+ attr_reader :startup_hooks, :shutdown_hooks
132
+ ```
133
+
134
+ ## Test Cases
135
+
136
+ Create `test/test_lifecycle.rb`:
137
+
138
+ ```ruby
139
+ class TestLifecycle < Minitest::Test
140
+ def test_on_startup_registers_hook
141
+ app = FunApi::App.new do |api|
142
+ api.on_startup { :startup }
143
+ end
144
+
145
+ assert_equal 1, app.startup_hooks.size
146
+ end
147
+
148
+ def test_on_shutdown_registers_hook
149
+ app = FunApi::App.new do |api|
150
+ api.on_shutdown { :shutdown }
151
+ end
152
+
153
+ assert_equal 1, app.shutdown_hooks.size
154
+ end
155
+
156
+ def test_multiple_startup_hooks
157
+ app = FunApi::App.new do |api|
158
+ api.on_startup { :first }
159
+ api.on_startup { :second }
160
+ end
161
+
162
+ assert_equal 2, app.startup_hooks.size
163
+ end
164
+
165
+ def test_multiple_shutdown_hooks
166
+ app = FunApi::App.new do |api|
167
+ api.on_shutdown { :first }
168
+ api.on_shutdown { :second }
169
+ end
170
+
171
+ assert_equal 2, app.shutdown_hooks.size
172
+ end
173
+
174
+ def test_run_startup_hooks_executes_in_order
175
+ order = []
176
+ app = FunApi::App.new do |api|
177
+ api.on_startup { order << 1 }
178
+ api.on_startup { order << 2 }
179
+ api.on_startup { order << 3 }
180
+ end
181
+
182
+ app.run_startup_hooks
183
+
184
+ assert_equal [1, 2, 3], order
185
+ end
186
+
187
+ def test_run_shutdown_hooks_executes_in_order
188
+ order = []
189
+ app = FunApi::App.new do |api|
190
+ api.on_shutdown { order << 1 }
191
+ api.on_shutdown { order << 2 }
192
+ end
193
+
194
+ app.run_shutdown_hooks
195
+
196
+ assert_equal [1, 2], order
197
+ end
198
+
199
+ def test_shutdown_hook_error_does_not_stop_other_hooks
200
+ order = []
201
+ app = FunApi::App.new do |api|
202
+ api.on_shutdown { order << 1 }
203
+ api.on_shutdown { raise "error" }
204
+ api.on_shutdown { order << 3 }
205
+ end
206
+
207
+ app.run_shutdown_hooks
208
+
209
+ assert_equal [1, 3], order
210
+ end
211
+
212
+ def test_startup_hook_error_propagates
213
+ app = FunApi::App.new do |api|
214
+ api.on_startup { raise "startup failed" }
215
+ end
216
+
217
+ assert_raises(RuntimeError) { app.run_startup_hooks }
218
+ end
219
+
220
+ def test_on_startup_requires_block
221
+ app = FunApi::App.new
222
+
223
+ assert_raises(ArgumentError) { app.on_startup }
224
+ end
225
+
226
+ def test_on_shutdown_requires_block
227
+ app = FunApi::App.new
228
+
229
+ assert_raises(ArgumentError) { app.on_shutdown }
230
+ end
231
+
232
+ def test_on_startup_returns_self_for_chaining
233
+ app = FunApi::App.new
234
+
235
+ result = app.on_startup { :hook }
236
+
237
+ assert_same app, result
238
+ end
239
+
240
+ def test_hooks_work_with_async_context
241
+ order = []
242
+ app = FunApi::App.new do |api|
243
+ api.on_startup do
244
+ Async do |task|
245
+ task.sleep(0.001)
246
+ order << :async_startup
247
+ end.wait
248
+ end
249
+ end
250
+
251
+ Async { app.run_startup_hooks }.wait
252
+
253
+ assert_equal [:async_startup], order
254
+ end
255
+ end
256
+ ```
257
+
258
+ ## Example Usage
259
+
260
+ Create `examples/lifecycle_demo.rb`:
261
+
262
+ ```ruby
263
+ require_relative "../lib/fun_api"
264
+ require_relative "../lib/fun_api/server/falcon"
265
+
266
+ DB = { connected: false, users: [] }
267
+ CACHE = { warmed: false }
268
+
269
+ app = FunApi::App.new(
270
+ title: "Lifecycle Demo",
271
+ version: "1.0.0"
272
+ ) do |api|
273
+ api.on_startup do
274
+ puts "Connecting to database..."
275
+ sleep 0.1 # Simulate connection time
276
+ DB[:connected] = true
277
+ DB[:users] = [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}]
278
+ puts "Database connected!"
279
+ end
280
+
281
+ api.on_startup do
282
+ puts "Warming cache..."
283
+ sleep 0.05
284
+ CACHE[:warmed] = true
285
+ puts "Cache warmed!"
286
+ end
287
+
288
+ api.on_shutdown do
289
+ puts "Closing database connection..."
290
+ DB[:connected] = false
291
+ puts "Database disconnected!"
292
+ end
293
+
294
+ api.on_shutdown do
295
+ puts "Clearing cache..."
296
+ CACHE[:warmed] = false
297
+ puts "Cache cleared!"
298
+ end
299
+
300
+ api.get "/status" do |_input, _req, _task|
301
+ [{
302
+ db_connected: DB[:connected],
303
+ cache_warmed: CACHE[:warmed]
304
+ }, 200]
305
+ end
306
+
307
+ api.get "/users" do |_input, _req, _task|
308
+ [DB[:users], 200]
309
+ end
310
+ end
311
+
312
+ puts "Starting Lifecycle Demo..."
313
+ puts "Try: curl http://localhost:3000/status"
314
+ puts "Try: curl http://localhost:3000/users"
315
+ puts ""
316
+
317
+ FunApi::Server::Falcon.start(app, port: 3000)
318
+ ```
319
+
320
+ ## Files to Modify
321
+
322
+ 1. `lib/fun_api/application.rb` - Add hook registration and execution methods
323
+ 2. `lib/fun_api/server/falcon.rb` - Call hooks at appropriate times
324
+
325
+ ## Files to Create
326
+
327
+ 1. `test/test_lifecycle.rb` - Lifecycle hook tests
328
+ 2. `examples/lifecycle_demo.rb` - Demo application
329
+
330
+ ## Files to Update
331
+
332
+ 1. `AGENTS.md` - Add lifecycle hooks documentation
333
+ 2. `README.md` - Add lifecycle hooks section
334
+ 3. `.claude/PROJECT_PLAN.md` - Mark lifecycle hooks as complete
335
+
336
+ ## Documentation Updates
337
+
338
+ ### AGENTS.md Addition
339
+
340
+ ```markdown
341
+ ### Lifecycle Hooks
342
+
343
+ Run code at startup/shutdown:
344
+ ```ruby
345
+ api.on_startup do
346
+ DB.connect
347
+ Cache.warm
348
+ end
349
+
350
+ api.on_shutdown do
351
+ DB.disconnect
352
+ end
353
+ ```
354
+
355
+ - Multiple hooks allowed (run in registration order)
356
+ - Startup hooks run before server accepts requests
357
+ - Shutdown hooks run after server stops
358
+ - Shutdown errors logged but don't stop other hooks
359
+ ```
360
+
361
+ ### README.md Addition
362
+
363
+ ```markdown
364
+ ### 10. Lifecycle Hooks
365
+
366
+ Execute code when the application starts up or shuts down:
367
+
368
+ ```ruby
369
+ app = FunApi::App.new do |api|
370
+ api.on_startup do
371
+ puts "Connecting to database..."
372
+ DB.connect
373
+ end
374
+
375
+ api.on_startup do
376
+ puts "Warming cache..."
377
+ Cache.warm
378
+ end
379
+
380
+ api.on_shutdown do
381
+ puts "Disconnecting..."
382
+ DB.disconnect
383
+ end
384
+ end
385
+ ```
386
+
387
+ **Key behaviors:**
388
+ - Multiple hooks supported (executed in registration order)
389
+ - Startup hooks run before server accepts requests
390
+ - Shutdown hooks run after server stops accepting requests
391
+ - Startup errors prevent server from starting
392
+ - Shutdown errors are logged but don't prevent other hooks from running
393
+
394
+ **Use cases:**
395
+ - Database connection pool initialization
396
+ - Cache warming
397
+ - Background task supervisor setup
398
+ - Metrics/logging initialization
399
+ - Graceful resource cleanup
400
+ ```
401
+
402
+ ## Success Criteria
403
+
404
+ 1. `on_startup` registers hooks that run before server accepts requests
405
+ 2. `on_shutdown` registers hooks that run after server stops
406
+ 3. Multiple hooks execute in registration order
407
+ 4. Shutdown hook errors don't prevent other hooks from running
408
+ 5. All tests pass
409
+ 6. Linter passes
410
+ 7. Demo example works correctly
411
+
412
+ ## Estimated Effort
413
+
414
+ ~1-2 hours
415
+
416
+ ## Notes
417
+
418
+ - Keep implementation minimal - this is a simple feature
419
+ - Don't over-engineer (no priority system, no async hook detection, etc.)
420
+ - The Falcon server integration is the key piece - hooks must run inside Async context
421
+ - Consider: should hooks have access to the app instance? (Probably not needed for v1)