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,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: OpenAPI
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# OpenAPI
|
|
6
|
+
|
|
7
|
+
FunApi automatically generates OpenAPI 3.0 documentation from your routes and schemas.
|
|
8
|
+
|
|
9
|
+
## Built-in Endpoints
|
|
10
|
+
|
|
11
|
+
Every FunApi application exposes:
|
|
12
|
+
|
|
13
|
+
| Endpoint | Description |
|
|
14
|
+
|----------|-------------|
|
|
15
|
+
| `/docs` | Interactive Swagger UI |
|
|
16
|
+
| `/openapi.json` | Raw OpenAPI specification |
|
|
17
|
+
|
|
18
|
+
## Configuring Your API
|
|
19
|
+
|
|
20
|
+
Set API metadata when creating the app:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
app = FunApi::App.new(
|
|
24
|
+
title: "User Management API",
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
description: "A comprehensive user management system"
|
|
27
|
+
) do |api|
|
|
28
|
+
# routes...
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This appears in the OpenAPI spec and Swagger UI header.
|
|
33
|
+
|
|
34
|
+
## What Gets Documented
|
|
35
|
+
|
|
36
|
+
### Routes
|
|
37
|
+
|
|
38
|
+
All routes are automatically included:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
api.get '/users' do |input, req, task|
|
|
42
|
+
# Documented as GET /users
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
api.post '/users' do |input, req, task|
|
|
46
|
+
# Documented as POST /users
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Path Parameters
|
|
51
|
+
|
|
52
|
+
Path parameters are extracted and documented:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
api.get '/users/:id' do |input, req, task|
|
|
56
|
+
# Documented with {id} parameter
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Schemas
|
|
61
|
+
|
|
62
|
+
Schemas become OpenAPI components:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
UserSchema = FunApi::Schema.define do
|
|
66
|
+
required(:name).filled(:string)
|
|
67
|
+
required(:email).filled(:string)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
api.post '/users', body: UserSchema do |input, req, task|
|
|
71
|
+
# Request body documented with UserSchema
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Response Schemas
|
|
76
|
+
|
|
77
|
+
Response schemas document the output:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
api.get '/users/:id', response_schema: UserOutputSchema do |input, req, task|
|
|
81
|
+
# Response documented with UserOutputSchema
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Swagger UI
|
|
86
|
+
|
|
87
|
+
The `/docs` endpoint serves an interactive Swagger UI where you can:
|
|
88
|
+
|
|
89
|
+
- Browse all endpoints
|
|
90
|
+
- See request/response schemas
|
|
91
|
+
- Try out API calls directly
|
|
92
|
+
- View example payloads
|
|
93
|
+
|
|
94
|
+
## OpenAPI JSON
|
|
95
|
+
|
|
96
|
+
Access the raw spec at `/openapi.json`:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"openapi": "3.0.0",
|
|
101
|
+
"info": {
|
|
102
|
+
"title": "User Management API",
|
|
103
|
+
"version": "1.0.0",
|
|
104
|
+
"description": "A comprehensive user management system"
|
|
105
|
+
},
|
|
106
|
+
"paths": {
|
|
107
|
+
"/users": {
|
|
108
|
+
"get": { ... },
|
|
109
|
+
"post": { ... }
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"components": {
|
|
113
|
+
"schemas": {
|
|
114
|
+
"UserSchema": { ... }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Schema Names
|
|
121
|
+
|
|
122
|
+
Schema names in OpenAPI come from your Ruby constant names:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
UserCreateSchema = FunApi::Schema.define { ... }
|
|
126
|
+
# Becomes "UserCreateSchema" in OpenAPI
|
|
127
|
+
|
|
128
|
+
UserOutputSchema = FunApi::Schema.define { ... }
|
|
129
|
+
# Becomes "UserOutputSchema" in OpenAPI
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Use Cases
|
|
133
|
+
|
|
134
|
+
### Client Generation
|
|
135
|
+
|
|
136
|
+
Use the OpenAPI spec to generate clients:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Generate TypeScript client
|
|
140
|
+
npx openapi-typescript http://localhost:3000/openapi.json -o api.ts
|
|
141
|
+
|
|
142
|
+
# Generate Python client
|
|
143
|
+
openapi-generator generate -i http://localhost:3000/openapi.json -g python
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### API Testing
|
|
147
|
+
|
|
148
|
+
Import the spec into Postman, Insomnia, or other API tools.
|
|
149
|
+
|
|
150
|
+
### Documentation Hosting
|
|
151
|
+
|
|
152
|
+
Export the spec and host on platforms like:
|
|
153
|
+
- SwaggerHub
|
|
154
|
+
- Redoc
|
|
155
|
+
- Stoplight
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Routing
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Routing
|
|
6
|
+
|
|
7
|
+
Routes map HTTP requests to handler functions based on the path and method.
|
|
8
|
+
|
|
9
|
+
## Defining Routes
|
|
10
|
+
|
|
11
|
+
FunApi provides methods for each HTTP verb:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
api.get '/users' do |input, req, task|
|
|
15
|
+
[{ users: [] }, 200]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
api.post '/users' do |input, req, task|
|
|
19
|
+
[{ created: input[:body] }, 201]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
api.put '/users/:id' do |input, req, task|
|
|
23
|
+
[{ updated: true }, 200]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
api.patch '/users/:id' do |input, req, task|
|
|
27
|
+
[{ patched: true }, 200]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
api.delete '/users/:id' do |input, req, task|
|
|
31
|
+
[{}, 204]
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Path Parameters
|
|
36
|
+
|
|
37
|
+
Capture dynamic segments with `:param` syntax:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
api.get '/users/:id' do |input, req, task|
|
|
41
|
+
user_id = input[:path]['id'] # Always a string
|
|
42
|
+
[{ id: user_id }, 200]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
api.get '/posts/:post_id/comments/:comment_id' do |input, req, task|
|
|
46
|
+
post_id = input[:path]['post_id']
|
|
47
|
+
comment_id = input[:path]['comment_id']
|
|
48
|
+
[{ post_id: post_id, comment_id: comment_id }, 200]
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> **Note**: Path parameters are always strings. Convert them manually if needed:
|
|
53
|
+
> ```ruby
|
|
54
|
+
> id = input[:path]['id'].to_i
|
|
55
|
+
> ```
|
|
56
|
+
|
|
57
|
+
## Query Parameters
|
|
58
|
+
|
|
59
|
+
Query parameters come from the URL query string:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# GET /search?q=ruby&limit=10
|
|
63
|
+
api.get '/search' do |input, req, task|
|
|
64
|
+
query = input[:query][:q]
|
|
65
|
+
limit = input[:query][:limit]&.to_i || 20
|
|
66
|
+
[{ query: query, limit: limit }, 200]
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
With validation:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
SearchSchema = FunApi::Schema.define do
|
|
74
|
+
required(:q).filled(:string)
|
|
75
|
+
optional(:limit).filled(:integer)
|
|
76
|
+
optional(:offset).filled(:integer)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
api.get '/search', query: SearchSchema do |input, req, task|
|
|
80
|
+
# input[:query] is validated and coerced
|
|
81
|
+
[{ results: search(input[:query]) }, 200]
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Request Body
|
|
86
|
+
|
|
87
|
+
POST, PUT, and PATCH routes typically receive a JSON body:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
UserSchema = FunApi::Schema.define do
|
|
91
|
+
required(:name).filled(:string)
|
|
92
|
+
required(:email).filled(:string)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
api.post '/users', body: UserSchema do |input, req, task|
|
|
96
|
+
user = input[:body] # Validated hash
|
|
97
|
+
[{ created: user }, 201]
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Route Priority
|
|
102
|
+
|
|
103
|
+
Routes are matched in the order they're defined. More specific routes should come first:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
api.get '/users/me' do |input, req, task|
|
|
107
|
+
# Matches /users/me
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
api.get '/users/:id' do |input, req, task|
|
|
111
|
+
# Matches /users/123, /users/anything
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## The Root Route
|
|
116
|
+
|
|
117
|
+
The root path `/` works like any other route:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
api.get '/' do |input, req, task|
|
|
121
|
+
[{ status: 'ok' }, 200]
|
|
122
|
+
end
|
|
123
|
+
```
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Validation
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Validation
|
|
6
|
+
|
|
7
|
+
FunApi uses [dry-schema](https://dry-rb.org/gems/dry-schema/) for request and response validation.
|
|
8
|
+
|
|
9
|
+
## Defining Schemas
|
|
10
|
+
|
|
11
|
+
Create schemas with `FunApi::Schema.define`:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
UserSchema = FunApi::Schema.define do
|
|
15
|
+
required(:name).filled(:string)
|
|
16
|
+
required(:email).filled(:string)
|
|
17
|
+
optional(:age).filled(:integer)
|
|
18
|
+
optional(:role).filled(:string)
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> **Technical Detail**: `FunApi::Schema.define` is a thin wrapper around `Dry::Schema.Params`. You have access to the full dry-schema DSL.
|
|
23
|
+
|
|
24
|
+
## Applying Schemas
|
|
25
|
+
|
|
26
|
+
### Body Validation
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
api.post '/users', body: UserSchema do |input, req, task|
|
|
30
|
+
user = input[:body] # Validated and coerced
|
|
31
|
+
[{ created: user }, 201]
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Query Validation
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
SearchSchema = FunApi::Schema.define do
|
|
39
|
+
required(:q).filled(:string)
|
|
40
|
+
optional(:limit).filled(:integer)
|
|
41
|
+
optional(:offset).filled(:integer)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
api.get '/search', query: SearchSchema do |input, req, task|
|
|
45
|
+
[{ results: search(input[:query]) }, 200]
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Response Validation
|
|
50
|
+
|
|
51
|
+
Filter and validate response data:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
UserOutputSchema = FunApi::Schema.define do
|
|
55
|
+
required(:id).filled(:integer)
|
|
56
|
+
required(:name).filled(:string)
|
|
57
|
+
required(:email).filled(:string)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
api.get '/users/:id', response_schema: UserOutputSchema do |input, req, task|
|
|
61
|
+
user = find_user(input[:path]['id'])
|
|
62
|
+
# password and other fields are filtered out
|
|
63
|
+
[user, 200]
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Schema DSL
|
|
68
|
+
|
|
69
|
+
### Required vs Optional
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
FunApi::Schema.define do
|
|
73
|
+
required(:name).filled(:string) # Must be present and non-empty
|
|
74
|
+
optional(:nickname).filled(:string) # Can be absent, but if present must be valid
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Types
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
FunApi::Schema.define do
|
|
82
|
+
required(:name).filled(:string)
|
|
83
|
+
required(:age).filled(:integer)
|
|
84
|
+
required(:price).filled(:float)
|
|
85
|
+
required(:active).filled(:bool)
|
|
86
|
+
required(:tags).filled(:array)
|
|
87
|
+
required(:metadata).filled(:hash)
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Nested Objects
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
AddressSchema = FunApi::Schema.define do
|
|
95
|
+
required(:street).filled(:string)
|
|
96
|
+
required(:city).filled(:string)
|
|
97
|
+
required(:zip).filled(:string)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
UserSchema = FunApi::Schema.define do
|
|
101
|
+
required(:name).filled(:string)
|
|
102
|
+
required(:address).hash(AddressSchema)
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Arrays of Objects
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
ItemSchema = FunApi::Schema.define do
|
|
110
|
+
required(:name).filled(:string)
|
|
111
|
+
required(:quantity).filled(:integer)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
OrderSchema = FunApi::Schema.define do
|
|
115
|
+
required(:items).array(:hash) do
|
|
116
|
+
required(:name).filled(:string)
|
|
117
|
+
required(:quantity).filled(:integer)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Or validate an array of items:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
api.post '/users/batch', body: [UserSchema] do |input, req, task|
|
|
126
|
+
users = input[:body] # Array of validated users
|
|
127
|
+
[{ created: users.length }, 201]
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Validation Errors
|
|
132
|
+
|
|
133
|
+
When validation fails, FunApi returns a FastAPI-style error response:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"detail": [
|
|
138
|
+
{
|
|
139
|
+
"loc": ["body", "email"],
|
|
140
|
+
"msg": "is missing",
|
|
141
|
+
"type": "value_error"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"loc": ["body", "age"],
|
|
145
|
+
"msg": "must be an integer",
|
|
146
|
+
"type": "value_error"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Status code: `422 Unprocessable Entity`
|
|
153
|
+
|
|
154
|
+
## Custom Validation
|
|
155
|
+
|
|
156
|
+
For complex validation, use dry-schema's full DSL:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
UserSchema = FunApi::Schema.define do
|
|
160
|
+
required(:email).filled(:string, format?: /@/)
|
|
161
|
+
required(:age).filled(:integer, gt?: 0, lt?: 150)
|
|
162
|
+
required(:password).filled(:string, min_size?: 8)
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
See the [dry-schema documentation](https://dry-rb.org/gems/dry-schema/) for the complete DSL reference.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: At Glance
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# At Glance
|
|
6
|
+
|
|
7
|
+
## What is FunApi?
|
|
8
|
+
|
|
9
|
+
FunApi is a minimal, async-first Ruby web framework for building APIs. It draws heavy inspiration from Python's FastAPI, bringing that same developer experience to Ruby.
|
|
10
|
+
|
|
11
|
+
## Philosophy
|
|
12
|
+
|
|
13
|
+
<!--
|
|
14
|
+
TODO: Fill in your philosophy here. Some prompts:
|
|
15
|
+
- Why did you create this?
|
|
16
|
+
- What's wrong with existing Ruby frameworks for APIs?
|
|
17
|
+
- What makes async-first important?
|
|
18
|
+
- Who is this for?
|
|
19
|
+
-->
|
|
20
|
+
|
|
21
|
+
### Async-first
|
|
22
|
+
|
|
23
|
+
FunApi is built from the ground up for async operations. Every route handler receives an `Async::Task` that enables true concurrent execution within your routes.
|
|
24
|
+
|
|
25
|
+
### Minimal Magic
|
|
26
|
+
|
|
27
|
+
Unlike larger frameworks, FunApi keeps things explicit. No hidden callbacks, no implicit behavior. You see exactly what's happening.
|
|
28
|
+
|
|
29
|
+
### Validation at the Edges
|
|
30
|
+
|
|
31
|
+
Request validation happens before your handler runs. Response filtering happens after. Your business logic stays clean.
|
|
32
|
+
|
|
33
|
+
### Auto-Documentation
|
|
34
|
+
|
|
35
|
+
Your API documentation is generated from your code. Define a schema once, get validation AND documentation.
|
|
36
|
+
|
|
37
|
+
## Where FunApi Fits
|
|
38
|
+
|
|
39
|
+
<!--
|
|
40
|
+
TODO: Fill in comparison with other Ruby frameworks:
|
|
41
|
+
- vs Rails API-only
|
|
42
|
+
- vs Sinatra
|
|
43
|
+
- vs Roda
|
|
44
|
+
- vs Grape
|
|
45
|
+
-->
|
|
46
|
+
|
|
47
|
+
| Framework | Use Case | FunApi Difference |
|
|
48
|
+
|-----------|----------|-------------------|
|
|
49
|
+
| Rails API | Full-featured API | FunApi is lighter, async-first |
|
|
50
|
+
| Sinatra | Simple APIs | FunApi adds validation, OpenAPI |
|
|
51
|
+
| Roda | Routing-focused | FunApi is async, has schemas |
|
|
52
|
+
| Grape | API-focused | FunApi is simpler, async |
|
|
53
|
+
|
|
54
|
+
## Core Concepts Preview
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
app = FunApi::App.new do |api|
|
|
58
|
+
# Validation schemas
|
|
59
|
+
UserSchema = FunApi::Schema.define do
|
|
60
|
+
required(:name).filled(:string)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Routes with validation
|
|
64
|
+
api.post '/users', body: UserSchema do |input, req, task|
|
|
65
|
+
# input[:body] is already validated
|
|
66
|
+
[{ user: input[:body] }, 201]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Async operations
|
|
70
|
+
api.get '/dashboard/:id' do |input, req, task|
|
|
71
|
+
# Concurrent fetches
|
|
72
|
+
user = task.async { fetch_user(input[:path]['id']) }
|
|
73
|
+
posts = task.async { fetch_posts(input[:path]['id']) }
|
|
74
|
+
|
|
75
|
+
[{ user: user.wait, posts: posts.wait }, 200]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Lifecycle hooks
|
|
79
|
+
api.on_startup { DB.connect }
|
|
80
|
+
api.on_shutdown { DB.disconnect }
|
|
81
|
+
end
|
|
82
|
+
```
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Key Concepts
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Key Concepts
|
|
6
|
+
|
|
7
|
+
Understanding these core concepts will help you work effectively with FunApi.
|
|
8
|
+
|
|
9
|
+
## The App
|
|
10
|
+
|
|
11
|
+
Everything starts with `FunApi::App`. It's your application container that holds routes, middleware, and configuration.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
app = FunApi::App.new(
|
|
15
|
+
title: "My API", # Shows in OpenAPI docs
|
|
16
|
+
version: "1.0.0", # API version
|
|
17
|
+
description: "..." # Optional description
|
|
18
|
+
) do |api|
|
|
19
|
+
# Define routes here
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Route Handlers
|
|
24
|
+
|
|
25
|
+
Route handlers are blocks that receive three arguments:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
api.get '/path' do |input, req, task|
|
|
29
|
+
# input - Hash with :path, :query, :body
|
|
30
|
+
# req - Rack::Request object
|
|
31
|
+
# task - Async::Task for concurrent operations
|
|
32
|
+
|
|
33
|
+
[response_data, status_code]
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### The Input Hash
|
|
38
|
+
|
|
39
|
+
All request data is normalized into a single `input` hash:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
api.post '/users/:id' do |input, req, task|
|
|
43
|
+
input[:path] # => { 'id' => '123' }
|
|
44
|
+
input[:query] # => { limit: 10, offset: 0 }
|
|
45
|
+
input[:body] # => { name: 'Alice', ... }
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Return Value
|
|
50
|
+
|
|
51
|
+
Handlers return a tuple of `[data, status_code]`:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
[{ user: user }, 200] # Success
|
|
55
|
+
[{ error: 'Not found' }, 404] # Error
|
|
56
|
+
[created_user, 201] # Created
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Schemas
|
|
60
|
+
|
|
61
|
+
Schemas define the shape of request and response data using dry-schema:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
UserSchema = FunApi::Schema.define do
|
|
65
|
+
required(:name).filled(:string)
|
|
66
|
+
required(:email).filled(:string)
|
|
67
|
+
optional(:age).filled(:integer)
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Apply schemas to routes:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
api.post '/users', body: UserSchema do |input, req, task|
|
|
75
|
+
# input[:body] is validated and coerced
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
api.get '/users', query: QuerySchema do |input, req, task|
|
|
79
|
+
# input[:query] is validated
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Async Task
|
|
84
|
+
|
|
85
|
+
The `task` parameter is an `Async::Task` from Ruby's Async library. Use it for concurrent operations:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
api.get '/dashboard' do |input, req, task|
|
|
89
|
+
# These run concurrently
|
|
90
|
+
user_task = task.async { fetch_user }
|
|
91
|
+
posts_task = task.async { fetch_posts }
|
|
92
|
+
stats_task = task.async { fetch_stats }
|
|
93
|
+
|
|
94
|
+
# Wait for all to complete
|
|
95
|
+
[{
|
|
96
|
+
user: user_task.wait,
|
|
97
|
+
posts: posts_task.wait,
|
|
98
|
+
stats: stats_task.wait
|
|
99
|
+
}, 200]
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Middleware Stack
|
|
104
|
+
|
|
105
|
+
Middleware wraps your application, processing requests before they reach handlers and responses after:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
app = FunApi::App.new do |api|
|
|
109
|
+
# Built-in middleware
|
|
110
|
+
api.add_cors(allow_origins: ['*'])
|
|
111
|
+
api.add_request_logger
|
|
112
|
+
|
|
113
|
+
# Standard Rack middleware
|
|
114
|
+
api.use Rack::Session::Cookie, secret: 'key'
|
|
115
|
+
|
|
116
|
+
# Routes...
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Middleware runs in order: first added runs first.
|
|
121
|
+
|
|
122
|
+
## Lifecycle Hooks
|
|
123
|
+
|
|
124
|
+
Run code when the application starts or stops:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
app = FunApi::App.new do |api|
|
|
128
|
+
api.on_startup do
|
|
129
|
+
DB.connect
|
|
130
|
+
Cache.warm
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
api.on_shutdown do
|
|
134
|
+
DB.disconnect
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## OpenAPI/Swagger
|
|
140
|
+
|
|
141
|
+
FunApi automatically generates OpenAPI documentation from your routes and schemas:
|
|
142
|
+
|
|
143
|
+
- `/docs` - Interactive Swagger UI
|
|
144
|
+
- `/openapi.json` - Raw OpenAPI specification
|
|
145
|
+
|
|
146
|
+
The docs are generated from:
|
|
147
|
+
- Route paths and methods
|
|
148
|
+
- Path parameters
|
|
149
|
+
- Query and body schemas
|
|
150
|
+
- Response schemas
|