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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d22a926eac2899f60270f0af1b81c694a12bb648249a7d19e6d4ae99e2ef82e8
|
|
4
|
+
data.tar.gz: 5f4b7f4356379e415fac6cc455deb6d4e147ab55bc7b4df4e202157cb97bce97
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ceb4ad7766b835792450291b431cefeb89fa8cdba7c11b55f2e902bc1fa428dcb78792dc319f99e7a84a206274d8ff197723cfb6d06422729c78a663678716e0
|
|
7
|
+
data.tar.gz: 30dd76f22965556f0a2c90124426adffb1bba3600c4113341a26efe8cefdddeb2c4e6c68d0e2b196a95533e43b615a5eb5ca0301f8e506189b1b7a0f190029af
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# OpenAPI/Swagger Documentation - Implementation Summary
|
|
2
|
+
|
|
3
|
+
## ✅ Implementation Complete
|
|
4
|
+
|
|
5
|
+
FunApi now automatically generates OpenAPI 3.0 specifications from your route definitions and schemas, providing interactive Swagger UI documentation.
|
|
6
|
+
|
|
7
|
+
## What Was Implemented
|
|
8
|
+
|
|
9
|
+
### 1. Route Metadata Storage
|
|
10
|
+
- **File**: `lib/fun_api/router.rb`
|
|
11
|
+
- Updated `Route` struct to include metadata field
|
|
12
|
+
- Added `routes` reader to expose routes for spec generation
|
|
13
|
+
- Routes now store: `path_template`, `body_schema`, `query_schema`, `response_schema`
|
|
14
|
+
|
|
15
|
+
### 2. App-Level Configuration
|
|
16
|
+
- **File**: `lib/fun_api/application.rb`
|
|
17
|
+
- Added OpenAPI config parameters to `App.new`: `title`, `version`, `description`
|
|
18
|
+
- Default values:
|
|
19
|
+
- title: "FunApi Application"
|
|
20
|
+
- version: "1.0.0"
|
|
21
|
+
- description: ""
|
|
22
|
+
|
|
23
|
+
### 3. Schema Converter
|
|
24
|
+
- **File**: `lib/fun_api/openapi/schema_converter.rb`
|
|
25
|
+
- Converts dry-schema definitions to JSON Schema (OpenAPI compatible)
|
|
26
|
+
- Supports types: `string`, `integer`, `number`, `boolean`, `array`, `object`
|
|
27
|
+
- Handles required vs optional fields
|
|
28
|
+
- Handles array schemas (e.g., `body: [UserSchema]`)
|
|
29
|
+
- Extracts schema names from Ruby constant names
|
|
30
|
+
|
|
31
|
+
### 4. OpenAPI Spec Generator
|
|
32
|
+
- **File**: `lib/fun_api/openapi/spec_generator.rb`
|
|
33
|
+
- Generates complete OpenAPI 3.0.3 specification
|
|
34
|
+
- Converts path templates: `/users/:id` → `/users/{id}`
|
|
35
|
+
- Generates path parameters from route patterns
|
|
36
|
+
- Generates query parameters from query schemas
|
|
37
|
+
- Generates request body schemas
|
|
38
|
+
- Generates response schemas
|
|
39
|
+
- Populates components/schemas section
|
|
40
|
+
- Excludes internal routes (marked with `internal: true`)
|
|
41
|
+
|
|
42
|
+
### 5. Documentation Endpoints
|
|
43
|
+
- **Route**: `GET /openapi.json`
|
|
44
|
+
- Returns the complete OpenAPI specification as JSON
|
|
45
|
+
- Automatically excluded from the spec itself
|
|
46
|
+
|
|
47
|
+
- **Route**: `GET /docs`
|
|
48
|
+
- Serves interactive Swagger UI
|
|
49
|
+
- Uses CDN-hosted Swagger UI 5.x
|
|
50
|
+
- Automatically loads spec from `/openapi.json`
|
|
51
|
+
- Automatically excluded from the spec itself
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
### Automatic Schema Detection
|
|
56
|
+
Schemas are automatically detected and named based on Ruby constant names:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
UserCreateSchema = FunApi::Schema.define do
|
|
60
|
+
required(:name).filled(:string)
|
|
61
|
+
required(:email).filled(:string)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Becomes: "UserCreateSchema" in OpenAPI components
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Path Parameters
|
|
68
|
+
Automatically extracted from route patterns:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
api.get '/users/:id' do |input, req, task|
|
|
72
|
+
# Generates OpenAPI path parameter:
|
|
73
|
+
# {
|
|
74
|
+
# "name": "id",
|
|
75
|
+
# "in": "path",
|
|
76
|
+
# "required": true,
|
|
77
|
+
# "schema": { "type": "string" }
|
|
78
|
+
# }
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Query Parameters
|
|
83
|
+
Generated from query schemas with proper required/optional handling:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
QuerySchema = FunApi::Schema.define do
|
|
87
|
+
optional(:limit).filled(:integer)
|
|
88
|
+
required(:filter).filled(:string)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
api.get '/users', query: QuerySchema do |input, req, task|
|
|
92
|
+
# Generates query parameters with correct 'required' field
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Request Body
|
|
97
|
+
Supports both single objects and arrays:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Single object
|
|
101
|
+
api.post '/users', body: UserSchema do
|
|
102
|
+
# ...
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Array of objects
|
|
106
|
+
api.post '/users/batch', body: [UserSchema] do
|
|
107
|
+
# Generates: { "type": "array", "items": { "$ref": "..." } }
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Response Schemas
|
|
112
|
+
Automatically documents response structure:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
api.get '/users/:id', response_schema: UserOutputSchema do
|
|
116
|
+
# Documents 200 response with UserOutputSchema structure
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
api.get '/users', response_schema: [UserOutputSchema] do
|
|
120
|
+
# Documents 200 response as array of UserOutputSchema
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Type Support
|
|
125
|
+
Fully supports all common JSON Schema types:
|
|
126
|
+
- ✅ `string` - from `.filled(:string)`
|
|
127
|
+
- ✅ `integer` - from `.filled(:integer)` or `.filled(:int)`
|
|
128
|
+
- ✅ `number` - from `.filled(:float)` or `.filled(:decimal)`
|
|
129
|
+
- ✅ `boolean` - from `.filled(:bool)`
|
|
130
|
+
- ✅ `array` - from `.array(:string)`, `.array(:integer)`, etc.
|
|
131
|
+
- ✅ `object` - from `.filled(:hash)`
|
|
132
|
+
|
|
133
|
+
## Usage Example
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
require 'fun_api'
|
|
137
|
+
require 'fun_api/server/falcon'
|
|
138
|
+
|
|
139
|
+
UserCreateSchema = FunApi::Schema.define do
|
|
140
|
+
required(:name).filled(:string)
|
|
141
|
+
required(:email).filled(:string)
|
|
142
|
+
optional(:age).filled(:integer)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
UserOutputSchema = FunApi::Schema.define do
|
|
146
|
+
required(:id).filled(:integer)
|
|
147
|
+
required(:name).filled(:string)
|
|
148
|
+
required(:email).filled(:string)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
app = FunApi::App.new(
|
|
152
|
+
title: "User Management API",
|
|
153
|
+
version: "1.0.0",
|
|
154
|
+
description: "A simple user management API"
|
|
155
|
+
) do |api|
|
|
156
|
+
api.get '/users/:id', response_schema: UserOutputSchema do |input, req, task|
|
|
157
|
+
user = { id: 1, name: 'John', email: 'john@example.com' }
|
|
158
|
+
[user, 200]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
api.post '/users',
|
|
162
|
+
body: UserCreateSchema,
|
|
163
|
+
response_schema: UserOutputSchema do |input, req, task|
|
|
164
|
+
user = input[:body].merge(id: rand(1000))
|
|
165
|
+
[user, 201]
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
FunApi::Server::Falcon.start(app, port: 9292)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Then visit:
|
|
173
|
+
- **Swagger UI**: http://localhost:9292/docs
|
|
174
|
+
- **OpenAPI JSON**: http://localhost:9292/openapi.json
|
|
175
|
+
|
|
176
|
+
## Files Modified/Created
|
|
177
|
+
|
|
178
|
+
### Modified Files
|
|
179
|
+
1. `lib/fun_api.rb` - Added router require
|
|
180
|
+
2. `lib/fun_api/router.rb` - Added metadata storage
|
|
181
|
+
3. `lib/fun_api/application.rb` - Added OpenAPI config and endpoints
|
|
182
|
+
4. `README.md` - Updated with OpenAPI documentation
|
|
183
|
+
|
|
184
|
+
### New Files
|
|
185
|
+
1. `lib/fun_api/openapi/schema_converter.rb` - Schema conversion logic
|
|
186
|
+
2. `lib/fun_api/openapi/spec_generator.rb` - OpenAPI spec generation
|
|
187
|
+
3. `test_openapi.rb` - Test application
|
|
188
|
+
4. `OPENAPI_IMPLEMENTATION.md` - This file
|
|
189
|
+
|
|
190
|
+
## Design Decisions
|
|
191
|
+
|
|
192
|
+
Following the plan, we implemented:
|
|
193
|
+
|
|
194
|
+
1. ✅ **Schema names**: Use constant names (e.g., `UserCreateSchema`)
|
|
195
|
+
2. ✅ **Default info**: Use `title`, `version`, `description` from app config
|
|
196
|
+
3. ✅ **Docs path**: Fixed at `/docs`
|
|
197
|
+
4. ✅ **No deduplication**: Each schema reference registered as-is
|
|
198
|
+
5. ✅ **FastAPI-inspired**: Response structure matches FastAPI patterns
|
|
199
|
+
|
|
200
|
+
## Testing
|
|
201
|
+
|
|
202
|
+
Run the test application:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
ruby test_openapi.rb
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Then test the endpoints:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Get OpenAPI spec
|
|
212
|
+
curl http://localhost:9292/openapi.json | jq .
|
|
213
|
+
|
|
214
|
+
# Test actual endpoints
|
|
215
|
+
curl http://localhost:9292/users
|
|
216
|
+
curl -X POST http://localhost:9292/users \
|
|
217
|
+
-H "Content-Type: application/json" \
|
|
218
|
+
-d '{"name":"John","email":"john@example.com","password":"secret"}'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Next Steps
|
|
222
|
+
|
|
223
|
+
Future enhancements could include:
|
|
224
|
+
- Support for additional OpenAPI features (tags, descriptions per route)
|
|
225
|
+
- ReDoc alternative UI at `/redoc`
|
|
226
|
+
- OpenAPI schema validation options
|
|
227
|
+
- Custom response status codes documentation
|
|
228
|
+
- Security scheme definitions
|
|
229
|
+
- Response examples
|
|
230
|
+
|
|
231
|
+
## Conclusion
|
|
232
|
+
|
|
233
|
+
The OpenAPI/Swagger documentation generation is fully implemented and working. Users can now get automatic, interactive API documentation by simply defining their routes and schemas, making FunApi truly FastAPI-like in its developer experience.
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# Response Schema Feature
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `response_schema` feature allows you to validate and filter response data before sending it to clients, similar to FastAPI's `response_model`. This ensures:
|
|
6
|
+
|
|
7
|
+
1. **Data Security** - Sensitive fields (like passwords) are automatically filtered from responses
|
|
8
|
+
2. **Data Validation** - Responses are validated to ensure your app returns correct data structure
|
|
9
|
+
3. **Documentation** - Response schemas will be used for automatic API documentation generation (future)
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
### Option 1: Using `response_schema` Parameter
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# Define input and output schemas
|
|
17
|
+
UserInput = FunApi::Schema.define do
|
|
18
|
+
required(:username).filled(:string)
|
|
19
|
+
required(:email).filled(:string)
|
|
20
|
+
required(:password).filled(:string)
|
|
21
|
+
optional(:age).filled(:integer)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
UserOutput = FunApi::Schema.define do
|
|
25
|
+
required(:id).filled(:integer)
|
|
26
|
+
required(:username).filled(:string)
|
|
27
|
+
required(:email).filled(:string)
|
|
28
|
+
optional(:age).filled(:integer)
|
|
29
|
+
# Note: password NOT included
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Use in route
|
|
33
|
+
api.post '/users',
|
|
34
|
+
body: UserInput,
|
|
35
|
+
response_schema: UserOutput do |input, req, task|
|
|
36
|
+
# Handler returns full user object with password
|
|
37
|
+
user = {
|
|
38
|
+
id: 1,
|
|
39
|
+
username: input[:body][:username],
|
|
40
|
+
email: input[:body][:email],
|
|
41
|
+
password: input[:body][:password], # This will be filtered!
|
|
42
|
+
age: input[:body][:age]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[user, 201]
|
|
46
|
+
# Response sent to client:
|
|
47
|
+
# { "id": 1, "username": "...", "email": "...", "age": ... }
|
|
48
|
+
# Password is automatically removed!
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Option 2: Return Schema Result Directly
|
|
53
|
+
|
|
54
|
+
You can also call the schema in your handler and return the result directly:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
api.post '/users', body: UserInput do |input, req, task|
|
|
58
|
+
user_data = {
|
|
59
|
+
id: 1,
|
|
60
|
+
username: input[:body][:username],
|
|
61
|
+
email: input[:body][:email],
|
|
62
|
+
password: input[:body][:password],
|
|
63
|
+
age: input[:body][:age]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Call schema and return result
|
|
67
|
+
result = UserOutput.call(user_data)
|
|
68
|
+
[result, 201]
|
|
69
|
+
# Framework automatically extracts result.to_h
|
|
70
|
+
# Password is filtered by the schema!
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This approach gives you more flexibility in the handler:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
api.post '/users', body: UserInput do |input, req, task|
|
|
78
|
+
user_data = {
|
|
79
|
+
id: 1,
|
|
80
|
+
username: input[:body][:username],
|
|
81
|
+
email: input[:body][:email],
|
|
82
|
+
password: input[:body][:password],
|
|
83
|
+
age: input[:body][:age]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Validate and filter in handler
|
|
87
|
+
result = UserOutput.call(user_data)
|
|
88
|
+
|
|
89
|
+
# Check if validation succeeded
|
|
90
|
+
if result.success?
|
|
91
|
+
[result, 201]
|
|
92
|
+
else
|
|
93
|
+
# Handle validation errors
|
|
94
|
+
raise FunApi::HTTPException.new(
|
|
95
|
+
status_code: 500,
|
|
96
|
+
detail: "Invalid response data: #{result.errors.to_h}"
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Array Responses
|
|
103
|
+
|
|
104
|
+
You can use both approaches with arrays too:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
ItemSchema = FunApi::Schema.define do
|
|
108
|
+
required(:name).filled(:string)
|
|
109
|
+
required(:price).filled(:float)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Option 1: Using response_schema parameter
|
|
113
|
+
api.post '/items/batch',
|
|
114
|
+
body: [ItemSchema],
|
|
115
|
+
response_schema: [ItemSchema] do |input, req, task|
|
|
116
|
+
|
|
117
|
+
items = input[:body].map do |item_data|
|
|
118
|
+
{
|
|
119
|
+
name: item_data[:name],
|
|
120
|
+
price: item_data[:price],
|
|
121
|
+
internal_cost: item_data[:price] * 0.5 # This will be filtered!
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
[items, 201]
|
|
126
|
+
# internal_cost is filtered from all items in the response
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Option 2: Return array of schema results
|
|
130
|
+
api.post '/items/batch', body: [ItemSchema] do |input, req, task|
|
|
131
|
+
results = input[:body].map do |item_data|
|
|
132
|
+
data = {
|
|
133
|
+
name: item_data[:name],
|
|
134
|
+
price: item_data[:price],
|
|
135
|
+
internal_cost: item_data[:price] * 0.5
|
|
136
|
+
}
|
|
137
|
+
ItemSchema.call(data) # Returns schema result
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
[results, 201]
|
|
141
|
+
# Framework converts array of results to array of hashes
|
|
142
|
+
# internal_cost is filtered by schema
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## How It Works
|
|
147
|
+
|
|
148
|
+
### 1. Request Flow
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
Request → Input Validation → Handler Execution → Response Validation → Client
|
|
152
|
+
↓ ↓
|
|
153
|
+
body/query schema response_schema
|
|
154
|
+
(validates input) (validates & filters output)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 2. Validation Behavior
|
|
158
|
+
|
|
159
|
+
**For Responses:**
|
|
160
|
+
- ✅ **Missing required fields** → Returns 500 error (your app code is broken)
|
|
161
|
+
- ✅ **Extra fields** → Automatically filtered out (security)
|
|
162
|
+
- ✅ **Wrong types** → Returns 500 error (your app code is broken)
|
|
163
|
+
|
|
164
|
+
**For Requests (body/query):**
|
|
165
|
+
- ✅ **Missing required fields** → Returns 422 error (client error)
|
|
166
|
+
- ✅ **Wrong types** → Returns 422 error (client error)
|
|
167
|
+
|
|
168
|
+
### 3. Array Support
|
|
169
|
+
|
|
170
|
+
Both input validation and response validation support arrays:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# Input array validation
|
|
174
|
+
api.post '/items', body: [ItemSchema] do |input, req, task|
|
|
175
|
+
# input[:body] is an array of validated hashes
|
|
176
|
+
items = input[:body].map { |item| create_item(item) }
|
|
177
|
+
[items, 201]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Output array validation
|
|
181
|
+
api.get '/items', response_schema: [ItemSchema] do |input, req, task|
|
|
182
|
+
items = fetch_all_items() # Returns array
|
|
183
|
+
[items, 200] # Each item validated and filtered
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Examples
|
|
188
|
+
|
|
189
|
+
### Example 1: Filtering Sensitive Data
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# Schema with password
|
|
193
|
+
UserWithPassword = FunApi::Schema.define do
|
|
194
|
+
required(:id).filled(:integer)
|
|
195
|
+
required(:username).filled(:string)
|
|
196
|
+
required(:password).filled(:string)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Public schema without password
|
|
200
|
+
PublicUser = FunApi::Schema.define do
|
|
201
|
+
required(:id).filled(:integer)
|
|
202
|
+
required(:username).filled(:string)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
api.get '/users/:id', response_schema: PublicUser do |input, req, task|
|
|
206
|
+
# Fetch from database returns password
|
|
207
|
+
user = db.fetch_user(input[:path]['id'])
|
|
208
|
+
# { id: 1, username: "john", password: "hashed_password" }
|
|
209
|
+
|
|
210
|
+
[user, 200]
|
|
211
|
+
# Client receives: { "id": 1, "username": "john" }
|
|
212
|
+
# Password automatically filtered!
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Example 2: Batch Operations
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
CreateUser = FunApi::Schema.define do
|
|
220
|
+
required(:username).filled(:string)
|
|
221
|
+
required(:email).filled(:string)
|
|
222
|
+
required(:password).filled(:string)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
UserResponse = FunApi::Schema.define do
|
|
226
|
+
required(:id).filled(:integer)
|
|
227
|
+
required(:username).filled(:string)
|
|
228
|
+
required(:email).filled(:string)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
api.post '/users/batch',
|
|
232
|
+
body: [CreateUser],
|
|
233
|
+
response_schema: [UserResponse] do |input, req, task|
|
|
234
|
+
|
|
235
|
+
users = input[:body].map do |user_data|
|
|
236
|
+
create_user(user_data) # Returns user with password
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
[users, 201]
|
|
240
|
+
# All passwords filtered from response array
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Example 3: Optional Response Schema
|
|
245
|
+
|
|
246
|
+
`response_schema` is optional. If not provided, data is returned as-is:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
# Without response_schema - returns all fields
|
|
250
|
+
api.get '/debug/user/:id' do |input, req, task|
|
|
251
|
+
user = fetch_user(input[:path]['id'])
|
|
252
|
+
[user, 200] # Returns everything, including sensitive fields
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# With response_schema - filters fields
|
|
256
|
+
api.get '/users/:id', response_schema: UserOutput do |input, req, task|
|
|
257
|
+
user = fetch_user(input[:path]['id'])
|
|
258
|
+
[user, 200] # Filters according to schema
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Error Handling
|
|
263
|
+
|
|
264
|
+
### Response Validation Errors (500)
|
|
265
|
+
|
|
266
|
+
If your handler returns data that doesn't match the `response_schema`, a 500 error is returned:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
api.get '/users/:id', response_schema: UserOutput do |input, req, task|
|
|
270
|
+
# Oops! Missing required 'id' field
|
|
271
|
+
user = { username: "john", email: "john@example.com" }
|
|
272
|
+
[user, 200]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Client receives:
|
|
276
|
+
# Status: 500
|
|
277
|
+
# { "detail": "Response validation failed: {id: [\"is missing\"]}" }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
This indicates a **bug in your application code** - you promised to return data with certain fields but didn't.
|
|
281
|
+
|
|
282
|
+
### Input Validation Errors (422)
|
|
283
|
+
|
|
284
|
+
Input validation errors return 422 (client error):
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
api.post '/users', body: UserInput do |input, req, task|
|
|
288
|
+
# ... handler code
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Client sends invalid data:
|
|
292
|
+
# POST /users { "username": "john" } // missing email
|
|
293
|
+
|
|
294
|
+
# Response:
|
|
295
|
+
# Status: 422
|
|
296
|
+
# {
|
|
297
|
+
# "detail": [
|
|
298
|
+
# {
|
|
299
|
+
# "loc": ["body", "email"],
|
|
300
|
+
# "msg": "is missing",
|
|
301
|
+
# "type": "value_error"
|
|
302
|
+
# }
|
|
303
|
+
# ]
|
|
304
|
+
# }
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Best Practices
|
|
308
|
+
|
|
309
|
+
1. **Choose the right approach**:
|
|
310
|
+
- Use `response_schema:` parameter when you want automatic validation
|
|
311
|
+
- Return schema results directly when you need more control or conditional logic
|
|
312
|
+
- Combine both for maximum safety (schema result + response_schema validation)
|
|
313
|
+
|
|
314
|
+
2. **Always filter sensitive data** - Use response_schema or schema results to prevent data leaks
|
|
315
|
+
|
|
316
|
+
3. **Separate input and output schemas** - Often input requires password, output doesn't
|
|
317
|
+
|
|
318
|
+
4. **Reuse schemas** - Define once, use for both validation and documentation
|
|
319
|
+
|
|
320
|
+
5. **Use arrays consistently** - `[Schema]` for both input and output arrays
|
|
321
|
+
|
|
322
|
+
6. **Let validation fail** - Don't catch response validation errors, they indicate bugs
|
|
323
|
+
|
|
324
|
+
## Implementation Details
|
|
325
|
+
|
|
326
|
+
### Powered by dry-schema
|
|
327
|
+
|
|
328
|
+
Both input and response validation use `dry-schema`:
|
|
329
|
+
- Consistent API for defining schemas
|
|
330
|
+
- Automatic type coercion
|
|
331
|
+
- Detailed error messages
|
|
332
|
+
- Built-in validation rules
|
|
333
|
+
|
|
334
|
+
### Filtering Mechanism
|
|
335
|
+
|
|
336
|
+
dry-schema automatically filters data:
|
|
337
|
+
```ruby
|
|
338
|
+
# Schema only defines these fields
|
|
339
|
+
schema = FunApi::Schema.define do
|
|
340
|
+
required(:name).filled(:string)
|
|
341
|
+
required(:email).filled(:string)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Data has extra fields
|
|
345
|
+
data = { name: "John", email: "john@example.com", password: "secret", admin: true }
|
|
346
|
+
|
|
347
|
+
# Calling schema.call(data).to_h returns:
|
|
348
|
+
# { name: "John", email: "john@example.com" }
|
|
349
|
+
# Extra fields automatically removed!
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Schema Result Detection
|
|
353
|
+
|
|
354
|
+
When you return a `Dry::Schema::Result` object, the framework automatically detects it and extracts the hash:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
# Handler returns schema result
|
|
358
|
+
api.post '/users' do |input, req, task|
|
|
359
|
+
result = UserOutput.call(user_data)
|
|
360
|
+
[result, 201] # Returns Dry::Schema::Result
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Framework checks:
|
|
364
|
+
# 1. Is payload a Dry::Schema::Result? Yes
|
|
365
|
+
# 2. Extract: result.to_h
|
|
366
|
+
# 3. Send filtered hash to client
|
|
367
|
+
|
|
368
|
+
# Works with arrays too:
|
|
369
|
+
api.post '/users/batch' do |input, req, task|
|
|
370
|
+
results = users.map { |u| UserOutput.call(u) }
|
|
371
|
+
[results, 201] # Array of Dry::Schema::Result
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Framework maps each result to .to_h automatically
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Future Enhancements
|
|
378
|
+
|
|
379
|
+
- OpenAPI/Swagger documentation generation from schemas
|
|
380
|
+
- `response_schema_exclude_unset` option (exclude default values)
|
|
381
|
+
- `response_schema_include/exclude` options for field filtering
|
|
382
|
+
- Response streaming support
|
|
383
|
+
- Content negotiation (JSON, XML, etc.)
|