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
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
data/docs
ADDED