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
data/README.md ADDED
@@ -0,0 +1,660 @@
1
+ # FunApi
2
+
3
+ A minimal, async-first Ruby web framework inspired by FastAPI. Built on top of Falcon and dry-schema, FunApi provides a simple, performant way to build web APIs in Ruby with a focus on developer experience.
4
+
5
+ ## Philosophy
6
+
7
+ FunApi aims to bring FastAPI's excellent developer experience to Ruby by providing:
8
+
9
+ - **Async-first**: Built on Ruby's Async library and Falcon server for high-performance concurrent operations
10
+ - **Simple validation**: Using dry-schema for straightforward request validation
11
+ - **Minimal magic**: Clear, explicit APIs without heavy DSLs
12
+ - **Easy to start**: Get an API up and running in minutes
13
+ - **Auto-documentation**: Automatic OpenAPI/Swagger documentation generation
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'funapi'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```bash
26
+ ./bin/bundle install
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```ruby
32
+ require 'funapi'
33
+ require 'funapi/server/falcon'
34
+
35
+ UserSchema = FunApi::Schema.define do
36
+ required(:name).filled(:string)
37
+ required(:email).filled(:string)
38
+ end
39
+
40
+ app = FunApi::App.new(
41
+ title: "My API",
42
+ version: "1.0.0",
43
+ description: "A simple API example"
44
+ ) do |api|
45
+ api.get '/hello' do |input, req, task|
46
+ [{ message: 'Hello, World!' }, 200]
47
+ end
48
+
49
+ api.post '/users', body: UserSchema do |input, req, task|
50
+ user = input[:body]
51
+ [{ created: user }, 201]
52
+ end
53
+ end
54
+
55
+ FunApi::Server::Falcon.start(app, port: 9292)
56
+ ```
57
+
58
+ Visit http://localhost:9292/docs to see your interactive API documentation!
59
+
60
+ ## Core Features
61
+
62
+ ### 1. Async-First Request Handling
63
+
64
+ All route handlers receive the current `Async::Task` as the third parameter, enabling true concurrent execution within your routes:
65
+
66
+ ```ruby
67
+ api.get '/dashboard/:id' do |input, req, task|
68
+ user_id = input[:path]['id']
69
+
70
+ user_task = task.async { fetch_user_data(user_id) }
71
+ posts_task = task.async { fetch_user_posts(user_id) }
72
+ stats_task = task.async { fetch_user_stats(user_id) }
73
+
74
+ data = {
75
+ user: user_task.wait,
76
+ posts: posts_task.wait,
77
+ stats: stats_task.wait
78
+ }
79
+
80
+ [{ dashboard: data }, 200]
81
+ end
82
+ ```
83
+
84
+ ### 2. Request Validation
85
+
86
+ FastAPI-style request validation using dry-schema:
87
+
88
+ ```ruby
89
+ UserCreateSchema = FunApi::Schema.define do
90
+ required(:name).filled(:string)
91
+ required(:email).filled(:string)
92
+ required(:password).filled(:string)
93
+ optional(:age).filled(:integer)
94
+ end
95
+
96
+ QuerySchema = FunApi::Schema.define do
97
+ optional(:limit).filled(:integer)
98
+ optional(:offset).filled(:integer)
99
+ end
100
+
101
+ app = FunApi::App.new do |api|
102
+ api.get '/hello', query: QuerySchema do |input, req, task|
103
+ name = input[:query][:name] || 'World'
104
+ [{ msg: "Hello, #{name}!" }, 200]
105
+ end
106
+
107
+ api.post '/users', body: UserCreateSchema do |input, req, task|
108
+ user = input[:body]
109
+ [{ created: user }, 201]
110
+ end
111
+
112
+ api.post '/users/batch', body: [UserCreateSchema] do |input, req, task|
113
+ users = input[:body].map { |u| create_user(u) }
114
+ [users, 201]
115
+ end
116
+ end
117
+ ```
118
+
119
+ ### 3. Response Schema Validation & Filtering
120
+
121
+ Automatically validate and filter response data, similar to FastAPI's `response_model`:
122
+
123
+ ```ruby
124
+ UserOutputSchema = FunApi::Schema.define do
125
+ required(:id).filled(:integer)
126
+ required(:name).filled(:string)
127
+ required(:email).filled(:string)
128
+ optional(:age).filled(:integer)
129
+ end
130
+
131
+ app = FunApi::App.new do |api|
132
+ api.post '/users',
133
+ body: UserCreateSchema,
134
+ response_schema: UserOutputSchema do |input, req, task|
135
+
136
+ user = {
137
+ id: 1,
138
+ name: input[:body][:name],
139
+ email: input[:body][:email],
140
+ password: input[:body][:password],
141
+ age: input[:body][:age]
142
+ }
143
+
144
+ [user, 201]
145
+ end
146
+
147
+ api.get '/users',
148
+ response_schema: [UserOutputSchema] do |input, req, task|
149
+ users = fetch_all_users()
150
+ [users, 200]
151
+ end
152
+ end
153
+ ```
154
+
155
+ ### 4. Automatic OpenAPI Documentation
156
+
157
+ FunApi automatically generates OpenAPI 3.0 specifications from your route definitions and schemas:
158
+
159
+ ```ruby
160
+ app = FunApi::App.new(
161
+ title: "User Management API",
162
+ version: "1.0.0",
163
+ description: "A comprehensive user management system"
164
+ ) do |api|
165
+ api.get '/users', query: QuerySchema, response_schema: [UserOutputSchema] do |input, req, task|
166
+ [fetch_users(input[:query]), 200]
167
+ end
168
+
169
+ api.post '/users', body: UserCreateSchema, response_schema: UserOutputSchema do |input, req, task|
170
+ [create_user(input[:body]), 201]
171
+ end
172
+ end
173
+
174
+ FunApi::Server::Falcon.start(app, port: 9292)
175
+ ```
176
+
177
+ Once running, you can access:
178
+ - **Interactive docs**: http://localhost:9292/docs (Swagger UI)
179
+ - **OpenAPI spec**: http://localhost:9292/openapi.json
180
+
181
+ The documentation is automatically generated from:
182
+ - Route paths and HTTP methods
183
+ - Path parameters (`:id` → `{id}`)
184
+ - Query parameter schemas
185
+ - Request body schemas
186
+ - Response schemas
187
+ - Schema names (from constant names)
188
+
189
+ ### 5. FastAPI-Style Error Handling
190
+
191
+ Validation errors return detailed, structured responses:
192
+
193
+ ```json
194
+ {
195
+ "detail": [
196
+ {
197
+ "loc": ["body", "email"],
198
+ "msg": "is missing",
199
+ "type": "value_error"
200
+ }
201
+ ]
202
+ }
203
+ ```
204
+
205
+ Custom exceptions with proper HTTP status codes:
206
+
207
+ ```ruby
208
+ raise FunApi::HTTPException.new(status_code: 404, detail: "User not found")
209
+ raise FunApi::ValidationError.new(errors: schema_errors)
210
+ ```
211
+
212
+ ### 6. Middleware Support
213
+
214
+ FunApi supports both standard Rack middleware and provides FastAPI-style convenience methods for common use cases.
215
+
216
+ #### Built-in Middleware
217
+
218
+ ```ruby
219
+ app = FunApi::App.new do |api|
220
+ api.add_cors(
221
+ allow_origins: ['http://localhost:3000'],
222
+ allow_methods: ['GET', 'POST', 'PUT', 'DELETE'],
223
+ allow_headers: ['Content-Type', 'Authorization']
224
+ )
225
+
226
+ api.add_request_logger
227
+
228
+ api.add_trusted_host(
229
+ allowed_hosts: ['localhost', '127.0.0.1', /\.example\.com$/]
230
+ )
231
+
232
+ api.add_gzip
233
+ end
234
+ ```
235
+
236
+ #### Using Standard Rack Middleware
237
+
238
+ Any Rack middleware works out of the box:
239
+
240
+ ```ruby
241
+ app = FunApi::App.new do |api|
242
+ api.use Rack::Attack
243
+ api.use Rack::ETag
244
+ api.use Rack::Session::Cookie, secret: 'your_secret'
245
+
246
+ api.get '/protected' do |input, req, task|
247
+ [{ data: 'Protected resource' }, 200]
248
+ end
249
+ end
250
+ ```
251
+
252
+ #### Custom Middleware
253
+
254
+ Create your own middleware following the Rack pattern:
255
+
256
+ ```ruby
257
+ class MyCustomMiddleware
258
+ def initialize(app)
259
+ @app = app
260
+ end
261
+
262
+ def call(env)
263
+ status, headers, body = @app.call(env)
264
+ headers['X-Custom-Header'] = 'my-value'
265
+ [status, headers, body]
266
+ end
267
+ end
268
+
269
+ app.use MyCustomMiddleware
270
+ ```
271
+
272
+ ### 7. Input Structure
273
+
274
+ All route handlers receive a unified `input` hash:
275
+
276
+ ```ruby
277
+ {
278
+ path: { id: "123" },
279
+ query: { name: "John" },
280
+ body: { email: "..." }
281
+ }
282
+ ```
283
+
284
+ ### 8. Background Tasks
285
+
286
+ Execute tasks after the response is sent, perfect for emails, logging, and webhooks:
287
+
288
+ ```ruby
289
+ api.post '/signup', body: UserSchema do |input, req, task, background:|
290
+ user = create_user(input[:body])
291
+
292
+ # Tasks execute AFTER response is sent but BEFORE dependencies close
293
+ background.add_task(method(:send_welcome_email), user[:email])
294
+ background.add_task(method(:log_signup_event), user[:id])
295
+ background.add_task(method(:notify_admin), user)
296
+
297
+ [{ user: user, message: 'Signup successful!' }, 201]
298
+ end
299
+ ```
300
+
301
+ **Key Benefits:**
302
+ - ✅ Response sent immediately to client
303
+ - ✅ Tasks run after handler completes
304
+ - ✅ Dependencies still available to tasks
305
+ - ✅ Multiple tasks execute in order
306
+ - ✅ Errors are handled gracefully
307
+
308
+ **Perfect for:**
309
+ - Email notifications
310
+ - Logging and analytics
311
+ - Cache warming
312
+ - Simple webhook calls
313
+ - Audit trail recording
314
+
315
+ **Not for:**
316
+ - Long-running jobs (> 30 seconds)
317
+ - Jobs requiring persistence/retries
318
+ - Jobs that must survive server restart
319
+ → Use Sidekiq, GoodJob, or Que for these cases
320
+
321
+ **With callable objects:**
322
+
323
+ ```ruby
324
+ # Lambda
325
+ background.add_task(->(email) { send_email(email) }, user[:email])
326
+
327
+ # Proc
328
+ background.add_task(proc { |id| log_event(id) }, user[:id])
329
+
330
+ # Method reference
331
+ background.add_task(method(:send_email), user[:email])
332
+ ```
333
+
334
+ **With arguments:**
335
+
336
+ ```ruby
337
+ # Positional arguments
338
+ background.add_task(->(a, b) { sum(a, b) }, 5, 3)
339
+
340
+ # Keyword arguments
341
+ background.add_task(->(name:, age:) { greet(name, age) }, name: 'Alice', age: 30)
342
+
343
+ # Mixed
344
+ background.add_task(->(msg, to:) { send(msg, to) }, 'Hello', to: 'user@example.com')
345
+ ```
346
+
347
+ **Access dependencies in background tasks:**
348
+
349
+ ```ruby
350
+ api.register(:mailer) { Mailer.new }
351
+ api.register(:logger) { Logger.new }
352
+
353
+ api.post '/signup', depends: [:mailer, :logger] do |input, req, task, mailer:, logger:, background:|
354
+ user = create_user(input[:body])
355
+
356
+ # Dependencies captured in closure, available to background tasks
357
+ background.add_task(lambda {
358
+ mailer.send_welcome(user[:email])
359
+ logger.info("Welcome email sent to #{user[:email]}")
360
+ })
361
+
362
+ [{ user: user }, 201]
363
+ end
364
+ ```
365
+
366
+ ### 9. Template Rendering
367
+
368
+ Render ERB templates for HTML responses, perfect for HTMX-style applications:
369
+
370
+ ```ruby
371
+ require 'funapi'
372
+ require 'funapi/templates'
373
+
374
+ templates = FunApi::Templates.new(directory: 'templates')
375
+
376
+ app = FunApi::App.new do |api|
377
+ api.get '/' do |input, req, task|
378
+ templates.response('index.html.erb', title: 'Home', message: 'Welcome!')
379
+ end
380
+
381
+ api.get '/users/:id' do |input, req, task|
382
+ user = fetch_user(input[:path]['id'])
383
+ templates.response('user.html.erb', user: user)
384
+ end
385
+ end
386
+ ```
387
+
388
+ #### Layouts
389
+
390
+ Use layouts to wrap your templates with common HTML structure:
391
+
392
+ ```ruby
393
+ templates = FunApi::Templates.new(
394
+ directory: 'templates',
395
+ layout: 'layouts/application.html.erb'
396
+ )
397
+
398
+ api.get '/' do |input, req, task|
399
+ templates.response('home.html.erb', title: 'Home')
400
+ end
401
+
402
+ # Disable layout for partials/HTMX responses
403
+ api.post '/items' do |input, req, task|
404
+ item = create_item(input[:body])
405
+ templates.response('items/_item.html.erb', layout: false, item: item, status: 201)
406
+ end
407
+ ```
408
+
409
+ Use `with_layout` to create a scoped templates object for route groups:
410
+
411
+ ```ruby
412
+ templates = FunApi::Templates.new(directory: 'templates')
413
+
414
+ # Create scoped templates for different sections
415
+ public_templates = templates.with_layout('layouts/public.html.erb')
416
+ admin_templates = templates.with_layout('layouts/admin.html.erb')
417
+
418
+ api.get '/' do |input, req, task|
419
+ public_templates.response('home.html.erb', title: 'Home')
420
+ end
421
+
422
+ api.get '/admin' do |input, req, task|
423
+ admin_templates.response('admin/dashboard.html.erb', title: 'Dashboard')
424
+ end
425
+ ```
426
+
427
+ Layout template with `yield_content`:
428
+
429
+ ```erb
430
+ <!-- templates/layouts/application.html.erb -->
431
+ <!DOCTYPE html>
432
+ <html>
433
+ <head>
434
+ <title><%= title %></title>
435
+ </head>
436
+ <body>
437
+ <%= yield_content %>
438
+ </body>
439
+ </html>
440
+ ```
441
+
442
+ #### Partials
443
+
444
+ Render partials within templates using `render_partial`:
445
+
446
+ ```erb
447
+ <!-- templates/items/index.html.erb -->
448
+ <ul>
449
+ <% items.each do |item| %>
450
+ <%= render_partial('items/_item.html.erb', item: item) %>
451
+ <% end %>
452
+ </ul>
453
+ ```
454
+
455
+ #### With HTMX
456
+
457
+ FunApi templates work great with HTMX for dynamic HTML updates:
458
+
459
+ ```ruby
460
+ api.get '/items' do |input, req, task|
461
+ items = fetch_items
462
+ templates.response('items/index.html.erb', items: items)
463
+ end
464
+
465
+ api.post '/items', body: ItemSchema do |input, req, task|
466
+ item = create_item(input[:body])
467
+ # Return partial for HTMX to insert
468
+ templates.response('items/_item.html.erb', layout: false, item: item, status: 201)
469
+ end
470
+
471
+ api.delete '/items/:id' do |input, req, task|
472
+ delete_item(input[:path]['id'])
473
+ # Return empty response for HTMX delete
474
+ FunApi::TemplateResponse.new('')
475
+ end
476
+ ```
477
+
478
+ ```erb
479
+ <!-- With HTMX attributes -->
480
+ <form hx-post="/items" hx-target="#items" hx-swap="beforeend">
481
+ <input name="title" placeholder="New item">
482
+ <button type="submit">Add</button>
483
+ </form>
484
+ ```
485
+
486
+ See `examples/templates_demo.rb` for a complete HTMX todo app example.
487
+
488
+ ### 10. Lifecycle Hooks
489
+
490
+ Execute code when the application starts up or shuts down:
491
+
492
+ ```ruby
493
+ app = FunApi::App.new do |api|
494
+ api.on_startup do
495
+ puts "Connecting to database..."
496
+ DB.connect
497
+ end
498
+
499
+ api.on_startup do
500
+ puts "Warming cache..."
501
+ Cache.warm
502
+ end
503
+
504
+ api.on_shutdown do
505
+ puts "Disconnecting..."
506
+ DB.disconnect
507
+ end
508
+ end
509
+ ```
510
+
511
+ **Key behaviors:**
512
+ - Multiple hooks supported (executed in registration order)
513
+ - Startup hooks run before server accepts requests
514
+ - Shutdown hooks run after server stops accepting requests
515
+ - Startup errors prevent server from starting
516
+ - Shutdown errors are logged but don't prevent other hooks from running
517
+
518
+ **Use cases:**
519
+ - Database connection pool initialization
520
+ - Cache warming
521
+ - Background task supervisor setup
522
+ - Metrics/logging initialization
523
+ - Graceful resource cleanup
524
+
525
+ See `examples/lifecycle_demo.rb` for a complete example.
526
+
527
+ ## Complete Example
528
+
529
+ ```ruby
530
+ require 'funapi'
531
+ require 'funapi/server/falcon'
532
+
533
+ UserCreateSchema = FunApi::Schema.define do
534
+ required(:name).filled(:string)
535
+ required(:email).filled(:string)
536
+ required(:password).filled(:string)
537
+ optional(:age).filled(:integer)
538
+ end
539
+
540
+ UserOutputSchema = FunApi::Schema.define do
541
+ required(:id).filled(:integer)
542
+ required(:name).filled(:string)
543
+ required(:email).filled(:string)
544
+ optional(:age).filled(:integer)
545
+ end
546
+
547
+ QuerySchema = FunApi::Schema.define do
548
+ optional(:limit).filled(:integer)
549
+ optional(:offset).filled(:integer)
550
+ end
551
+
552
+ app = FunApi::App.new(
553
+ title: "User Management API",
554
+ version: "1.0.0",
555
+ description: "A simple user management API"
556
+ ) do |api|
557
+ api.add_cors(allow_origins: ['*'])
558
+ api.add_request_logger
559
+
560
+ api.get '/users', query: QuerySchema, response_schema: [UserOutputSchema] do |input, req, task|
561
+ users = [
562
+ { id: 1, name: 'John Doe', email: 'john@example.com', age: 30 },
563
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
564
+ ]
565
+ [users, 200]
566
+ end
567
+
568
+ api.get '/users/:id', response_schema: UserOutputSchema do |input, req, task|
569
+ user_id = input[:path]['id']
570
+ user = { id: user_id.to_i, name: 'John Doe', email: 'john@example.com', age: 30 }
571
+ [user, 200]
572
+ end
573
+
574
+ api.post '/users', body: UserCreateSchema, response_schema: UserOutputSchema do |input, req, task|
575
+ user = input[:body].merge(id: rand(1000))
576
+ [user, 201]
577
+ end
578
+
579
+ api.get '/dashboard/:id' do |input, req, task|
580
+ user_id = input[:path]['id']
581
+
582
+ user_task = task.async { fetch_user(user_id) }
583
+ posts_task = task.async { fetch_posts(user_id) }
584
+ stats_task = task.async { fetch_stats(user_id) }
585
+
586
+ data = {
587
+ user: user_task.wait,
588
+ posts: posts_task.wait,
589
+ stats: stats_task.wait
590
+ }
591
+
592
+ [{ dashboard: data }, 200]
593
+ end
594
+ end
595
+
596
+ FunApi::Server::Falcon.start(app, port: 9292)
597
+ ```
598
+
599
+ ## Architecture
600
+
601
+ - **Router**: Simple pattern-based routing with path parameter extraction
602
+ - **Async Helpers**: Wrapper around Ruby's Async library for concurrent operations
603
+ - **Schema**: Thin wrapper around dry-schema for validation
604
+ - **Exceptions**: FastAPI-inspired exception classes with proper HTTP responses
605
+ - **Server**: Falcon-based async HTTP server
606
+ - **OpenAPI**: Automatic OpenAPI 3.0 specification generation from routes and schemas
607
+
608
+ ## Dependencies
609
+
610
+ - **rack** (>= 3.0.0): Web server interface
611
+ - **async** (>= 2.8): Async/await and concurrency primitives
612
+ - **dry-schema** (>= 1.13): Schema validation
613
+ - **falcon** (>= 0.44): High-performance async HTTP server
614
+
615
+ ## Design Goals
616
+
617
+ 1. **Performance**: Leverage Ruby's async capabilities for concurrent operations
618
+ 2. **Simplicity**: Minimal API surface, easy to learn
619
+ 3. **Explicitness**: No hidden magic, clear separation of concerns
620
+ 4. **Type Safety**: Validation at the edges using dry-schema
621
+ 5. **FastAPI-inspired**: Bring the best ideas from Python's FastAPI to Ruby
622
+
623
+ ## Current Status
624
+
625
+ Active development. Core features implemented:
626
+ - ✅ Async-first request handling with Async::Task
627
+ - ✅ Route definition with path params
628
+ - ✅ Request validation (body/query) with array support
629
+ - ✅ Response schema validation and filtering
630
+ - ✅ FastAPI-style error responses
631
+ - ✅ Falcon server integration
632
+ - ✅ OpenAPI/Swagger documentation generation
633
+ - ✅ Middleware support (Rack-compatible + convenience methods)
634
+ - ✅ Dependency injection with cleanup
635
+ - ✅ Background tasks (post-response execution)
636
+ - ✅ Template rendering (ERB with layouts and partials)
637
+ - ✅ Lifecycle hooks (startup/shutdown)
638
+
639
+ ## Future Enhancements
640
+
641
+ - ~~Dependency injection system~~ ✅ Implemented
642
+ - ~~Background tasks~~ ✅ Implemented
643
+ - ~~Template rendering~~ ✅ Implemented
644
+ - ~~Lifecycle hooks (startup/shutdown)~~ ✅ Implemented
645
+ - Path parameter type validation
646
+ - Response schema options (exclude_unset, include, exclude)
647
+ - WebSocket support
648
+ - Content negotiation (JSON, XML, etc.)
649
+
650
+ ## Development
651
+
652
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
653
+
654
+ ## Contributing
655
+
656
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/fun_api.
657
+
658
+ ## License
659
+
660
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
data/docs ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ # Run the FunApi docs site
3
+
4
+ cd "$(dirname "$0")/docs-site" || exit 1
5
+
6
+ # Use docs-site's own Gemfile
7
+ BUNDLE_GEMFILE="$PWD/Gemfile" bundle install
8
+ BUNDLE_GEMFILE="$PWD/Gemfile" bundle exec ruby app.rb "$@"
@@ -0,0 +1,3 @@
1
+ /vendor/bundle/
2
+ /.bundle/
3
+ Gemfile.lock
data/docs-site/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "funapi", path: ".."
6
+ gem "kramdown"
7
+ gem "kramdown-parser-gfm"
8
+ gem "rouge"
9
+ gem "erb"