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,753 @@
1
+ # Dependency Injection Implementation Plan
2
+
3
+ ## Date: 2024-10-27
4
+
5
+ ## Goal
6
+
7
+ Implement FastAPI-style dependency injection for FunApi, providing a flexible, lightweight system that allows injecting dependencies into route handlers without heavy external frameworks.
8
+
9
+ ## Research Summary
10
+
11
+ ### FastAPI's Dependency Injection
12
+
13
+ **Core Mechanism:**
14
+ ```python
15
+ from fastapi import Depends
16
+
17
+ def common_parameters(q: str = None):
18
+ return {"q": q}
19
+
20
+ @app.get("/items/")
21
+ async def read_items(commons: dict = Depends(common_parameters)):
22
+ return commons
23
+ ```
24
+
25
+ **Key Features:**
26
+ 1. Dependencies are functions that can take same parameters as path operations
27
+ 2. `Depends()` wrapper marks parameters as dependencies
28
+ 3. Dependencies can have sub-dependencies (nested)
29
+ 4. Dependencies can be callable classes (using `__init__` for params, `__call__` for execution)
30
+ 5. Dependencies with `yield` for setup/teardown
31
+ 6. Type aliases for reusable dependency declarations
32
+ 7. Integrated with OpenAPI documentation
33
+
34
+ **Execution Flow:**
35
+ 1. FastAPI inspects route handler signature
36
+ 2. Identifies parameters with `Depends()`
37
+ 3. Executes dependency functions with their own dependencies
38
+ 4. Injects results into route handler parameters
39
+
40
+ ### Ruby Ecosystem Review
41
+
42
+ **dry-system + dry-auto_inject:**
43
+ - Full-featured DI framework
44
+ - Constructor injection focused
45
+ - Requires container setup, auto-registration
46
+ - Heavy for FunApi's minimal philosophy
47
+
48
+ **dry-container:**
49
+ - Simple key-value container
50
+ - Manual registration required
51
+ - Good for complex apps but overkill for our use case
52
+
53
+ **Recommendation: Custom Lightweight Implementation**
54
+ - Align with FunApi's minimal, explicit philosophy
55
+ - FastAPI-like API adapted to Ruby idioms
56
+ - No heavy dependencies beyond what we already use
57
+ - Clear, simple implementation users can understand
58
+
59
+ ## Design
60
+
61
+ ### API Design (Ruby-Idiomatic)
62
+
63
+ ```ruby
64
+ # Define a dependency (just a proc, lambda, or method)
65
+ def get_db
66
+ Database.connect
67
+ end
68
+
69
+ def get_current_user(req:, db: Depends(get_db))
70
+ token = req.env['HTTP_AUTHORIZATION']
71
+ db.find_user_by_token(token) || raise FunApi::HTTPException.new(status_code: 401)
72
+ end
73
+
74
+ # Use in routes
75
+ api.get '/users/:id', deps: { db: get_db } do |input, req, task, db:|
76
+ user = db.find(input[:path]['id'])
77
+ [user, 200]
78
+ end
79
+
80
+ # Or with Depends for sub-dependencies
81
+ api.get '/profile', deps: { user: get_current_user } do |input, req, task, user:|
82
+ [user, 200]
83
+ end
84
+
85
+ # Class-based dependencies (callable)
86
+ class Paginator
87
+ def initialize(max_limit: 100)
88
+ @max_limit = max_limit
89
+ end
90
+
91
+ def call(limit: 10, offset: 0)
92
+ {
93
+ limit: [limit, @max_limit].min,
94
+ offset: offset
95
+ }
96
+ end
97
+ end
98
+
99
+ pagination = Paginator.new(max_limit: 50)
100
+
101
+ api.get '/items', deps: { page: pagination } do |input, req, task, page:|
102
+ [{ pagination: page }, 200]
103
+ end
104
+ ```
105
+
106
+ ### Alternative Syntax (More Ruby-like)
107
+
108
+ After consideration, let's use a simpler approach:
109
+
110
+ ```ruby
111
+ # Dependencies as keyword arguments to route
112
+ api.get '/users/:id',
113
+ depends: { db: -> { Database.connect } } do |input, req, task, db:|
114
+ user = db.find(input[:path]['id'])
115
+ [user, 200]
116
+ end
117
+
118
+ # Or use a helper for cleaner syntax
119
+ def db_connection
120
+ -> { Database.connect }
121
+ end
122
+
123
+ api.get '/users/:id',
124
+ depends: { db: db_connection } do |input, req, task, db:|
125
+ user = db.find(input[:path]['id'])
126
+ [user, 200]
127
+ end
128
+ ```
129
+
130
+ ### Core Components
131
+
132
+ 1. **FunApi::Depends** - Wrapper class to mark dependencies with sub-dependencies
133
+ 2. **Dependency Resolution** - Mechanism to resolve dependency graph
134
+ 3. **Route Handler Enhancement** - Inject resolved dependencies as keyword args
135
+ 4. **OpenAPI Integration** - Document dependencies in spec
136
+
137
+ ### Implementation Strategy
138
+
139
+ **Phase 1: Core Functionality**
140
+ 1. Create `FunApi::Depends` class
141
+ 2. Add `depends:` parameter to route definition
142
+ 3. Implement dependency resolution in request handling
143
+ 4. Support simple dependencies (procs/lambdas)
144
+
145
+ **Phase 2: Advanced Features**
146
+ 5. Support class-based dependencies (callable)
147
+ 6. Support sub-dependencies (nested Depends)
148
+ 7. Handle request-scoped dependencies
149
+
150
+ **Phase 3: Integration**
151
+ 8. OpenAPI documentation
152
+ 9. Error handling for dependency failures
153
+ 10. Testing utilities
154
+
155
+ ## Detailed Implementation
156
+
157
+ ### 1. FunApi::Depends Class
158
+
159
+ ```ruby
160
+ # lib/fun_api/depends.rb
161
+ module FunApi
162
+ class Depends
163
+ attr_reader :dependency, :kwargs
164
+
165
+ def initialize(dependency, **kwargs)
166
+ @dependency = dependency
167
+ @kwargs = kwargs
168
+ end
169
+
170
+ def call(context)
171
+ resolved_kwargs = resolve_kwargs(context)
172
+
173
+ if @dependency.respond_to?(:call)
174
+ if @dependency.arity == 0 || @dependency.arity == -1
175
+ # Lambda/Proc with no args or variable args
176
+ @dependency.call(**resolved_kwargs)
177
+ else
178
+ @dependency.call(**resolved_kwargs)
179
+ end
180
+ else
181
+ raise ArgumentError, "Dependency must be callable"
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def resolve_kwargs(context)
188
+ @kwargs.transform_values do |value|
189
+ if value.is_a?(Depends)
190
+ value.call(context)
191
+ elsif value.respond_to?(:call)
192
+ value.call
193
+ else
194
+ value
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ ```
201
+
202
+ ### 2. Route Definition Enhancement
203
+
204
+ ```ruby
205
+ # In lib/fun_api/application.rb
206
+
207
+ def add_route(verb, path, body: nil, query: nil, response_schema: nil, depends: {}, &handler)
208
+ route = {
209
+ handler: handler,
210
+ body_schema: body,
211
+ query_schema: query,
212
+ response_schema: response_schema,
213
+ dependencies: normalize_dependencies(depends)
214
+ }
215
+
216
+ router.add_route(verb, path, route)
217
+ end
218
+
219
+ private
220
+
221
+ def normalize_dependencies(depends)
222
+ depends.transform_values do |dep|
223
+ if dep.is_a?(Depends)
224
+ dep
225
+ elsif dep.respond_to?(:call)
226
+ Depends.new(dep)
227
+ else
228
+ raise ArgumentError, "Dependency must be callable or Depends instance"
229
+ end
230
+ end
231
+ end
232
+ ```
233
+
234
+ ### 3. Dependency Resolution in Request Handling
235
+
236
+ ```ruby
237
+ # In lib/fun_api/application.rb (call method)
238
+
239
+ def call(env)
240
+ Async do |task|
241
+ # ... existing route matching code ...
242
+
243
+ # Resolve dependencies
244
+ dependency_context = {
245
+ input: validated_input,
246
+ req: request,
247
+ task: task
248
+ }
249
+
250
+ resolved_deps = resolve_dependencies(
251
+ route[:dependencies],
252
+ dependency_context
253
+ )
254
+
255
+ # Call handler with dependencies as keyword arguments
256
+ response_data, status_code = handler.call(
257
+ validated_input,
258
+ request,
259
+ task,
260
+ **resolved_deps
261
+ )
262
+
263
+ # ... rest of response handling ...
264
+ end.wait
265
+ end
266
+
267
+ private
268
+
269
+ def resolve_dependencies(dependencies, context)
270
+ dependencies.transform_values do |dep|
271
+ dep.call(context)
272
+ end
273
+ rescue => e
274
+ # Convert to HTTPException
275
+ raise FunApi::HTTPException.new(
276
+ status_code: 500,
277
+ detail: "Dependency resolution failed: #{e.message}"
278
+ )
279
+ end
280
+ ```
281
+
282
+ ### 4. Support for Request Parameters in Dependencies
283
+
284
+ Dependencies should be able to access request data:
285
+
286
+ ```ruby
287
+ # Dependencies can declare what they need
288
+ api.get '/profile',
289
+ depends: {
290
+ user: ->(req:) {
291
+ token = req.env['HTTP_AUTHORIZATION']
292
+ find_user_by_token(token)
293
+ }
294
+ } do |input, req, task, user:|
295
+ [{ user: user }, 200]
296
+ end
297
+ ```
298
+
299
+ Implementation:
300
+ ```ruby
301
+ class Depends
302
+ def call(context)
303
+ # Introspect what the dependency needs
304
+ params = {}
305
+
306
+ if @dependency.respond_to?(:parameters)
307
+ @dependency.parameters.each do |type, name|
308
+ next unless type == :keyreq || type == :key
309
+
310
+ # Provide from context
311
+ params[name] = context[name] if context.key?(name)
312
+ end
313
+ end
314
+
315
+ # Merge with sub-dependencies
316
+ params.merge!(resolve_kwargs(context))
317
+
318
+ @dependency.call(**params)
319
+ end
320
+ end
321
+ ```
322
+
323
+ ### 5. Class-based Dependencies
324
+
325
+ ```ruby
326
+ class DatabaseConnection
327
+ def call(req:, task:)
328
+ # Use task for async operations if needed
329
+ task.async { connect_to_db }.wait
330
+ end
331
+
332
+ private
333
+
334
+ def connect_to_db
335
+ Database.connect
336
+ end
337
+ end
338
+
339
+ db_dep = DatabaseConnection.new
340
+
341
+ api.get '/users',
342
+ depends: { db: db_dep } do |input, req, task, db:|
343
+ users = db.all_users
344
+ [users, 200]
345
+ end
346
+ ```
347
+
348
+ ### 6. Parameterized Dependencies (Callable Classes)
349
+
350
+ ```ruby
351
+ class Paginator
352
+ def initialize(max_limit: 100)
353
+ @max_limit = max_limit
354
+ end
355
+
356
+ def call(input:)
357
+ limit = [input[:query][:limit] || 10, @max_limit].min
358
+ offset = input[:query][:offset] || 0
359
+ { limit: limit, offset: offset }
360
+ end
361
+ end
362
+
363
+ api.get '/items',
364
+ query: PaginationSchema,
365
+ depends: { page: Paginator.new(max_limit: 50) } do |input, req, task, page:|
366
+ items = Item.limit(page[:limit]).offset(page[:offset])
367
+ [items, 200]
368
+ end
369
+ ```
370
+
371
+ ### 7. Nested Dependencies (Sub-dependencies)
372
+
373
+ ```ruby
374
+ def get_db
375
+ ->(task:) {
376
+ task.async { Database.connect }.wait
377
+ }
378
+ end
379
+
380
+ def get_current_user
381
+ ->(req:, db: FunApi::Depends(get_db)) {
382
+ token = req.env['HTTP_AUTHORIZATION']
383
+ db.find_user_by_token(token) || raise FunApi::HTTPException.new(status_code: 401)
384
+ }
385
+ end
386
+
387
+ api.get '/profile',
388
+ depends: { user: get_current_user } do |input, req, task, user:|
389
+ [user, 200]
390
+ end
391
+ ```
392
+
393
+ ## Testing Strategy
394
+
395
+ ### Unit Tests
396
+
397
+ ```ruby
398
+ # test/test_depends.rb
399
+ class TestDepends < Minitest::Test
400
+ def test_simple_dependency
401
+ dep = FunApi::Depends.new(-> { "hello" })
402
+ result = dep.call({})
403
+ assert_equal "hello", result
404
+ end
405
+
406
+ def test_dependency_with_context
407
+ dep = FunApi::Depends.new(->(req:) { req[:value] })
408
+ result = dep.call(req: { value: 42 })
409
+ assert_equal 42, result
410
+ end
411
+
412
+ def test_nested_dependencies
413
+ db = FunApi::Depends.new(-> { "db_connection" })
414
+ user = FunApi::Depends.new(->(db:) { "user_from_#{db}" }, db: db)
415
+
416
+ result = user.call({})
417
+ assert_equal "user_from_db_connection", result
418
+ end
419
+ end
420
+
421
+ # test/test_dependency_injection.rb
422
+ class TestDependencyInjection < Minitest::Test
423
+ def test_route_with_simple_dependency
424
+ app = FunApi::App.new do |api|
425
+ api.get '/test',
426
+ depends: { value: -> { 42 } } do |input, req, task, value:|
427
+ [{ value: value }, 200]
428
+ end
429
+ end
430
+
431
+ res = async_request(app, :get, '/test')
432
+ assert_equal 200, res.status
433
+ data = JSON.parse(res.body, symbolize_names: true)
434
+ assert_equal 42, data[:value]
435
+ end
436
+
437
+ def test_route_with_request_context_dependency
438
+ app = FunApi::App.new do |api|
439
+ api.get '/test',
440
+ depends: {
441
+ auth: ->(req:) { req.env['HTTP_AUTHORIZATION'] }
442
+ } do |input, req, task, auth:|
443
+ [{ token: auth }, 200]
444
+ end
445
+ end
446
+
447
+ res = async_request(app, :get, '/test', 'HTTP_AUTHORIZATION' => 'Bearer token123')
448
+ assert_equal 200, res.status
449
+ data = JSON.parse(res.body, symbolize_names: true)
450
+ assert_equal 'Bearer token123', data[:token]
451
+ end
452
+
453
+ def test_route_with_nested_dependencies
454
+ app = FunApi::App.new do |api|
455
+ db = -> { { users: [{ id: 1, name: 'Alice' }] } }
456
+ user = ->(db:) { db[:users].first }
457
+
458
+ api.get '/user',
459
+ depends: {
460
+ current_user: FunApi::Depends.new(user, db: FunApi::Depends.new(db))
461
+ } do |input, req, task, current_user:|
462
+ [current_user, 200]
463
+ end
464
+ end
465
+
466
+ res = async_request(app, :get, '/user')
467
+ assert_equal 200, res.status
468
+ data = JSON.parse(res.body, symbolize_names: true)
469
+ assert_equal 'Alice', data[:name]
470
+ end
471
+
472
+ def test_class_based_dependency
473
+ class TestDep
474
+ def call
475
+ "from_class"
476
+ end
477
+ end
478
+
479
+ app = FunApi::App.new do |api|
480
+ api.get '/test',
481
+ depends: { value: TestDep.new } do |input, req, task, value:|
482
+ [{ value: value }, 200]
483
+ end
484
+ end
485
+
486
+ res = async_request(app, :get, '/test')
487
+ data = JSON.parse(res.body, symbolize_names: true)
488
+ assert_equal "from_class", data[:value]
489
+ end
490
+ end
491
+ ```
492
+
493
+ ### Integration Tests
494
+
495
+ Test with real-world scenarios:
496
+ - Database connections
497
+ - Authentication/authorization
498
+ - Rate limiting
499
+ - Caching
500
+
501
+ ## OpenAPI Integration
502
+
503
+ Dependencies should appear in OpenAPI spec when they affect request parameters:
504
+
505
+ ```ruby
506
+ # If dependency uses query params, document them
507
+ api.get '/items',
508
+ query: PaginationSchema,
509
+ depends: { page: Paginator.new } do |input, req, task, page:|
510
+ # The PaginationSchema is documented
511
+ # The page dependency transforms those params
512
+ end
513
+ ```
514
+
515
+ Dependencies themselves don't need to appear in OpenAPI (they're implementation details), but their effects on the API should be documented through schemas.
516
+
517
+ ## Error Handling
518
+
519
+ ```ruby
520
+ # Dependency raises exception
521
+ def require_auth
522
+ ->(req:) {
523
+ token = req.env['HTTP_AUTHORIZATION']
524
+ raise FunApi::HTTPException.new(
525
+ status_code: 401,
526
+ detail: "Not authenticated"
527
+ ) unless token
528
+ verify_token(token)
529
+ }
530
+ end
531
+
532
+ # FunApi should catch and convert to proper HTTP response
533
+ api.get '/protected',
534
+ depends: { user: require_auth } do |input, req, task, user:|
535
+ [{ user: user }, 200]
536
+ end
537
+ ```
538
+
539
+ ## Examples
540
+
541
+ ### Authentication Example
542
+
543
+ ```ruby
544
+ # examples/dependency_auth.rb
545
+ require 'fun_api'
546
+ require 'fun_api/server/falcon'
547
+
548
+ class AuthError < FunApi::HTTPException
549
+ def initialize(detail = "Not authenticated")
550
+ super(status_code: 401, detail: detail)
551
+ end
552
+ end
553
+
554
+ FAKE_DB = {
555
+ "token123" => { id: 1, name: "Alice", email: "alice@example.com" },
556
+ "token456" => { id: 2, name: "Bob", email: "bob@example.com" }
557
+ }
558
+
559
+ def get_current_user
560
+ ->(req:) {
561
+ auth = req.env['HTTP_AUTHORIZATION']
562
+ raise AuthError.new unless auth
563
+
564
+ token = auth.split(' ').last
565
+ user = FAKE_DB[token]
566
+ raise AuthError.new("Invalid token") unless user
567
+
568
+ user
569
+ }
570
+ end
571
+
572
+ def get_admin_user
573
+ ->(user: FunApi::Depends.new(get_current_user)) {
574
+ raise FunApi::HTTPException.new(
575
+ status_code: 403,
576
+ detail: "Not authorized"
577
+ ) unless user[:name] == "Alice"
578
+
579
+ user
580
+ }
581
+ end
582
+
583
+ app = FunApi::App.new(
584
+ title: "Dependency Injection Auth Demo",
585
+ version: "1.0.0"
586
+ ) do |api|
587
+ api.get '/public' do |input, req, task|
588
+ [{ message: "Public endpoint" }, 200]
589
+ end
590
+
591
+ api.get '/profile',
592
+ depends: { user: get_current_user } do |input, req, task, user:|
593
+ [user, 200]
594
+ end
595
+
596
+ api.get '/admin',
597
+ depends: { admin: get_admin_user } do |input, req, task, admin:|
598
+ [{ message: "Admin area", user: admin }, 200]
599
+ end
600
+ end
601
+
602
+ puts "Starting server on http://localhost:3000"
603
+ puts "Try:"
604
+ puts " curl http://localhost:3000/public"
605
+ puts " curl -H 'Authorization: Bearer token123' http://localhost:3000/profile"
606
+ puts " curl -H 'Authorization: Bearer token123' http://localhost:3000/admin"
607
+ puts " curl -H 'Authorization: Bearer token456' http://localhost:3000/admin"
608
+
609
+ FunApi::Server::Falcon.start(app, port: 3000)
610
+ ```
611
+
612
+ ### Database Connection Example
613
+
614
+ ```ruby
615
+ # examples/dependency_database.rb
616
+ class Database
617
+ def self.connect
618
+ new
619
+ end
620
+
621
+ def find_user(id)
622
+ { id: id, name: "User #{id}" }
623
+ end
624
+
625
+ def all_users
626
+ [
627
+ { id: 1, name: "Alice" },
628
+ { id: 2, name: "Bob" }
629
+ ]
630
+ end
631
+ end
632
+
633
+ app = FunApi::App.new do |api|
634
+ db_connection = -> { Database.connect }
635
+
636
+ api.get '/users',
637
+ depends: { db: db_connection } do |input, req, task, db:|
638
+ users = db.all_users
639
+ [users, 200]
640
+ end
641
+
642
+ api.get '/users/:id',
643
+ depends: { db: db_connection } do |input, req, task, db:|
644
+ user = db.find_user(input[:path]['id'].to_i)
645
+ [user, 200]
646
+ end
647
+ end
648
+ ```
649
+
650
+ ### Pagination Example
651
+
652
+ ```ruby
653
+ # examples/dependency_pagination.rb
654
+ class Paginator
655
+ def initialize(max_limit: 100)
656
+ @max_limit = max_limit
657
+ end
658
+
659
+ def call(input:)
660
+ limit = input[:query][:limit] || 10
661
+ offset = input[:query][:offset] || 0
662
+
663
+ {
664
+ limit: [limit.to_i, @max_limit].min,
665
+ offset: offset.to_i
666
+ }
667
+ end
668
+ end
669
+
670
+ QuerySchema = FunApi::Schema.define do
671
+ optional(:limit).filled(:integer)
672
+ optional(:offset).filled(:integer)
673
+ end
674
+
675
+ app = FunApi::App.new do |api|
676
+ pagination = Paginator.new(max_limit: 50)
677
+
678
+ api.get '/items',
679
+ query: QuerySchema,
680
+ depends: { page: pagination } do |input, req, task, page:|
681
+
682
+ items = (1..100).to_a
683
+ paginated = items[page[:offset], page[:limit]]
684
+
685
+ [{
686
+ items: paginated,
687
+ pagination: page,
688
+ total: items.length
689
+ }, 200]
690
+ end
691
+ end
692
+ ```
693
+
694
+ ## Implementation Checklist
695
+
696
+ **Phase 1: Core (MVP)**
697
+ - [ ] Create `FunApi::Depends` class
698
+ - [ ] Add `depends:` parameter to `add_route`
699
+ - [ ] Implement basic dependency resolution
700
+ - [ ] Support proc/lambda dependencies
701
+ - [ ] Write core tests
702
+ - [ ] Update AGENTS.md with new patterns
703
+
704
+ **Phase 2: Advanced**
705
+ - [ ] Support class-based dependencies (callable)
706
+ - [ ] Support nested dependencies
707
+ - [ ] Request context injection (req, input, task)
708
+ - [ ] Write advanced tests
709
+ - [ ] Create example: authentication
710
+ - [ ] Create example: database connection
711
+ - [ ] Create example: pagination
712
+
713
+ **Phase 3: Polish**
714
+ - [ ] Error handling for dependency failures
715
+ - [ ] OpenAPI integration (if applicable)
716
+ - [ ] Performance optimization
717
+ - [ ] Documentation in README.md
718
+ - [ ] Add to DECISIONS.md
719
+
720
+ ## Open Questions
721
+
722
+ 1. **Dependency Caching**: Should dependencies be cached per-request?
723
+ - Probably yes for request-scoped dependencies
724
+ - Need to ensure same dependency called multiple times returns same instance
725
+
726
+ 2. **Dependency with `yield`**: Support setup/teardown pattern?
727
+ - Could be useful for database connections
728
+ - Would need to track lifecycle carefully
729
+
730
+ 3. **Global Dependencies**: Should we support app-level dependencies?
731
+ - FastAPI has this
732
+ - Could be useful for common auth, logging, etc.
733
+
734
+ 4. **Testing Helpers**: How to override dependencies in tests?
735
+ - FastAPI has dependency overrides
736
+ - Could be very useful for testing
737
+
738
+ ## Success Criteria
739
+
740
+ 1. Clean, Ruby-idiomatic API that feels natural
741
+ 2. Supports common use cases: auth, database, pagination
742
+ 3. Works seamlessly with existing FunApi features
743
+ 4. Well-tested with comprehensive test suite
744
+ 5. Clear documentation and examples
745
+ 6. No performance degradation for routes without dependencies
746
+
747
+ ## Future Enhancements
748
+
749
+ - Dependency overrides for testing
750
+ - Background task dependencies
751
+ - Dependency lifecycle hooks (setup/teardown with yield)
752
+ - Global/application-level dependencies
753
+ - Dependency graph visualization/debugging tools