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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/25-09-01-OPENAPI_IMPLEMENTATION.md +233 -0
  3. data/.claude/25-09-05-RESPONSE_SCHEMA.md +383 -0
  4. data/.claude/25-09-10-OPENAPI_PLAN.md +219 -0
  5. data/.claude/25-10-26-MIDDLEWARE_IMPLEMENTATION.md +230 -0
  6. data/.claude/25-10-26-MIDDLEWARE_PLAN.md +353 -0
  7. data/.claude/25-10-27-BACKGROUND_TASKS_ANALYSIS.md +325 -0
  8. data/.claude/25-10-27-DEPENDENCY_IMPLEMENTATION_SUMMARY.md +325 -0
  9. data/.claude/25-10-27-DEPENDENCY_INJECTION_PLAN.md +753 -0
  10. data/.claude/25-12-24-LIFECYCLE_HOOKS_PLAN.md +421 -0
  11. data/.claude/25-12-24-PUBLISHING_AND_DOGFOODING_PLAN.md +327 -0
  12. data/.claude/25-12-24-TEMPLATE_RENDERING_PLAN.md +704 -0
  13. data/.claude/DECISIONS.md +397 -0
  14. data/.claude/PROJECT_PLAN.md +80 -0
  15. data/.claude/TESTING_PLAN.md +285 -0
  16. data/.claude/TESTING_STATUS.md +157 -0
  17. data/.tool-versions +1 -0
  18. data/AGENTS.md +416 -0
  19. data/CHANGELOG.md +5 -0
  20. data/CODE_OF_CONDUCT.md +132 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +660 -0
  23. data/Rakefile +10 -0
  24. data/docs +8 -0
  25. data/docs-site/.gitignore +3 -0
  26. data/docs-site/Gemfile +9 -0
  27. data/docs-site/app.rb +138 -0
  28. data/docs-site/content/essential/handler.md +156 -0
  29. data/docs-site/content/essential/lifecycle.md +161 -0
  30. data/docs-site/content/essential/middleware.md +201 -0
  31. data/docs-site/content/essential/openapi.md +155 -0
  32. data/docs-site/content/essential/routing.md +123 -0
  33. data/docs-site/content/essential/validation.md +166 -0
  34. data/docs-site/content/getting-started/at-glance.md +82 -0
  35. data/docs-site/content/getting-started/key-concepts.md +150 -0
  36. data/docs-site/content/getting-started/quick-start.md +127 -0
  37. data/docs-site/content/index.md +81 -0
  38. data/docs-site/content/patterns/async-operations.md +137 -0
  39. data/docs-site/content/patterns/background-tasks.md +143 -0
  40. data/docs-site/content/patterns/database.md +175 -0
  41. data/docs-site/content/patterns/dependencies.md +141 -0
  42. data/docs-site/content/patterns/deployment.md +212 -0
  43. data/docs-site/content/patterns/error-handling.md +184 -0
  44. data/docs-site/content/patterns/response-schema.md +159 -0
  45. data/docs-site/content/patterns/templates.md +193 -0
  46. data/docs-site/content/patterns/testing.md +218 -0
  47. data/docs-site/mise.toml +2 -0
  48. data/docs-site/public/css/style.css +234 -0
  49. data/docs-site/templates/layouts/docs.html.erb +28 -0
  50. data/docs-site/templates/page.html.erb +3 -0
  51. data/docs-site/templates/partials/_nav.html.erb +19 -0
  52. data/examples/background_tasks_demo.rb +159 -0
  53. data/examples/demo_middleware.rb +55 -0
  54. data/examples/demo_openapi.rb +63 -0
  55. data/examples/dependency_block_demo.rb +150 -0
  56. data/examples/dependency_cleanup_demo.rb +146 -0
  57. data/examples/dependency_injection_demo.rb +200 -0
  58. data/examples/lifecycle_demo.rb +57 -0
  59. data/examples/middleware_demo.rb +74 -0
  60. data/examples/templates/layouts/application.html.erb +66 -0
  61. data/examples/templates/todos/_todo.html.erb +15 -0
  62. data/examples/templates/todos/index.html.erb +12 -0
  63. data/examples/templates_demo.rb +87 -0
  64. data/lib/funapi/application.rb +521 -0
  65. data/lib/funapi/async.rb +57 -0
  66. data/lib/funapi/background_tasks.rb +52 -0
  67. data/lib/funapi/config.rb +23 -0
  68. data/lib/funapi/database/sequel/fibered_connection_pool.rb +87 -0
  69. data/lib/funapi/dependency_wrapper.rb +66 -0
  70. data/lib/funapi/depends.rb +138 -0
  71. data/lib/funapi/exceptions.rb +72 -0
  72. data/lib/funapi/middleware/base.rb +13 -0
  73. data/lib/funapi/middleware/cors.rb +23 -0
  74. data/lib/funapi/middleware/request_logger.rb +32 -0
  75. data/lib/funapi/middleware/trusted_host.rb +34 -0
  76. data/lib/funapi/middleware.rb +4 -0
  77. data/lib/funapi/openapi/schema_converter.rb +85 -0
  78. data/lib/funapi/openapi/spec_generator.rb +179 -0
  79. data/lib/funapi/router.rb +43 -0
  80. data/lib/funapi/schema.rb +65 -0
  81. data/lib/funapi/server/falcon.rb +38 -0
  82. data/lib/funapi/template_response.rb +17 -0
  83. data/lib/funapi/templates.rb +111 -0
  84. data/lib/funapi/version.rb +5 -0
  85. data/lib/funapi.rb +14 -0
  86. data/sig/fun_api.rbs +499 -0
  87. 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.)