funapi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/25-09-01-OPENAPI_IMPLEMENTATION.md +233 -0
- data/.claude/25-09-05-RESPONSE_SCHEMA.md +383 -0
- data/.claude/25-09-10-OPENAPI_PLAN.md +219 -0
- data/.claude/25-10-26-MIDDLEWARE_IMPLEMENTATION.md +230 -0
- data/.claude/25-10-26-MIDDLEWARE_PLAN.md +353 -0
- data/.claude/25-10-27-BACKGROUND_TASKS_ANALYSIS.md +325 -0
- data/.claude/25-10-27-DEPENDENCY_IMPLEMENTATION_SUMMARY.md +325 -0
- data/.claude/25-10-27-DEPENDENCY_INJECTION_PLAN.md +753 -0
- data/.claude/25-12-24-LIFECYCLE_HOOKS_PLAN.md +421 -0
- data/.claude/25-12-24-PUBLISHING_AND_DOGFOODING_PLAN.md +327 -0
- data/.claude/25-12-24-TEMPLATE_RENDERING_PLAN.md +704 -0
- data/.claude/DECISIONS.md +397 -0
- data/.claude/PROJECT_PLAN.md +80 -0
- data/.claude/TESTING_PLAN.md +285 -0
- data/.claude/TESTING_STATUS.md +157 -0
- data/.tool-versions +1 -0
- data/AGENTS.md +416 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +660 -0
- data/Rakefile +10 -0
- data/docs +8 -0
- data/docs-site/.gitignore +3 -0
- data/docs-site/Gemfile +9 -0
- data/docs-site/app.rb +138 -0
- data/docs-site/content/essential/handler.md +156 -0
- data/docs-site/content/essential/lifecycle.md +161 -0
- data/docs-site/content/essential/middleware.md +201 -0
- data/docs-site/content/essential/openapi.md +155 -0
- data/docs-site/content/essential/routing.md +123 -0
- data/docs-site/content/essential/validation.md +166 -0
- data/docs-site/content/getting-started/at-glance.md +82 -0
- data/docs-site/content/getting-started/key-concepts.md +150 -0
- data/docs-site/content/getting-started/quick-start.md +127 -0
- data/docs-site/content/index.md +81 -0
- data/docs-site/content/patterns/async-operations.md +137 -0
- data/docs-site/content/patterns/background-tasks.md +143 -0
- data/docs-site/content/patterns/database.md +175 -0
- data/docs-site/content/patterns/dependencies.md +141 -0
- data/docs-site/content/patterns/deployment.md +212 -0
- data/docs-site/content/patterns/error-handling.md +184 -0
- data/docs-site/content/patterns/response-schema.md +159 -0
- data/docs-site/content/patterns/templates.md +193 -0
- data/docs-site/content/patterns/testing.md +218 -0
- data/docs-site/mise.toml +2 -0
- data/docs-site/public/css/style.css +234 -0
- data/docs-site/templates/layouts/docs.html.erb +28 -0
- data/docs-site/templates/page.html.erb +3 -0
- data/docs-site/templates/partials/_nav.html.erb +19 -0
- data/examples/background_tasks_demo.rb +159 -0
- data/examples/demo_middleware.rb +55 -0
- data/examples/demo_openapi.rb +63 -0
- data/examples/dependency_block_demo.rb +150 -0
- data/examples/dependency_cleanup_demo.rb +146 -0
- data/examples/dependency_injection_demo.rb +200 -0
- data/examples/lifecycle_demo.rb +57 -0
- data/examples/middleware_demo.rb +74 -0
- data/examples/templates/layouts/application.html.erb +66 -0
- data/examples/templates/todos/_todo.html.erb +15 -0
- data/examples/templates/todos/index.html.erb +12 -0
- data/examples/templates_demo.rb +87 -0
- data/lib/funapi/application.rb +521 -0
- data/lib/funapi/async.rb +57 -0
- data/lib/funapi/background_tasks.rb +52 -0
- data/lib/funapi/config.rb +23 -0
- data/lib/funapi/database/sequel/fibered_connection_pool.rb +87 -0
- data/lib/funapi/dependency_wrapper.rb +66 -0
- data/lib/funapi/depends.rb +138 -0
- data/lib/funapi/exceptions.rb +72 -0
- data/lib/funapi/middleware/base.rb +13 -0
- data/lib/funapi/middleware/cors.rb +23 -0
- data/lib/funapi/middleware/request_logger.rb +32 -0
- data/lib/funapi/middleware/trusted_host.rb +34 -0
- data/lib/funapi/middleware.rb +4 -0
- data/lib/funapi/openapi/schema_converter.rb +85 -0
- data/lib/funapi/openapi/spec_generator.rb +179 -0
- data/lib/funapi/router.rb +43 -0
- data/lib/funapi/schema.rb +65 -0
- data/lib/funapi/server/falcon.rb +38 -0
- data/lib/funapi/template_response.rb +17 -0
- data/lib/funapi/templates.rb +111 -0
- data/lib/funapi/version.rb +5 -0
- data/lib/funapi.rb +14 -0
- data/sig/fun_api.rbs +499 -0
- metadata +220 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Quick Start
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Quick Start
|
|
6
|
+
|
|
7
|
+
Get a FunApi application running in under 5 minutes.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add FunApi to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem 'funapi'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then install:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Create Your First App
|
|
24
|
+
|
|
25
|
+
Create a file called `app.rb`:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require 'funapi'
|
|
29
|
+
require 'funapi/server/falcon'
|
|
30
|
+
|
|
31
|
+
app = FunApi::App.new(
|
|
32
|
+
title: "My First API",
|
|
33
|
+
version: "1.0.0"
|
|
34
|
+
) do |api|
|
|
35
|
+
api.get '/hello' do |input, req, task|
|
|
36
|
+
[{ message: 'Hello, World!' }, 200]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
api.get '/hello/:name' do |input, req, task|
|
|
40
|
+
name = input[:path]['name']
|
|
41
|
+
[{ message: "Hello, #{name}!" }, 200]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
FunApi::Server::Falcon.start(app, port: 3000)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Run It
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
ruby app.rb
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You should see:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Falcon listening on 0.0.0.0:3000
|
|
58
|
+
Try: curl http://0.0.0.0:3000/hello
|
|
59
|
+
Press Ctrl+C to stop
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Test It
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
$ curl http://localhost:3000/hello
|
|
66
|
+
{"message":"Hello, World!"}
|
|
67
|
+
|
|
68
|
+
$ curl http://localhost:3000/hello/Ruby
|
|
69
|
+
{"message":"Hello, Ruby!"}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Check the Docs
|
|
73
|
+
|
|
74
|
+
Open your browser to `http://localhost:3000/docs`
|
|
75
|
+
|
|
76
|
+
You'll see interactive Swagger UI documentation automatically generated from your routes.
|
|
77
|
+
|
|
78
|
+
## Add Validation
|
|
79
|
+
|
|
80
|
+
Let's add a POST endpoint with request validation:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
require 'funapi'
|
|
84
|
+
require 'funapi/server/falcon'
|
|
85
|
+
|
|
86
|
+
UserSchema = FunApi::Schema.define do
|
|
87
|
+
required(:name).filled(:string)
|
|
88
|
+
required(:email).filled(:string)
|
|
89
|
+
optional(:age).filled(:integer)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
app = FunApi::App.new(title: "My API") do |api|
|
|
93
|
+
api.get '/hello' do |input, req, task|
|
|
94
|
+
[{ message: 'Hello, World!' }, 200]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
api.post '/users', body: UserSchema do |input, req, task|
|
|
98
|
+
user = input[:body]
|
|
99
|
+
# user is already validated!
|
|
100
|
+
[{ created: user }, 201]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
FunApi::Server::Falcon.start(app, port: 3000)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Test the validation:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Valid request
|
|
111
|
+
$ curl -X POST http://localhost:3000/users \
|
|
112
|
+
-H 'Content-Type: application/json' \
|
|
113
|
+
-d '{"name":"Alice","email":"alice@example.com"}'
|
|
114
|
+
{"created":{"name":"Alice","email":"alice@example.com"}}
|
|
115
|
+
|
|
116
|
+
# Invalid request (missing email)
|
|
117
|
+
$ curl -X POST http://localhost:3000/users \
|
|
118
|
+
-H 'Content-Type: application/json' \
|
|
119
|
+
-d '{"name":"Alice"}'
|
|
120
|
+
{"detail":[{"loc":["body","email"],"msg":"is missing","type":"value_error"}]}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Next Steps
|
|
124
|
+
|
|
125
|
+
- [Key Concepts](/docs/getting-started/key-concepts) - Understand the core ideas
|
|
126
|
+
- [Routing](/docs/essential/routing) - Learn about path parameters and HTTP methods
|
|
127
|
+
- [Validation](/docs/essential/validation) - Deep dive into dry-schema
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: FunApi
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# FunApi
|
|
6
|
+
|
|
7
|
+
A minimal, async-first Ruby web framework inspired by FastAPI.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require 'funapi'
|
|
11
|
+
require 'funapi/server/falcon'
|
|
12
|
+
|
|
13
|
+
UserSchema = FunApi::Schema.define do
|
|
14
|
+
required(:name).filled(:string)
|
|
15
|
+
required(:email).filled(:string)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
app = FunApi::App.new(title: "My API", version: "1.0.0") do |api|
|
|
19
|
+
api.get '/hello' do |input, req, task|
|
|
20
|
+
[{ message: 'Hello, World!' }, 200]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
api.post '/users', body: UserSchema do |input, req, task|
|
|
24
|
+
[{ created: input[:body] }, 201]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
FunApi::Server::Falcon.start(app, port: 3000)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Visit `http://localhost:3000/docs` to see your interactive API documentation.
|
|
32
|
+
|
|
33
|
+
## Key Features
|
|
34
|
+
|
|
35
|
+
- **Async-first** - Built on Falcon and Ruby's Async library for high-performance concurrent operations
|
|
36
|
+
- **Simple validation** - Using dry-schema for straightforward request/response validation
|
|
37
|
+
- **Auto-documentation** - Automatic OpenAPI/Swagger docs generated from your code
|
|
38
|
+
- **Minimal magic** - Clear, explicit APIs without heavy DSLs
|
|
39
|
+
- **Rack-compatible** - Works with any Rack middleware
|
|
40
|
+
|
|
41
|
+
## Standing on Giants
|
|
42
|
+
|
|
43
|
+
FunApi brings together proven Ruby libraries:
|
|
44
|
+
|
|
45
|
+
- **[Falcon](https://github.com/socketry/falcon)** - High-performance async HTTP server
|
|
46
|
+
- **[dry-schema](https://dry-rb.org/gems/dry-schema/)** - Powerful, composable validation
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
Add to your Gemfile:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
gem 'funapi'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then run:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bundle install
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Example
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
require 'funapi'
|
|
66
|
+
require 'funapi/server/falcon'
|
|
67
|
+
|
|
68
|
+
app = FunApi::App.new do |api|
|
|
69
|
+
api.get '/hello/:name' do |input, req, task|
|
|
70
|
+
name = input[:path]['name']
|
|
71
|
+
[{ message: "Hello, #{name}!" }, 200]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
FunApi::Server::Falcon.start(app, port: 3000)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
$ curl http://localhost:3000/hello/world
|
|
80
|
+
{"message":"Hello, world!"}
|
|
81
|
+
```
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Async Operations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Async Operations
|
|
6
|
+
|
|
7
|
+
FunApi is async-first. Every handler receives an `Async::Task` for concurrent operations.
|
|
8
|
+
|
|
9
|
+
## The Task Parameter
|
|
10
|
+
|
|
11
|
+
The third handler parameter is an `Async::Task`:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
api.get '/dashboard' do |input, req, task|
|
|
15
|
+
# task is Async::Task.current
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Concurrent Fetches
|
|
20
|
+
|
|
21
|
+
Run multiple operations in parallel:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
api.get '/dashboard/:id' do |input, req, task|
|
|
25
|
+
id = input[:path]['id']
|
|
26
|
+
|
|
27
|
+
# These run concurrently
|
|
28
|
+
user_task = task.async { fetch_user(id) }
|
|
29
|
+
posts_task = task.async { fetch_posts(id) }
|
|
30
|
+
stats_task = task.async { fetch_stats(id) }
|
|
31
|
+
|
|
32
|
+
# Wait for all to complete
|
|
33
|
+
[{
|
|
34
|
+
user: user_task.wait,
|
|
35
|
+
posts: posts_task.wait,
|
|
36
|
+
stats: stats_task.wait
|
|
37
|
+
}, 200]
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Without async, this would take `time(user) + time(posts) + time(stats)`.
|
|
42
|
+
With async, it takes `max(time(user), time(posts), time(stats))`.
|
|
43
|
+
|
|
44
|
+
## Error Handling
|
|
45
|
+
|
|
46
|
+
Handle errors from async operations:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
api.get '/data' do |input, req, task|
|
|
50
|
+
primary = task.async { fetch_from_primary }
|
|
51
|
+
fallback = task.async { fetch_from_fallback }
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
[{ data: primary.wait }, 200]
|
|
55
|
+
rescue => e
|
|
56
|
+
# Primary failed, use fallback
|
|
57
|
+
[{ data: fallback.wait, source: 'fallback' }, 200]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Timeouts
|
|
63
|
+
|
|
64
|
+
Add timeouts to operations:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
api.get '/external' do |input, req, task|
|
|
68
|
+
result = task.with_timeout(5) do
|
|
69
|
+
fetch_from_slow_api
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
[{ data: result }, 200]
|
|
73
|
+
rescue Async::TimeoutError
|
|
74
|
+
raise FunApi::HTTPException.new(
|
|
75
|
+
status_code: 504,
|
|
76
|
+
detail: "External API timeout"
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Sleep
|
|
82
|
+
|
|
83
|
+
Use `Kernel#sleep` (not `task.sleep`):
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
api.get '/delayed' do |input, req, task|
|
|
87
|
+
sleep(1) # Non-blocking in async context
|
|
88
|
+
[{ message: 'Done' }, 200]
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Real-World Example
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
api.get '/user/:id/feed' do |input, req, task|
|
|
96
|
+
user_id = input[:path]['id']
|
|
97
|
+
|
|
98
|
+
# Fetch user and check permissions first
|
|
99
|
+
user = fetch_user(user_id)
|
|
100
|
+
raise FunApi::HTTPException.new(status_code: 404) unless user
|
|
101
|
+
|
|
102
|
+
# Then fetch feed data concurrently
|
|
103
|
+
posts = task.async { Post.where(user_id: user_id).limit(20) }
|
|
104
|
+
notifications = task.async { Notification.unread(user_id) }
|
|
105
|
+
suggestions = task.async { RecommendationService.for(user_id) }
|
|
106
|
+
|
|
107
|
+
[{
|
|
108
|
+
user: user,
|
|
109
|
+
posts: posts.wait,
|
|
110
|
+
notifications: notifications.wait,
|
|
111
|
+
suggestions: suggestions.wait
|
|
112
|
+
}, 200]
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## When to Use Async
|
|
117
|
+
|
|
118
|
+
**Good candidates:**
|
|
119
|
+
- Multiple independent database queries
|
|
120
|
+
- External API calls
|
|
121
|
+
- File I/O operations
|
|
122
|
+
- Any I/O-bound work
|
|
123
|
+
|
|
124
|
+
**Not needed for:**
|
|
125
|
+
- CPU-bound calculations
|
|
126
|
+
- Single database query
|
|
127
|
+
- Simple transformations
|
|
128
|
+
|
|
129
|
+
## Technical Details
|
|
130
|
+
|
|
131
|
+
FunApi uses Ruby's [Async](https://github.com/socketry/async) library and [Falcon](https://github.com/socketry/falcon) server. The task parameter is the current `Async::Task`, giving you access to the full Async API.
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
# These are equivalent
|
|
135
|
+
task.async { work }
|
|
136
|
+
Async::Task.current.async { work }
|
|
137
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Background Tasks
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Background Tasks
|
|
6
|
+
|
|
7
|
+
Execute tasks after the response is sent to the client.
|
|
8
|
+
|
|
9
|
+
## Basic Usage
|
|
10
|
+
|
|
11
|
+
Request the `background:` parameter in your handler:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
api.post '/signup', body: UserSchema do |input, req, task, background:|
|
|
15
|
+
user = create_user(input[:body])
|
|
16
|
+
|
|
17
|
+
# These run AFTER the response is sent
|
|
18
|
+
background.add_task(method(:send_welcome_email), user[:email])
|
|
19
|
+
background.add_task(method(:log_signup), user[:id])
|
|
20
|
+
|
|
21
|
+
[{ user: user }, 201]
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The client receives the response immediately. The tasks execute afterward.
|
|
26
|
+
|
|
27
|
+
## Adding Tasks
|
|
28
|
+
|
|
29
|
+
### With Method References
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
def send_email(to, subject)
|
|
33
|
+
# send email
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
background.add_task(method(:send_email), "user@example.com", "Welcome!")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### With Lambdas
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
background.add_task(->(email) { Mailer.send(email) }, user[:email])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### With Keyword Arguments
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
background.add_task(
|
|
49
|
+
->(to:, subject:) { send_email(to: to, subject: subject) },
|
|
50
|
+
to: "user@example.com",
|
|
51
|
+
subject: "Welcome!"
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## With Dependencies
|
|
56
|
+
|
|
57
|
+
Dependencies are available to background tasks:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
api.register(:mailer) { Mailer.new }
|
|
61
|
+
api.register(:analytics) { Analytics.new }
|
|
62
|
+
|
|
63
|
+
api.post '/signup', depends: [:mailer, :analytics] do |input, req, task, mailer:, analytics:, background:|
|
|
64
|
+
user = create_user(input[:body])
|
|
65
|
+
|
|
66
|
+
# Dependencies captured in closure
|
|
67
|
+
background.add_task(lambda {
|
|
68
|
+
mailer.send_welcome(user[:email])
|
|
69
|
+
analytics.track('signup', user[:id])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
[{ user: user }, 201]
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Error Handling
|
|
77
|
+
|
|
78
|
+
Background task errors are logged but don't affect the response:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
background.add_task(lambda {
|
|
82
|
+
raise "Task failed!"
|
|
83
|
+
# Logged as warning, doesn't crash the server
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## When to Use
|
|
88
|
+
|
|
89
|
+
**Good for:**
|
|
90
|
+
- Email notifications
|
|
91
|
+
- Logging and analytics
|
|
92
|
+
- Cache warming
|
|
93
|
+
- Simple webhook calls
|
|
94
|
+
- Audit trail recording
|
|
95
|
+
|
|
96
|
+
**Not for:**
|
|
97
|
+
- Long-running jobs (> 30 seconds)
|
|
98
|
+
- Jobs requiring persistence
|
|
99
|
+
- Jobs that must survive restarts
|
|
100
|
+
- Jobs needing retries
|
|
101
|
+
|
|
102
|
+
For complex jobs, use a proper job queue like Sidekiq or GoodJob.
|
|
103
|
+
|
|
104
|
+
## Complete Example
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
require 'funapi'
|
|
108
|
+
require 'funapi/server/falcon'
|
|
109
|
+
|
|
110
|
+
def send_welcome_email(email)
|
|
111
|
+
puts "Sending welcome email to #{email}"
|
|
112
|
+
# Actually send email
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def log_signup(user_id)
|
|
116
|
+
puts "Logging signup for user #{user_id}"
|
|
117
|
+
# Log to analytics
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def notify_admin(user)
|
|
121
|
+
puts "Notifying admin about new user: #{user[:name]}"
|
|
122
|
+
# Send Slack notification
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
app = FunApi::App.new do |api|
|
|
126
|
+
UserSchema = FunApi::Schema.define do
|
|
127
|
+
required(:name).filled(:string)
|
|
128
|
+
required(:email).filled(:string)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
api.post '/signup', body: UserSchema do |input, req, task, background:|
|
|
132
|
+
user = { id: rand(1000), **input[:body] }
|
|
133
|
+
|
|
134
|
+
background.add_task(method(:send_welcome_email), user[:email])
|
|
135
|
+
background.add_task(method(:log_signup), user[:id])
|
|
136
|
+
background.add_task(method(:notify_admin), user)
|
|
137
|
+
|
|
138
|
+
[{ user: user, message: 'Check your email!' }, 201]
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
FunApi::Server::Falcon.start(app, port: 3000)
|
|
143
|
+
```
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Database
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Database
|
|
6
|
+
|
|
7
|
+
FunApi works with any database library. Here's how to integrate common options.
|
|
8
|
+
|
|
9
|
+
## With db-postgres
|
|
10
|
+
|
|
11
|
+
[db-postgres](https://github.com/socketry/db-postgres) is async-native and works great with FunApi:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require 'funapi'
|
|
15
|
+
require 'db/postgres'
|
|
16
|
+
require 'funapi/server/falcon'
|
|
17
|
+
|
|
18
|
+
app = FunApi::App.new do |api|
|
|
19
|
+
api.on_startup do
|
|
20
|
+
$db = DB::Postgres::Connection.new(
|
|
21
|
+
host: 'localhost',
|
|
22
|
+
database: 'myapp',
|
|
23
|
+
user: 'postgres'
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
api.on_shutdown do
|
|
28
|
+
$db&.close
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
api.get '/users' do |input, req, task|
|
|
32
|
+
result = $db.query("SELECT id, name, email FROM users")
|
|
33
|
+
[{ users: result.to_a }, 200]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## With Sequel
|
|
39
|
+
|
|
40
|
+
[Sequel](https://sequel.jeremyevans.net/) is a powerful database toolkit:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require 'funapi'
|
|
44
|
+
require 'sequel'
|
|
45
|
+
require 'funapi/server/falcon'
|
|
46
|
+
|
|
47
|
+
DB = Sequel.connect(ENV['DATABASE_URL'])
|
|
48
|
+
|
|
49
|
+
app = FunApi::App.new do |api|
|
|
50
|
+
api.get '/users' do |input, req, task|
|
|
51
|
+
users = DB[:users].all
|
|
52
|
+
[{ users: users }, 200]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
api.get '/users/:id' do |input, req, task|
|
|
56
|
+
user = DB[:users].where(id: input[:path]['id']).first
|
|
57
|
+
raise FunApi::HTTPException.new(status_code: 404) unless user
|
|
58
|
+
[{ user: user }, 200]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
api.post '/users', body: UserSchema do |input, req, task|
|
|
62
|
+
id = DB[:users].insert(input[:body])
|
|
63
|
+
user = DB[:users].where(id: id).first
|
|
64
|
+
[{ user: user }, 201]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## With ActiveRecord
|
|
70
|
+
|
|
71
|
+
You can use ActiveRecord standalone (without Rails):
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
require 'funapi'
|
|
75
|
+
require 'active_record'
|
|
76
|
+
require 'funapi/server/falcon'
|
|
77
|
+
|
|
78
|
+
ActiveRecord::Base.establish_connection(ENV['DATABASE_URL'])
|
|
79
|
+
|
|
80
|
+
class User < ActiveRecord::Base
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
app = FunApi::App.new do |api|
|
|
84
|
+
api.get '/users' do |input, req, task|
|
|
85
|
+
users = User.all.map(&:attributes)
|
|
86
|
+
[{ users: users }, 200]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
api.post '/users', body: UserSchema do |input, req, task|
|
|
90
|
+
user = User.create!(input[:body])
|
|
91
|
+
[{ user: user.attributes }, 201]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Connection Pooling with Dependencies
|
|
97
|
+
|
|
98
|
+
Use dependency injection for connection management:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
require 'connection_pool'
|
|
102
|
+
|
|
103
|
+
app = FunApi::App.new do |api|
|
|
104
|
+
pool = ConnectionPool.new(size: 10) do
|
|
105
|
+
PG.connect(ENV['DATABASE_URL'])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
api.register(:db) do
|
|
109
|
+
conn = pool.checkout
|
|
110
|
+
[conn, -> { pool.checkin(conn) }]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
api.get '/users', depends: [:db] do |input, req, task, db:|
|
|
114
|
+
result = db.exec("SELECT * FROM users")
|
|
115
|
+
[{ users: result.to_a }, 200]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Transactions
|
|
121
|
+
|
|
122
|
+
### With Block Dependencies
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
api.register(:transaction) do |yielder|
|
|
126
|
+
conn = PG.connect(ENV['DATABASE_URL'])
|
|
127
|
+
conn.exec("BEGIN")
|
|
128
|
+
|
|
129
|
+
yielder.call(conn)
|
|
130
|
+
|
|
131
|
+
conn.exec("COMMIT")
|
|
132
|
+
rescue
|
|
133
|
+
conn.exec("ROLLBACK")
|
|
134
|
+
raise
|
|
135
|
+
ensure
|
|
136
|
+
conn.close
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
api.post '/transfer', depends: [:transaction] do |input, req, task, transaction:|
|
|
140
|
+
transaction.exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2",
|
|
141
|
+
[input[:body][:amount], input[:body][:from_id]])
|
|
142
|
+
transaction.exec("UPDATE accounts SET balance = balance + $1 WHERE id = $2",
|
|
143
|
+
[input[:body][:amount], input[:body][:to_id]])
|
|
144
|
+
|
|
145
|
+
[{ success: true }, 200]
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Async Queries
|
|
150
|
+
|
|
151
|
+
With async-compatible drivers, run queries concurrently:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
api.get '/dashboard/:id' do |input, req, task|
|
|
155
|
+
id = input[:path]['id']
|
|
156
|
+
|
|
157
|
+
user = task.async { DB[:users].where(id: id).first }
|
|
158
|
+
posts = task.async { DB[:posts].where(user_id: id).limit(10).all }
|
|
159
|
+
stats = task.async { DB[:stats].where(user_id: id).first }
|
|
160
|
+
|
|
161
|
+
[{
|
|
162
|
+
user: user.wait,
|
|
163
|
+
posts: posts.wait,
|
|
164
|
+
stats: stats.wait
|
|
165
|
+
}, 200]
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Migrations
|
|
170
|
+
|
|
171
|
+
FunApi doesn't include migrations. Use your database library's migration tool:
|
|
172
|
+
|
|
173
|
+
- Sequel: `sequel -m migrations/ postgres://...`
|
|
174
|
+
- ActiveRecord: `rake db:migrate`
|
|
175
|
+
- Raw SQL files with a migration runner
|