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/docs-site/app.rb
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "funapi"
|
|
4
|
+
require "funapi/templates"
|
|
5
|
+
require "funapi/server/falcon"
|
|
6
|
+
require "kramdown"
|
|
7
|
+
require "kramdown-parser-gfm"
|
|
8
|
+
require "rouge"
|
|
9
|
+
|
|
10
|
+
class DocsRenderer
|
|
11
|
+
def initialize(content_dir:)
|
|
12
|
+
@content_dir = Pathname.new(content_dir)
|
|
13
|
+
@cache = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render(path)
|
|
17
|
+
file_path = @content_dir.join("#{path}.md")
|
|
18
|
+
raise FunApi::HTTPException.new(status_code: 404, detail: "Page not found") unless file_path.exist?
|
|
19
|
+
|
|
20
|
+
@cache[path] ||= begin
|
|
21
|
+
content = file_path.read
|
|
22
|
+
frontmatter, body = parse_frontmatter(content)
|
|
23
|
+
html = Kramdown::Document.new(
|
|
24
|
+
body,
|
|
25
|
+
input: "GFM",
|
|
26
|
+
syntax_highlighter: "rouge",
|
|
27
|
+
syntax_highlighter_opts: {default_lang: "ruby"}
|
|
28
|
+
).to_html
|
|
29
|
+
|
|
30
|
+
{frontmatter: frontmatter, html: html}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def navigation
|
|
35
|
+
@navigation ||= build_navigation
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def parse_frontmatter(content)
|
|
41
|
+
if content.start_with?("---")
|
|
42
|
+
parts = content.split("---", 3)
|
|
43
|
+
frontmatter = parse_yaml(parts[1])
|
|
44
|
+
body = parts[2]
|
|
45
|
+
[frontmatter, body]
|
|
46
|
+
else
|
|
47
|
+
[{}, content]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_yaml(yaml_str)
|
|
52
|
+
result = {}
|
|
53
|
+
yaml_str.each_line do |line|
|
|
54
|
+
result[::Regexp.last_match(1).to_sym] = ::Regexp.last_match(2).strip if line =~ /^(\w+):\s*(.+)$/
|
|
55
|
+
end
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_navigation
|
|
60
|
+
[
|
|
61
|
+
{
|
|
62
|
+
title: "Getting Started",
|
|
63
|
+
items: [
|
|
64
|
+
{path: "getting-started/at-glance", title: "At Glance"},
|
|
65
|
+
{path: "getting-started/quick-start", title: "Quick Start"},
|
|
66
|
+
{path: "getting-started/key-concepts", title: "Key Concepts"}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: "Essential",
|
|
71
|
+
items: [
|
|
72
|
+
{path: "essential/routing", title: "Routing"},
|
|
73
|
+
{path: "essential/handler", title: "Handler"},
|
|
74
|
+
{path: "essential/validation", title: "Validation"},
|
|
75
|
+
{path: "essential/openapi", title: "OpenAPI"},
|
|
76
|
+
{path: "essential/lifecycle", title: "Lifecycle"},
|
|
77
|
+
{path: "essential/middleware", title: "Middleware"}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: "Patterns",
|
|
82
|
+
items: [
|
|
83
|
+
{path: "patterns/async-operations", title: "Async Operations"},
|
|
84
|
+
{path: "patterns/dependencies", title: "Dependencies"},
|
|
85
|
+
{path: "patterns/background-tasks", title: "Background Tasks"},
|
|
86
|
+
{path: "patterns/templates", title: "Templates"},
|
|
87
|
+
{path: "patterns/error-handling", title: "Error Handling"},
|
|
88
|
+
{path: "patterns/response-schema", title: "Response Schema"},
|
|
89
|
+
{path: "patterns/database", title: "Database"},
|
|
90
|
+
{path: "patterns/testing", title: "Testing"},
|
|
91
|
+
{path: "patterns/deployment", title: "Deployment"}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get the docs-site directory path relative to this script
|
|
99
|
+
docs_site_dir = __dir__
|
|
100
|
+
docs = DocsRenderer.new(content_dir: File.join(docs_site_dir, "content"))
|
|
101
|
+
templates = FunApi::Templates.new(
|
|
102
|
+
directory: File.join(docs_site_dir, "templates"),
|
|
103
|
+
layout: "layouts/docs.html.erb"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
app = FunApi::App.new(
|
|
107
|
+
title: "FunApi Documentation",
|
|
108
|
+
version: "0.1.0",
|
|
109
|
+
description: "Documentation for the FunApi framework"
|
|
110
|
+
) do |api|
|
|
111
|
+
api.use Rack::Static, urls: ["/css"], root: File.join(docs_site_dir, "public")
|
|
112
|
+
api.add_request_logger
|
|
113
|
+
|
|
114
|
+
api.get "/" do |_input, _req, _task|
|
|
115
|
+
page = docs.render("index")
|
|
116
|
+
templates.response(
|
|
117
|
+
"page.html.erb",
|
|
118
|
+
title: page[:frontmatter][:title] || "FunApi",
|
|
119
|
+
content: page[:html],
|
|
120
|
+
nav: docs.navigation,
|
|
121
|
+
current_path: "index"
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
api.get "/docs/:section/:page" do |input, _req, _task|
|
|
126
|
+
path = "#{input[:path]["section"]}/#{input[:path]["page"]}"
|
|
127
|
+
page = docs.render(path)
|
|
128
|
+
templates.response(
|
|
129
|
+
"page.html.erb",
|
|
130
|
+
title: page[:frontmatter][:title] || "FunApi",
|
|
131
|
+
content: page[:html],
|
|
132
|
+
nav: docs.navigation,
|
|
133
|
+
current_path: path
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
FunApi::Server::Falcon.start(app, port: 3000) if __FILE__ == $0
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Handler
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Handler
|
|
6
|
+
|
|
7
|
+
The handler is the function that processes a request and returns a response.
|
|
8
|
+
|
|
9
|
+
## Handler Signature
|
|
10
|
+
|
|
11
|
+
Every handler receives three positional arguments:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
api.get '/path' do |input, req, task|
|
|
15
|
+
[response_data, status_code]
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Argument | Type | Description |
|
|
20
|
+
|----------|------|-------------|
|
|
21
|
+
| `input` | Hash | Request data (path, query, body) |
|
|
22
|
+
| `req` | Rack::Request | Full Rack request object |
|
|
23
|
+
| `task` | Async::Task | Current async task for concurrency |
|
|
24
|
+
|
|
25
|
+
## The Input Hash
|
|
26
|
+
|
|
27
|
+
The `input` hash normalizes all request data:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
api.post '/users/:id' do |input, req, task|
|
|
31
|
+
input[:path] # Path parameters: { 'id' => '123' }
|
|
32
|
+
input[:query] # Query params: { search: 'ruby' }
|
|
33
|
+
input[:body] # Parsed JSON body: { name: 'Alice' }
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Accessing Path Parameters
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
api.get '/posts/:post_id/comments/:id' do |input, req, task|
|
|
41
|
+
post_id = input[:path]['post_id']
|
|
42
|
+
comment_id = input[:path]['id']
|
|
43
|
+
# ...
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Accessing Query Parameters
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# GET /search?q=ruby&page=2
|
|
51
|
+
api.get '/search' do |input, req, task|
|
|
52
|
+
query = input[:query][:q]
|
|
53
|
+
page = input[:query][:page]
|
|
54
|
+
# ...
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Accessing Request Body
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
api.post '/users' do |input, req, task|
|
|
62
|
+
name = input[:body][:name]
|
|
63
|
+
email = input[:body][:email]
|
|
64
|
+
# ...
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## The Rack Request
|
|
69
|
+
|
|
70
|
+
The `req` object is a standard `Rack::Request`:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
api.get '/info' do |input, req, task|
|
|
74
|
+
{
|
|
75
|
+
method: req.request_method,
|
|
76
|
+
path: req.path_info,
|
|
77
|
+
host: req.host,
|
|
78
|
+
content_type: req.content_type,
|
|
79
|
+
user_agent: req.user_agent,
|
|
80
|
+
ip: req.ip
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Accessing Headers
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
api.get '/auth' do |input, req, task|
|
|
89
|
+
auth_header = req.get_header('HTTP_AUTHORIZATION')
|
|
90
|
+
# or
|
|
91
|
+
auth_header = req.env['HTTP_AUTHORIZATION']
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## The Async Task
|
|
96
|
+
|
|
97
|
+
The `task` parameter enables concurrent operations:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
api.get '/dashboard' do |input, req, task|
|
|
101
|
+
# Run operations concurrently
|
|
102
|
+
user = task.async { UserService.find(id) }
|
|
103
|
+
posts = task.async { PostService.recent }
|
|
104
|
+
|
|
105
|
+
[{
|
|
106
|
+
user: user.wait,
|
|
107
|
+
posts: posts.wait
|
|
108
|
+
}, 200]
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See [Async Operations](/docs/patterns/async-operations) for more.
|
|
113
|
+
|
|
114
|
+
## Return Value
|
|
115
|
+
|
|
116
|
+
Handlers must return `[data, status_code]`:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# JSON response
|
|
120
|
+
[{ message: 'Hello' }, 200]
|
|
121
|
+
|
|
122
|
+
# Array response
|
|
123
|
+
[users, 200]
|
|
124
|
+
|
|
125
|
+
# Empty response
|
|
126
|
+
[{}, 204]
|
|
127
|
+
|
|
128
|
+
# Error response
|
|
129
|
+
[{ error: 'Not found' }, 404]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Returning HTML
|
|
133
|
+
|
|
134
|
+
Return a `TemplateResponse` for HTML:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
api.get '/' do |input, req, task|
|
|
138
|
+
templates.response('home.html.erb', title: 'Home')
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
See [Templates](/docs/patterns/templates) for more.
|
|
143
|
+
|
|
144
|
+
## Keyword Arguments (Dependencies)
|
|
145
|
+
|
|
146
|
+
When using dependency injection, dependencies come as keyword arguments:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
api.get '/users', depends: [:db, :logger] do |input, req, task, db:, logger:|
|
|
150
|
+
logger.info("Fetching users")
|
|
151
|
+
users = db.query("SELECT * FROM users")
|
|
152
|
+
[{ users: users }, 200]
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
See [Dependencies](/docs/patterns/dependencies) for more.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Lifecycle
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Lifecycle
|
|
6
|
+
|
|
7
|
+
Lifecycle hooks let you run code when your application starts up or shuts down.
|
|
8
|
+
|
|
9
|
+
## Startup Hooks
|
|
10
|
+
|
|
11
|
+
Run code before the server accepts requests:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
app = FunApi::App.new do |api|
|
|
15
|
+
api.on_startup do
|
|
16
|
+
puts "Connecting to database..."
|
|
17
|
+
DB.connect
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
api.on_startup do
|
|
21
|
+
puts "Warming cache..."
|
|
22
|
+
Cache.warm
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Use Cases
|
|
28
|
+
|
|
29
|
+
- Database connection pool initialization
|
|
30
|
+
- Cache warming
|
|
31
|
+
- Loading configuration
|
|
32
|
+
- Starting background workers
|
|
33
|
+
- Metrics/logging initialization
|
|
34
|
+
|
|
35
|
+
## Shutdown Hooks
|
|
36
|
+
|
|
37
|
+
Run code when the server stops:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
app = FunApi::App.new do |api|
|
|
41
|
+
api.on_shutdown do
|
|
42
|
+
puts "Closing database connections..."
|
|
43
|
+
DB.disconnect
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
api.on_shutdown do
|
|
47
|
+
puts "Flushing metrics..."
|
|
48
|
+
Metrics.flush
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Use Cases
|
|
54
|
+
|
|
55
|
+
- Graceful database disconnection
|
|
56
|
+
- Flushing buffers/queues
|
|
57
|
+
- Stopping background workers
|
|
58
|
+
- Cleanup of temporary files
|
|
59
|
+
|
|
60
|
+
## Multiple Hooks
|
|
61
|
+
|
|
62
|
+
You can register multiple hooks of each type. They run in registration order:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
app = FunApi::App.new do |api|
|
|
66
|
+
api.on_startup do
|
|
67
|
+
puts "1. First startup hook"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
api.on_startup do
|
|
71
|
+
puts "2. Second startup hook"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
api.on_shutdown do
|
|
75
|
+
puts "1. First shutdown hook"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
api.on_shutdown do
|
|
79
|
+
puts "2. Second shutdown hook"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Error Handling
|
|
85
|
+
|
|
86
|
+
### Startup Errors
|
|
87
|
+
|
|
88
|
+
If a startup hook raises an error, the server won't start:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
api.on_startup do
|
|
92
|
+
raise "Database unavailable"
|
|
93
|
+
# Server fails to start
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Shutdown Errors
|
|
98
|
+
|
|
99
|
+
Shutdown hook errors are logged but don't stop other hooks:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
api.on_shutdown do
|
|
103
|
+
raise "Cleanup failed"
|
|
104
|
+
# Error logged, but next hook still runs
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
api.on_shutdown do
|
|
108
|
+
puts "This still runs"
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Complete Example
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
require 'funapi'
|
|
116
|
+
require 'funapi/server/falcon'
|
|
117
|
+
|
|
118
|
+
app = FunApi::App.new(title: "My API") do |api|
|
|
119
|
+
api.on_startup do
|
|
120
|
+
puts "Starting up..."
|
|
121
|
+
$db = Database.connect(ENV['DATABASE_URL'])
|
|
122
|
+
$cache = Cache.new
|
|
123
|
+
$cache.warm
|
|
124
|
+
puts "Ready!"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
api.on_shutdown do
|
|
128
|
+
puts "Shutting down..."
|
|
129
|
+
$cache.flush
|
|
130
|
+
$db.disconnect
|
|
131
|
+
puts "Goodbye!"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
api.get '/health' do |input, req, task|
|
|
135
|
+
[{ status: 'ok', db: $db.connected? }, 200]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
FunApi::Server::Falcon.start(app, port: 3000)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## With Dependencies
|
|
143
|
+
|
|
144
|
+
Combine lifecycle hooks with dependency injection:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
app = FunApi::App.new do |api|
|
|
148
|
+
api.on_startup do
|
|
149
|
+
db_pool = ConnectionPool.new(size: 10) { Database.connect }
|
|
150
|
+
api.register(:db) { db_pool.checkout }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
api.on_shutdown do
|
|
154
|
+
api.resolve(:db).close_all
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
api.get '/users', depends: [:db] do |input, req, task, db:|
|
|
158
|
+
[{ users: db.query("SELECT * FROM users") }, 200]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
```
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Middleware
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Middleware
|
|
6
|
+
|
|
7
|
+
Middleware wraps your application, processing requests before handlers and responses after.
|
|
8
|
+
|
|
9
|
+
## Built-in Middleware
|
|
10
|
+
|
|
11
|
+
FunApi provides convenience methods for common middleware:
|
|
12
|
+
|
|
13
|
+
### CORS
|
|
14
|
+
|
|
15
|
+
Handle Cross-Origin Resource Sharing:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
api.add_cors(
|
|
19
|
+
allow_origins: ['http://localhost:3000', 'https://myapp.com'],
|
|
20
|
+
allow_methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
21
|
+
allow_headers: ['Content-Type', 'Authorization'],
|
|
22
|
+
expose_headers: ['X-Request-Id'],
|
|
23
|
+
max_age: 600,
|
|
24
|
+
allow_credentials: true
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For development, allow all origins:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
api.add_cors(allow_origins: ['*'])
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Request Logger
|
|
35
|
+
|
|
36
|
+
Log incoming requests:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
api.add_request_logger
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
With custom logger:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
api.add_request_logger(
|
|
46
|
+
logger: Logger.new('logs/requests.log'),
|
|
47
|
+
level: :info
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Trusted Host
|
|
52
|
+
|
|
53
|
+
Validate the Host header (security):
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
api.add_trusted_host(
|
|
57
|
+
allowed_hosts: ['myapp.com', 'api.myapp.com']
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
With regex patterns:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
api.add_trusted_host(
|
|
65
|
+
allowed_hosts: ['localhost', /\.myapp\.com$/]
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Gzip Compression
|
|
70
|
+
|
|
71
|
+
Compress JSON responses:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
api.add_gzip
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Using Rack Middleware
|
|
78
|
+
|
|
79
|
+
Any Rack middleware works with FunApi:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
app = FunApi::App.new do |api|
|
|
83
|
+
api.use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
|
|
84
|
+
api.use Rack::Attack
|
|
85
|
+
api.use Rack::ETag
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Custom Middleware
|
|
90
|
+
|
|
91
|
+
Create middleware following the Rack pattern:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
class TimingMiddleware
|
|
95
|
+
def initialize(app)
|
|
96
|
+
@app = app
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def call(env)
|
|
100
|
+
start = Time.now
|
|
101
|
+
status, headers, body = @app.call(env)
|
|
102
|
+
duration = Time.now - start
|
|
103
|
+
|
|
104
|
+
headers['X-Response-Time'] = "#{(duration * 1000).round}ms"
|
|
105
|
+
[status, headers, body]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
app = FunApi::App.new do |api|
|
|
110
|
+
api.use TimingMiddleware
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### With Options
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
class AuthMiddleware
|
|
118
|
+
def initialize(app, secret:, exclude: [])
|
|
119
|
+
@app = app
|
|
120
|
+
@secret = secret
|
|
121
|
+
@exclude = exclude
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def call(env)
|
|
125
|
+
path = env['PATH_INFO']
|
|
126
|
+
|
|
127
|
+
if @exclude.include?(path)
|
|
128
|
+
return @app.call(env)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
token = env['HTTP_AUTHORIZATION']&.delete_prefix('Bearer ')
|
|
132
|
+
|
|
133
|
+
unless valid_token?(token)
|
|
134
|
+
return [401, {'content-type' => 'application/json'}, ['{"error":"Unauthorized"}']]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@app.call(env)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def valid_token?(token)
|
|
143
|
+
# Verify token with @secret
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
app = FunApi::App.new do |api|
|
|
148
|
+
api.use AuthMiddleware,
|
|
149
|
+
secret: ENV['JWT_SECRET'],
|
|
150
|
+
exclude: ['/health', '/docs', '/openapi.json']
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Middleware Order
|
|
155
|
+
|
|
156
|
+
Middleware runs in the order registered (first in, first out):
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
app = FunApi::App.new do |api|
|
|
160
|
+
api.use LoggingMiddleware # 1. Runs first
|
|
161
|
+
api.use AuthMiddleware # 2. Runs second
|
|
162
|
+
api.use TimingMiddleware # 3. Runs third
|
|
163
|
+
|
|
164
|
+
# Then your routes handle the request
|
|
165
|
+
# Response goes back through in reverse order
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Request flow:
|
|
170
|
+
```
|
|
171
|
+
Request → Logging → Auth → Timing → Handler
|
|
172
|
+
Response ← Logging ← Auth ← Timing ← Handler
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Complete Example
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
require 'funapi'
|
|
179
|
+
require 'funapi/server/falcon'
|
|
180
|
+
|
|
181
|
+
app = FunApi::App.new(title: "My API") do |api|
|
|
182
|
+
# Security
|
|
183
|
+
api.add_trusted_host(allowed_hosts: ['localhost', 'myapi.com'])
|
|
184
|
+
api.add_cors(allow_origins: ['https://myapp.com'])
|
|
185
|
+
|
|
186
|
+
# Logging
|
|
187
|
+
api.add_request_logger
|
|
188
|
+
|
|
189
|
+
# Compression
|
|
190
|
+
api.add_gzip
|
|
191
|
+
|
|
192
|
+
# Custom
|
|
193
|
+
api.use Rack::Session::Cookie, secret: 'secret'
|
|
194
|
+
|
|
195
|
+
api.get '/hello' do |input, req, task|
|
|
196
|
+
[{ message: 'Hello!' }, 200]
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
FunApi::Server::Falcon.start(app, port: 3000)
|
|
201
|
+
```
|