rapitapir 0.1.1 ā 2.0.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 +4 -4
- data/.rubocop.yml +7 -7
- data/.rubocop_todo.yml +83 -0
- data/README.md +1319 -235
- data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
- data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
- data/docs/SINATRA_EXTENSION.md +399 -348
- data/docs/STRICT_VALIDATION.md +229 -0
- data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
- data/docs/ai-integration-plan.md +112 -0
- data/docs/auto-derivation.md +505 -92
- data/docs/endpoint-definition.md +536 -129
- data/docs/n8n-integration.md +212 -0
- data/docs/observability.md +810 -500
- data/docs/using-mcp.md +93 -0
- data/examples/ai/knowledge_base_rag.rb +83 -0
- data/examples/ai/user_management_mcp.rb +92 -0
- data/examples/ai/user_validation_llm.rb +187 -0
- data/examples/rails/RAILS_8_GUIDE.md +165 -0
- data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
- data/examples/rails/README.md +497 -0
- data/examples/rails/comprehensive_test.rb +91 -0
- data/examples/rails/config/routes.rb +48 -0
- data/examples/rails/debug_controller.rb +63 -0
- data/examples/rails/detailed_test.rb +46 -0
- data/examples/rails/enhanced_users_controller.rb +278 -0
- data/examples/rails/final_server_test.rb +50 -0
- data/examples/rails/hello_world_app.rb +116 -0
- data/examples/rails/hello_world_controller.rb +186 -0
- data/examples/rails/hello_world_routes.rb +28 -0
- data/examples/rails/rails8_minimal_demo.rb +132 -0
- data/examples/rails/rails8_simple_demo.rb +140 -0
- data/examples/rails/rails8_working_demo.rb +255 -0
- data/examples/rails/real_world_blog_api.rb +510 -0
- data/examples/rails/server_test.rb +46 -0
- data/examples/rails/test_direct_processing.rb +41 -0
- data/examples/rails/test_hello_world.rb +80 -0
- data/examples/rails/test_rails_integration.rb +54 -0
- data/examples/rails/traditional_app/Gemfile +37 -0
- data/examples/rails/traditional_app/README.md +265 -0
- data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
- data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
- data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
- data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
- data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
- data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
- data/examples/rails/traditional_app/config/routes.rb +25 -0
- data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
- data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
- data/examples/rails/traditional_app_runnable.rb +406 -0
- data/examples/rails/users_controller.rb +4 -1
- data/examples/serverless/Gemfile +43 -0
- data/examples/serverless/QUICKSTART.md +331 -0
- data/examples/serverless/README.md +520 -0
- data/examples/serverless/aws_lambda_example.rb +307 -0
- data/examples/serverless/aws_sam_template.yaml +215 -0
- data/examples/serverless/azure_functions_example.rb +407 -0
- data/examples/serverless/deploy.rb +204 -0
- data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
- data/examples/serverless/gcp_function.yaml +23 -0
- data/examples/serverless/host.json +24 -0
- data/examples/serverless/package.json +32 -0
- data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
- data/examples/serverless/spec/spec_helper.rb +89 -0
- data/examples/serverless/vercel.json +31 -0
- data/examples/serverless/vercel_example.rb +404 -0
- data/examples/strict_validation_examples.rb +104 -0
- data/examples/validation_error_examples.rb +173 -0
- data/lib/rapitapir/ai/llm_instruction.rb +456 -0
- data/lib/rapitapir/ai/mcp.rb +134 -0
- data/lib/rapitapir/ai/rag.rb +287 -0
- data/lib/rapitapir/ai/rag_middleware.rb +147 -0
- data/lib/rapitapir/auth/oauth2.rb +43 -57
- data/lib/rapitapir/cli/command.rb +362 -2
- data/lib/rapitapir/cli/mcp_export.rb +18 -0
- data/lib/rapitapir/cli/validator.rb +2 -6
- data/lib/rapitapir/core/endpoint.rb +59 -6
- data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
- data/lib/rapitapir/endpoint_registry.rb +47 -0
- data/lib/rapitapir/observability/health_check.rb +4 -4
- data/lib/rapitapir/observability/logging.rb +10 -10
- data/lib/rapitapir/schema.rb +2 -2
- data/lib/rapitapir/server/rack_adapter.rb +1 -3
- data/lib/rapitapir/server/rails/configuration.rb +77 -0
- data/lib/rapitapir/server/rails/controller_base.rb +185 -0
- data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
- data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
- data/lib/rapitapir/server/rails/routes.rb +114 -0
- data/lib/rapitapir/server/rails_adapter.rb +10 -3
- data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
- data/lib/rapitapir/server/rails_controller.rb +1 -3
- data/lib/rapitapir/server/rails_integration.rb +67 -0
- data/lib/rapitapir/server/rails_response_handler.rb +16 -3
- data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
- data/lib/rapitapir/server/sinatra_integration.rb +4 -4
- data/lib/rapitapir/sinatra/extension.rb +2 -2
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
- data/lib/rapitapir/types/array.rb +4 -0
- data/lib/rapitapir/types/auto_derivation.rb +4 -18
- data/lib/rapitapir/types/datetime.rb +1 -3
- data/lib/rapitapir/types/float.rb +2 -6
- data/lib/rapitapir/types/hash.rb +40 -2
- data/lib/rapitapir/types/integer.rb +4 -12
- data/lib/rapitapir/types/object.rb +6 -2
- data/lib/rapitapir/types.rb +6 -2
- data/lib/rapitapir/version.rb +1 -1
- data/lib/rapitapir.rb +5 -3
- data/rapitapir.gemspec +7 -5
- metadata +116 -16
@@ -0,0 +1,497 @@
|
|
1
|
+
# Using RapiTapir in Real Rails Applications
|
2
|
+
|
3
|
+
This guide demonstrates how to integrate RapiTapir into real-world Rails 8+ applications for type-safe, documented APIs.
|
4
|
+
|
5
|
+
## š Quick Start
|
6
|
+
|
7
|
+
### 1. Add RapiTapir to your Gemfile
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'rapitapir', '~> 1.0'
|
11
|
+
```
|
12
|
+
|
13
|
+
### 2. Update your ApplicationController
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
class ApplicationController < RapiTapir::Server::Rails::ControllerBase
|
17
|
+
rapitapir do
|
18
|
+
development_defaults! if Rails.env.development?
|
19
|
+
|
20
|
+
# Global error responses
|
21
|
+
error_out(json_body(error: T.string), 401)
|
22
|
+
error_out(json_body(error: T.string), 403)
|
23
|
+
error_out(json_body(error: T.string), 404)
|
24
|
+
error_out(json_body(error: T.string, errors: T.array(T.string).optional), 422)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
### 3. Create API Controllers
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class Api::V1::UsersController < ApplicationController
|
33
|
+
rapitapir do
|
34
|
+
user_type = T.hash(
|
35
|
+
id: T.integer,
|
36
|
+
name: T.string,
|
37
|
+
email: T.string,
|
38
|
+
created_at: T.string
|
39
|
+
)
|
40
|
+
|
41
|
+
GET('/api/v1/users')
|
42
|
+
.in(query(:page, T.integer.default(1)))
|
43
|
+
.out(json_body(users: T.array(user_type)))
|
44
|
+
.summary("List users")
|
45
|
+
.tag("Users")
|
46
|
+
|
47
|
+
POST('/api/v1/users')
|
48
|
+
.in(json_body(name: T.string, email: T.string))
|
49
|
+
.out(json_body(user: user_type), 201)
|
50
|
+
.summary("Create user")
|
51
|
+
.tag("Users")
|
52
|
+
end
|
53
|
+
|
54
|
+
def list_users
|
55
|
+
users = User.page(inputs[:page])
|
56
|
+
{ users: users.map(&method(:serialize_user)) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_user
|
60
|
+
user = User.create!(inputs.slice(:name, :email))
|
61
|
+
render json: { user: serialize_user(user) }, status: 201
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def serialize_user(user)
|
67
|
+
{
|
68
|
+
id: user.id,
|
69
|
+
name: user.name,
|
70
|
+
email: user.email,
|
71
|
+
created_at: user.created_at.iso8601
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
### 4. Update Routes
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
Rails.application.routes.draw do
|
81
|
+
namespace :api do
|
82
|
+
namespace :v1 do
|
83
|
+
rapitapir_routes_for 'Api::V1::UsersController'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Documentation (development only)
|
88
|
+
if Rails.env.development?
|
89
|
+
get '/docs', to: 'documentation#swagger_ui'
|
90
|
+
get '/openapi.json', to: 'documentation#openapi_spec'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
Use the new `ControllerBase` class for the cleanest syntax:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
# app/controllers/users_controller.rb
|
99
|
+
require 'rapitapir/server/rails/controller_base'
|
100
|
+
|
101
|
+
class UsersController < RapiTapir::Server::Rails::ControllerBase
|
102
|
+
rapitapir do
|
103
|
+
info(title: 'Users API', version: '1.0.0')
|
104
|
+
# development_defaults! # Coming soon
|
105
|
+
end
|
106
|
+
|
107
|
+
USER_SCHEMA = T.hash({
|
108
|
+
"id" => T.integer,
|
109
|
+
"name" => T.string,
|
110
|
+
"email" => T.email
|
111
|
+
})
|
112
|
+
|
113
|
+
api_resource '/users', schema: USER_SCHEMA do
|
114
|
+
crud do
|
115
|
+
index { User.all.map(&:attributes) }
|
116
|
+
show { |inputs| User.find(inputs[:id]).attributes }
|
117
|
+
create { |inputs| User.create!(inputs[:body]).attributes }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
### 3. Auto-Generate Routes
|
124
|
+
|
125
|
+
Add to your `config/routes.rb`:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
Rails.application.routes.draw do
|
129
|
+
# Option 1: Auto-generate routes for specific controller
|
130
|
+
rapitapir_routes_for UsersController
|
131
|
+
|
132
|
+
# Option 2: Auto-discover all RapiTapir controllers
|
133
|
+
rapitapir_auto_routes
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
### 4. Access Documentation
|
138
|
+
|
139
|
+
Start your Rails server and visit:
|
140
|
+
- Interactive Documentation: `http://localhost:3000/docs`
|
141
|
+
- OpenAPI Specification: `http://localhost:3000/openapi.json`
|
142
|
+
|
143
|
+
## šļø Core Features
|
144
|
+
|
145
|
+
### Clean Base Class Syntax
|
146
|
+
|
147
|
+
The new `ControllerBase` provides inheritance-based setup:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class ApiController < RapiTapir::Server::Rails::ControllerBase
|
151
|
+
rapitapir do
|
152
|
+
info(title: 'My API', version: '1.0.0')
|
153
|
+
end
|
154
|
+
|
155
|
+
# T shortcut automatically available
|
156
|
+
# Enhanced HTTP verb DSL automatically available
|
157
|
+
# Auto action generation
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
### Enhanced HTTP Verb DSL
|
162
|
+
|
163
|
+
Use the same fluent DSL as Sinatra:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
endpoint(
|
167
|
+
GET('/books/:id')
|
168
|
+
.summary('Get book by ID')
|
169
|
+
.path_param(:id, T.integer(minimum: 1))
|
170
|
+
.ok(BOOK_SCHEMA)
|
171
|
+
.not_found(T.hash({ "error" => T.string }))
|
172
|
+
.build
|
173
|
+
) do |inputs|
|
174
|
+
book = Book.find(inputs[:id])
|
175
|
+
book ? book.attributes : (render json: {error: 'Not found'}, status: 404)
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
### RESTful Resource Builder
|
180
|
+
|
181
|
+
Create complete CRUD APIs with minimal code:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
api_resource '/books', schema: BOOK_SCHEMA do
|
185
|
+
crud do
|
186
|
+
index do
|
187
|
+
books = Book.all
|
188
|
+
books = books.where(published: true) if params[:published] == 'true'
|
189
|
+
books.limit(params[:limit] || 50).map(&:attributes)
|
190
|
+
end
|
191
|
+
|
192
|
+
show { |inputs| Book.find(inputs[:id]).attributes }
|
193
|
+
|
194
|
+
create do |inputs|
|
195
|
+
book = Book.create!(inputs[:body])
|
196
|
+
response.status = 201
|
197
|
+
book.attributes
|
198
|
+
end
|
199
|
+
|
200
|
+
update { |inputs| Book.update!(inputs[:id], inputs[:body]).attributes }
|
201
|
+
destroy { |inputs| Book.destroy(inputs[:id]); head :no_content }
|
202
|
+
end
|
203
|
+
|
204
|
+
# Add custom endpoints
|
205
|
+
custom :get, 'featured' do
|
206
|
+
Book.where(featured: true).map(&:attributes)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
### Automatic Route Generation
|
212
|
+
|
213
|
+
Three options for route generation:
|
214
|
+
|
215
|
+
#### Option 1: Per-Controller Generation
|
216
|
+
```ruby
|
217
|
+
# config/routes.rb
|
218
|
+
Rails.application.routes.draw do
|
219
|
+
rapitapir_routes_for UsersController
|
220
|
+
rapitapir_routes_for BooksController
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
#### Option 2: Auto-Discovery
|
225
|
+
```ruby
|
226
|
+
# config/routes.rb
|
227
|
+
Rails.application.routes.draw do
|
228
|
+
rapitapir_auto_routes # Finds all RapiTapir controllers
|
229
|
+
end
|
230
|
+
```
|
231
|
+
|
232
|
+
#### Option 3: Manual Routes (Still Supported)
|
233
|
+
```ruby
|
234
|
+
# config/routes.rb
|
235
|
+
Rails.application.routes.draw do
|
236
|
+
resources :users, only: [:index, :show, :create, :update, :destroy]
|
237
|
+
end
|
238
|
+
```
|
239
|
+
|
240
|
+
## š Migration from Legacy Approach
|
241
|
+
|
242
|
+
### Before (Verbose)
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
class UsersController < ApplicationController
|
246
|
+
include RapiTapir::Server::Rails::Controller
|
247
|
+
|
248
|
+
rapitapir_endpoint :index, RapiTapir.get('/users')
|
249
|
+
.summary('List all users')
|
250
|
+
.out(RapiTapir::Core::Output.new(
|
251
|
+
kind: :json, type: { users: Array }
|
252
|
+
)) do |_inputs|
|
253
|
+
{ users: @users.values }
|
254
|
+
end
|
255
|
+
|
256
|
+
def index
|
257
|
+
process_rapitapir_endpoint
|
258
|
+
end
|
259
|
+
end
|
260
|
+
```
|
261
|
+
|
262
|
+
### After (Clean)
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
class UsersController < RapiTapir::Server::Rails::ControllerBase
|
266
|
+
rapitapir do
|
267
|
+
info(title: 'Users API', version: '1.0.0')
|
268
|
+
end
|
269
|
+
|
270
|
+
USER_SCHEMA = T.hash({ "id" => T.integer, "name" => T.string })
|
271
|
+
|
272
|
+
api_resource '/users', schema: USER_SCHEMA do
|
273
|
+
crud do
|
274
|
+
index { User.all.map(&:attributes) }
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
## š§ Advanced Features
|
281
|
+
|
282
|
+
### Custom Endpoints with Full Type Safety
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
endpoint(
|
286
|
+
POST('/users/bulk')
|
287
|
+
.summary('Create multiple users')
|
288
|
+
.json_body(T.array(USER_CREATE_SCHEMA))
|
289
|
+
.created(T.array(USER_SCHEMA))
|
290
|
+
.bad_request(ERROR_SCHEMA)
|
291
|
+
.build
|
292
|
+
) do |inputs|
|
293
|
+
users = inputs[:body].map { |user_data| User.create!(user_data) }
|
294
|
+
response.status = 201
|
295
|
+
users.map(&:attributes)
|
296
|
+
end
|
297
|
+
```
|
298
|
+
|
299
|
+
### Search Endpoints
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
endpoint(
|
303
|
+
GET('/users/search')
|
304
|
+
.summary('Search users')
|
305
|
+
.query(:q, T.string(min_length: 1))
|
306
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)))
|
307
|
+
.ok(T.array(USER_SCHEMA))
|
308
|
+
.build
|
309
|
+
) do |inputs|
|
310
|
+
results = User.where("name ILIKE ?", "%#{inputs[:q]}%")
|
311
|
+
results = results.limit(inputs[:limit]) if inputs[:limit]
|
312
|
+
results.map(&:attributes)
|
313
|
+
end
|
314
|
+
```
|
315
|
+
|
316
|
+
### Error Handling
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
show do |inputs|
|
320
|
+
user = User.find_by(id: inputs[:id])
|
321
|
+
|
322
|
+
if user.nil?
|
323
|
+
render json: { error: 'User not found' }, status: :not_found
|
324
|
+
return
|
325
|
+
end
|
326
|
+
|
327
|
+
user.attributes
|
328
|
+
end
|
329
|
+
```
|
330
|
+
|
331
|
+
## š Complete Example
|
332
|
+
|
333
|
+
Here's a full-featured Rails controller using all the enhanced features:
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
# app/controllers/books_controller.rb
|
337
|
+
require 'rapitapir/server/rails/controller_base'
|
338
|
+
|
339
|
+
class BooksController < RapiTapir::Server::Rails::ControllerBase
|
340
|
+
rapitapir do
|
341
|
+
info(
|
342
|
+
title: 'Books API',
|
343
|
+
description: 'A comprehensive book management API',
|
344
|
+
version: '1.0.0'
|
345
|
+
)
|
346
|
+
end
|
347
|
+
|
348
|
+
BOOK_SCHEMA = T.hash({
|
349
|
+
"id" => T.integer,
|
350
|
+
"title" => T.string(min_length: 1, max_length: 255),
|
351
|
+
"author" => T.string(min_length: 1),
|
352
|
+
"published" => T.boolean,
|
353
|
+
"isbn" => T.optional(T.string),
|
354
|
+
"pages" => T.optional(T.integer(minimum: 1)),
|
355
|
+
"created_at" => T.datetime,
|
356
|
+
"updated_at" => T.datetime
|
357
|
+
})
|
358
|
+
|
359
|
+
BOOK_CREATE_SCHEMA = T.hash({
|
360
|
+
"title" => T.string(min_length: 1, max_length: 255),
|
361
|
+
"author" => T.string(min_length: 1),
|
362
|
+
"published" => T.optional(T.boolean),
|
363
|
+
"isbn" => T.optional(T.string),
|
364
|
+
"pages" => T.optional(T.integer(minimum: 1))
|
365
|
+
})
|
366
|
+
|
367
|
+
# CRUD operations with enhanced resource builder
|
368
|
+
api_resource '/books', schema: BOOK_SCHEMA do
|
369
|
+
crud do
|
370
|
+
index do
|
371
|
+
books = Book.includes(:author)
|
372
|
+
books = books.where(published: true) if params[:published] == 'true'
|
373
|
+
books = books.where('title ILIKE ?', "%#{params[:search]}%") if params[:search]
|
374
|
+
|
375
|
+
limit = params[:limit]&.to_i || 20
|
376
|
+
offset = params[:offset]&.to_i || 0
|
377
|
+
|
378
|
+
books.offset(offset).limit(limit).map(&:attributes)
|
379
|
+
end
|
380
|
+
|
381
|
+
show do |inputs|
|
382
|
+
book = Book.find_by(id: inputs[:id])
|
383
|
+
book ? book.attributes : (render json: {error: 'Book not found'}, status: 404)
|
384
|
+
end
|
385
|
+
|
386
|
+
create do |inputs|
|
387
|
+
book = Book.create!(inputs[:body])
|
388
|
+
response.status = 201
|
389
|
+
book.attributes
|
390
|
+
end
|
391
|
+
|
392
|
+
update do |inputs|
|
393
|
+
book = Book.find_by(id: inputs[:id])
|
394
|
+
|
395
|
+
if book.nil?
|
396
|
+
render json: { error: 'Book not found' }, status: 404
|
397
|
+
return
|
398
|
+
end
|
399
|
+
|
400
|
+
book.update!(inputs[:body])
|
401
|
+
book.attributes
|
402
|
+
end
|
403
|
+
|
404
|
+
destroy do |inputs|
|
405
|
+
book = Book.find_by(id: inputs[:id])
|
406
|
+
|
407
|
+
if book.nil?
|
408
|
+
render json: { error: 'Book not found' }, status: 404
|
409
|
+
return
|
410
|
+
end
|
411
|
+
|
412
|
+
book.destroy!
|
413
|
+
head :no_content
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# Custom endpoints
|
418
|
+
custom :get, 'featured' do
|
419
|
+
Book.where(featured: true).map(&:attributes)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
# Additional custom endpoints
|
424
|
+
endpoint(
|
425
|
+
GET('/books/search')
|
426
|
+
.summary('Advanced book search')
|
427
|
+
.query(:q, T.string(min_length: 1), description: 'Search query')
|
428
|
+
.query(:author, T.optional(T.string), description: 'Filter by author')
|
429
|
+
.query(:published, T.optional(T.boolean), description: 'Filter by published status')
|
430
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Results limit')
|
431
|
+
.ok(T.array(BOOK_SCHEMA))
|
432
|
+
.build
|
433
|
+
) do |inputs|
|
434
|
+
query = inputs[:q]
|
435
|
+
books = Book.where('title ILIKE ? OR description ILIKE ?', "%#{query}%", "%#{query}%")
|
436
|
+
books = books.joins(:author).where('authors.name ILIKE ?', "%#{inputs[:author]}%") if inputs[:author]
|
437
|
+
books = books.where(published: inputs[:published]) if inputs.key?(:published)
|
438
|
+
books = books.limit(inputs[:limit] || 20)
|
439
|
+
|
440
|
+
books.map(&:attributes)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
## š£ļø Routes Configuration
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
# config/routes.rb
|
449
|
+
Rails.application.routes.draw do
|
450
|
+
# Auto-generate all RapiTapir routes
|
451
|
+
rapitapir_auto_routes
|
452
|
+
|
453
|
+
# Or generate for specific controllers
|
454
|
+
# rapitapir_routes_for BooksController
|
455
|
+
# rapitapir_routes_for UsersController
|
456
|
+
end
|
457
|
+
```
|
458
|
+
|
459
|
+
## šÆ Benefits
|
460
|
+
|
461
|
+
### Compared to Legacy Rails Integration
|
462
|
+
|
463
|
+
| Feature | Legacy Approach | Enhanced Approach |
|
464
|
+
|---------|----------------|-------------------|
|
465
|
+
| **Base Class** | Manual include | Clean inheritance |
|
466
|
+
| **HTTP Verbs** | Verbose `RapiTapir.get()` | Clean `GET()` |
|
467
|
+
| **Type Shortcuts** | `RapiTapir::Types.string` | `T.string` |
|
468
|
+
| **Action Generation** | Manual `def action; process_rapitapir_endpoint; end` | Automatic |
|
469
|
+
| **Route Generation** | Manual Rails routes | Auto-generated |
|
470
|
+
| **CRUD Operations** | Individual endpoint definitions | `api_resource` with `crud` block |
|
471
|
+
| **Configuration** | Scattered setup | Single `rapitapir` block |
|
472
|
+
|
473
|
+
### Compared to Sinatra
|
474
|
+
|
475
|
+
| Feature | Sinatra | Enhanced Rails | Status |
|
476
|
+
|---------|---------|----------------|---------|
|
477
|
+
| **Clean Inheritance** | ā
`< SinatraRapiTapir` | ā
`< ControllerBase` | **Achieved** |
|
478
|
+
| **HTTP Verb DSL** | ā
`GET()`, `POST()` | ā
`GET()`, `POST()` | **Achieved** |
|
479
|
+
| **Resource Builder** | ā
`api_resource` | ā
`api_resource` | **Achieved** |
|
480
|
+
| **T Shortcuts** | ā
`T.string` | ā
`T.string` | **Achieved** |
|
481
|
+
| **Auto Routes** | ā
Automatic | ā
`rapitapir_auto_routes` | **Achieved** |
|
482
|
+
| **Documentation** | ā
`/docs` | š§ Coming soon | **In Progress** |
|
483
|
+
|
484
|
+
## š§ Coming Soon
|
485
|
+
|
486
|
+
- **Development Defaults**: Auto CORS, health checks, documentation
|
487
|
+
- **Built-in Documentation**: `/docs` endpoint for Rails apps
|
488
|
+
- **Authentication Helpers**: Bearer token, OAuth2 integration
|
489
|
+
- **Observability**: Metrics and tracing integration
|
490
|
+
- **Generator**: Rails generator for RapiTapir controllers
|
491
|
+
|
492
|
+
## š See Also
|
493
|
+
|
494
|
+
- [Enhanced Users Controller Example](enhanced_users_controller.rb)
|
495
|
+
- [Legacy Users Controller Example](users_controller.rb) (for comparison)
|
496
|
+
- [Sinatra Integration Guide](../docs/SINATRA_EXTENSION.md)
|
497
|
+
- [Type System Documentation](../docs/types.md)
|
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Comprehensive Rails Integration Validation
|
5
|
+
|
6
|
+
puts "šÆ RapiTapir Rails Integration - Comprehensive Test"
|
7
|
+
puts "=" * 55
|
8
|
+
|
9
|
+
success_count = 0
|
10
|
+
total_tests = 7
|
11
|
+
|
12
|
+
def test(description)
|
13
|
+
print "\n#{description}... "
|
14
|
+
begin
|
15
|
+
result = yield
|
16
|
+
puts "ā
"
|
17
|
+
return true
|
18
|
+
rescue => e
|
19
|
+
puts "ā #{e.message}"
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Test 1: Basic loading
|
25
|
+
success_count += 1 if test("Loading Rails integration") do
|
26
|
+
require_relative 'hello_world_app'
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Test 2: Controller inheritance
|
31
|
+
success_count += 1 if test("Controller inheritance working") do
|
32
|
+
HelloWorldController < RapiTapir::Server::Rails::ControllerBase
|
33
|
+
end
|
34
|
+
|
35
|
+
# Test 3: T shortcuts
|
36
|
+
success_count += 1 if test("T shortcuts available") do
|
37
|
+
type = HelloWorldController::T.string
|
38
|
+
type.is_a?(RapiTapir::Types::String)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Test 4: HTTP verbs
|
42
|
+
success_count += 1 if test("HTTP verb DSL working") do
|
43
|
+
builder = HelloWorldController.GET('/test')
|
44
|
+
builder.is_a?(RapiTapir::DSL::FluentEndpointBuilder)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Test 5: Endpoints registered
|
48
|
+
success_count += 1 if test("Endpoints properly registered") do
|
49
|
+
endpoints = HelloWorldController.rapitapir_endpoints
|
50
|
+
endpoints.is_a?(Hash) && endpoints.count == 4
|
51
|
+
end
|
52
|
+
|
53
|
+
# Test 6: Rails routes generated
|
54
|
+
success_count += 1 if test("Rails routes generated") do
|
55
|
+
routes = HelloWorldRailsApp.routes.routes
|
56
|
+
rapitapir_routes = routes.select { |r| r.defaults[:controller] == 'hello_world' }
|
57
|
+
rapitapir_routes.count == 4
|
58
|
+
end
|
59
|
+
|
60
|
+
# Test 7: Controller can be instantiated
|
61
|
+
success_count += 1 if test("Controller instantiation") do
|
62
|
+
controller = HelloWorldController.new
|
63
|
+
controller.respond_to?(:process_rapitapir_endpoint)
|
64
|
+
end
|
65
|
+
|
66
|
+
puts "\n" + "=" * 55
|
67
|
+
puts "š Test Results: #{success_count}/#{total_tests} tests passed"
|
68
|
+
|
69
|
+
if success_count == total_tests
|
70
|
+
puts "\nā
RAILS INTEGRATION FULLY WORKING!"
|
71
|
+
puts "\nš Key Features Implemented:"
|
72
|
+
puts " ⢠Enhanced controller base class"
|
73
|
+
puts " ⢠Sinatra-like syntax for Rails"
|
74
|
+
puts " ⢠Automatic action derivation"
|
75
|
+
puts " ⢠Route generation"
|
76
|
+
puts " ⢠Type shortcuts (T.string, etc.)"
|
77
|
+
puts " ⢠HTTP verb DSL"
|
78
|
+
puts "\nš Available Endpoints:"
|
79
|
+
HelloWorldController.rapitapir_endpoints.each do |action, config|
|
80
|
+
endpoint = config[:endpoint]
|
81
|
+
puts " #{endpoint.method.upcase} #{endpoint.path} => #{action}"
|
82
|
+
end
|
83
|
+
|
84
|
+
puts "\nšÆ Developer Experience Gap: ELIMINATED ā
"
|
85
|
+
puts "š Feature Parity with Sinatra: ACHIEVED ā
"
|
86
|
+
puts "\nš Rails developers now have the same elegant syntax!"
|
87
|
+
else
|
88
|
+
puts "\nā Some tests failed. Rails integration needs debugging."
|
89
|
+
end
|
90
|
+
|
91
|
+
puts "\n" + "=" * 55
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example Rails routes configuration for RapiTapir controllers
|
4
|
+
# Place this in your config/routes.rb file
|
5
|
+
|
6
|
+
Rails.application.routes.draw do
|
7
|
+
# Option 1: Auto-discover and register all RapiTapir controllers (Recommended)
|
8
|
+
# This will automatically find any controller that inherits from ControllerBase
|
9
|
+
# or includes the Rails::Controller module
|
10
|
+
rapitapir_auto_routes
|
11
|
+
|
12
|
+
# Option 2: Register specific controllers manually
|
13
|
+
# rapitapir_routes_for EnhancedUsersController
|
14
|
+
# rapitapir_routes_for BooksController
|
15
|
+
# rapitapir_routes_for OrdersController
|
16
|
+
|
17
|
+
# Option 3: Traditional Rails routes (still works if you prefer manual control)
|
18
|
+
# resources :enhanced_users, only: [:index, :show, :create, :update, :destroy] do
|
19
|
+
# collection do
|
20
|
+
# get :active
|
21
|
+
# post :bulk
|
22
|
+
# get :search
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
|
26
|
+
# Example of mixed approach - some auto, some manual
|
27
|
+
# rapitapir_routes_for EnhancedUsersController
|
28
|
+
#
|
29
|
+
# resources :admin_users, controller: 'admin/users' do
|
30
|
+
# # Manual routes for admin section
|
31
|
+
# end
|
32
|
+
|
33
|
+
# The rapitapir_auto_routes method will generate routes like:
|
34
|
+
#
|
35
|
+
# For EnhancedUsersController with api_resource '/users':
|
36
|
+
# GET /users enhanced_users#index
|
37
|
+
# GET /users/:id enhanced_users#show
|
38
|
+
# POST /users enhanced_users#create
|
39
|
+
# PUT /users/:id enhanced_users#update
|
40
|
+
# DELETE /users/:id enhanced_users#destroy
|
41
|
+
# GET /users/active enhanced_users#active
|
42
|
+
# POST /users/bulk enhanced_users#bulk
|
43
|
+
# GET /users/search enhanced_users#search
|
44
|
+
|
45
|
+
# For documentation and OpenAPI endpoints (coming soon):
|
46
|
+
# GET /docs rapitapir#docs
|
47
|
+
# GET /openapi.json rapitapir#openapi
|
48
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Debug Rails controller methods
|
5
|
+
|
6
|
+
puts "š Debugging Rails Controller Methods..."
|
7
|
+
puts "=" * 45
|
8
|
+
|
9
|
+
begin
|
10
|
+
# Load the controller
|
11
|
+
require_relative 'hello_world_app'
|
12
|
+
|
13
|
+
puts "\n1ļøā£ Checking controller class hierarchy..."
|
14
|
+
puts "HelloWorldController ancestors:"
|
15
|
+
HelloWorldController.ancestors[0..10].each_with_index do |ancestor, i|
|
16
|
+
puts " #{i}: #{ancestor}"
|
17
|
+
end
|
18
|
+
|
19
|
+
puts "\n2ļøā£ Checking if controller includes required modules..."
|
20
|
+
includes_controller = HelloWorldController.included_modules.include?(RapiTapir::Server::Rails::Controller)
|
21
|
+
puts "Includes RapiTapir::Server::Rails::Controller: #{includes_controller}"
|
22
|
+
|
23
|
+
includes_input_processor = HelloWorldController.included_modules.include?(RapiTapir::Server::Rails::InputProcessor)
|
24
|
+
puts "Includes InputProcessor: #{includes_input_processor}"
|
25
|
+
|
26
|
+
includes_response_handler = HelloWorldController.included_modules.include?(RapiTapir::Server::Rails::ResponseHandler)
|
27
|
+
puts "Includes ResponseHandler: #{includes_response_handler}"
|
28
|
+
|
29
|
+
puts "\n3ļøā£ Checking available instance methods..."
|
30
|
+
controller = HelloWorldController.new
|
31
|
+
has_process_method = controller.respond_to?(:process_rapitapir_endpoint, true)
|
32
|
+
puts "Has process_rapitapir_endpoint method: #{has_process_method}"
|
33
|
+
|
34
|
+
has_extract_inputs = controller.respond_to?(:extract_rails_inputs, true)
|
35
|
+
puts "Has extract_rails_inputs method: #{has_extract_inputs}"
|
36
|
+
|
37
|
+
has_render_response = controller.respond_to?(:render_rapitapir_response, true)
|
38
|
+
puts "Has render_rapitapir_response method: #{has_render_response}"
|
39
|
+
|
40
|
+
puts "\n4ļøā£ Checking registered endpoints..."
|
41
|
+
endpoints = HelloWorldController.rapitapir_endpoints
|
42
|
+
puts "Registered endpoints: #{endpoints.keys}"
|
43
|
+
|
44
|
+
endpoints.each do |action, config|
|
45
|
+
puts " #{action}: #{config[:endpoint].method} #{config[:endpoint].path}"
|
46
|
+
end
|
47
|
+
|
48
|
+
puts "\n5ļøā£ Checking Rails controller methods..."
|
49
|
+
has_render = controller.respond_to?(:render)
|
50
|
+
puts "Has render method: #{has_render}"
|
51
|
+
|
52
|
+
has_request = controller.respond_to?(:request)
|
53
|
+
puts "Has request method: #{has_request}"
|
54
|
+
|
55
|
+
has_params = controller.respond_to?(:params)
|
56
|
+
puts "Has params method: #{has_params}"
|
57
|
+
|
58
|
+
puts "\nā
Debug complete!"
|
59
|
+
|
60
|
+
rescue => e
|
61
|
+
puts "ā Error: #{e.message}"
|
62
|
+
puts e.backtrace[0..5].join("\n")
|
63
|
+
end
|