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,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Detailed Rails integration test
|
5
|
+
|
6
|
+
puts "🧪 Testing Rails Integration in Detail..."
|
7
|
+
puts "=" * 50
|
8
|
+
|
9
|
+
begin
|
10
|
+
puts "\n1. Loading Rails app..."
|
11
|
+
require_relative 'hello_world_app'
|
12
|
+
puts "✅ App loaded: #{HelloWorldRailsApp.class}"
|
13
|
+
|
14
|
+
puts "\n2. Checking controller endpoints..."
|
15
|
+
if HelloWorldController.respond_to?(:rapitapir_endpoints)
|
16
|
+
endpoints = HelloWorldController.rapitapir_endpoints
|
17
|
+
puts "✅ Found #{endpoints.count} endpoints:"
|
18
|
+
endpoints.each do |action, config|
|
19
|
+
endpoint = config[:endpoint]
|
20
|
+
puts " #{endpoint.method} #{endpoint.path} => #{action}"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
puts "❌ No rapitapir_endpoints found"
|
24
|
+
end
|
25
|
+
|
26
|
+
puts "\n3. Checking routes..."
|
27
|
+
routes = HelloWorldRailsApp.routes.routes
|
28
|
+
puts "✅ Found #{routes.count} routes:"
|
29
|
+
routes.each do |route|
|
30
|
+
puts " #{route.verb} #{route.path.spec} => #{route.defaults[:controller]}##{route.defaults[:action]}"
|
31
|
+
end
|
32
|
+
|
33
|
+
puts "\n4. Testing controller instantiation..."
|
34
|
+
controller = HelloWorldController.new
|
35
|
+
puts "✅ Controller instantiated: #{controller.class}"
|
36
|
+
|
37
|
+
puts "\n5. Testing Rails engine..."
|
38
|
+
app = HelloWorldRailsApp
|
39
|
+
puts "✅ Rails app ready: #{app}"
|
40
|
+
|
41
|
+
puts "\n🎉 Rails integration appears to be working!"
|
42
|
+
|
43
|
+
rescue => e
|
44
|
+
puts "❌ Error: #{e.message}"
|
45
|
+
puts e.backtrace[0..5].join("\n")
|
46
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Enhanced Rails controller example using RapiTapir's new base class
|
4
|
+
# This demonstrates the improved developer experience that matches Sinatra's elegance
|
5
|
+
#
|
6
|
+
# Usage in a Rails app:
|
7
|
+
# 1. Add this file to app/controllers/
|
8
|
+
# 2. Add routes using the automatic route generator
|
9
|
+
# 3. That's it! No boilerplate needed.
|
10
|
+
|
11
|
+
require_relative '../../lib/rapitapir'
|
12
|
+
require_relative '../../lib/rapitapir/server/rails/controller_base'
|
13
|
+
|
14
|
+
class EnhancedUsersController < RapiTapir::Server::Rails::ControllerBase
|
15
|
+
# Configure RapiTapir - same clean syntax as Sinatra
|
16
|
+
rapitapir do
|
17
|
+
info(
|
18
|
+
title: 'Enhanced Users API',
|
19
|
+
description: 'A clean, type-safe user management API built with RapiTapir for Rails',
|
20
|
+
version: '1.0.0'
|
21
|
+
)
|
22
|
+
# Future: development_defaults! will auto-setup CORS, docs, health checks
|
23
|
+
end
|
24
|
+
|
25
|
+
# Define schema using T shortcut (automatically available!)
|
26
|
+
USER_SCHEMA = T.hash({
|
27
|
+
"id" => T.integer,
|
28
|
+
"name" => T.string(min_length: 1, max_length: 100),
|
29
|
+
"email" => T.email,
|
30
|
+
"active" => T.boolean,
|
31
|
+
"created_at" => T.datetime,
|
32
|
+
"updated_at" => T.datetime
|
33
|
+
})
|
34
|
+
|
35
|
+
USER_CREATE_SCHEMA = T.hash({
|
36
|
+
"name" => T.string(min_length: 1, max_length: 100),
|
37
|
+
"email" => T.email,
|
38
|
+
"active" => T.optional(T.boolean)
|
39
|
+
})
|
40
|
+
|
41
|
+
USER_UPDATE_SCHEMA = T.hash({
|
42
|
+
"name" => T.optional(T.string(min_length: 1, max_length: 100)),
|
43
|
+
"email" => T.optional(T.email),
|
44
|
+
"active" => T.optional(T.boolean)
|
45
|
+
})
|
46
|
+
|
47
|
+
before_action :setup_users_data
|
48
|
+
|
49
|
+
# Option 1: Use the enhanced api_resource DSL (recommended for CRUD)
|
50
|
+
api_resource '/users', schema: USER_SCHEMA do
|
51
|
+
crud do
|
52
|
+
index do
|
53
|
+
# Access Rails helpers and instance variables naturally
|
54
|
+
users = @users.values
|
55
|
+
|
56
|
+
# Apply optional filtering (Rails-style)
|
57
|
+
users = users.select { |u| u[:active] } if params[:active] == 'true'
|
58
|
+
|
59
|
+
# Simple pagination
|
60
|
+
limit = params[:limit]&.to_i || 20
|
61
|
+
offset = params[:offset]&.to_i || 0
|
62
|
+
|
63
|
+
users[offset, limit] || []
|
64
|
+
end
|
65
|
+
|
66
|
+
show do |inputs|
|
67
|
+
user = @users[inputs[:id]]
|
68
|
+
|
69
|
+
# Rails-style error handling
|
70
|
+
if user.nil?
|
71
|
+
render json: { error: 'User not found' }, status: :not_found
|
72
|
+
return
|
73
|
+
end
|
74
|
+
|
75
|
+
user
|
76
|
+
end
|
77
|
+
|
78
|
+
create do |inputs|
|
79
|
+
new_id = (@users.keys.max || 0) + 1
|
80
|
+
|
81
|
+
new_user = {
|
82
|
+
id: new_id,
|
83
|
+
name: inputs[:body]['name'],
|
84
|
+
email: inputs[:body]['email'],
|
85
|
+
active: inputs[:body]['active'] || true,
|
86
|
+
created_at: Time.now.iso8601,
|
87
|
+
updated_at: Time.now.iso8601
|
88
|
+
}
|
89
|
+
|
90
|
+
@users[new_id] = new_user
|
91
|
+
|
92
|
+
# Rails-style status setting
|
93
|
+
response.status = 201
|
94
|
+
new_user
|
95
|
+
end
|
96
|
+
|
97
|
+
update do |inputs|
|
98
|
+
user = @users[inputs[:id]]
|
99
|
+
|
100
|
+
if user.nil?
|
101
|
+
render json: { error: 'User not found' }, status: :not_found
|
102
|
+
return
|
103
|
+
end
|
104
|
+
|
105
|
+
# Update fields
|
106
|
+
update_data = inputs[:body]
|
107
|
+
user[:name] = update_data['name'] if update_data['name']
|
108
|
+
user[:email] = update_data['email'] if update_data['email']
|
109
|
+
user[:active] = update_data['active'] if update_data.key?('active')
|
110
|
+
user[:updated_at] = Time.now.iso8601
|
111
|
+
|
112
|
+
@users[inputs[:id]] = user
|
113
|
+
user
|
114
|
+
end
|
115
|
+
|
116
|
+
destroy do |inputs|
|
117
|
+
user = @users[inputs[:id]]
|
118
|
+
|
119
|
+
if user.nil?
|
120
|
+
render json: { error: 'User not found' }, status: :not_found
|
121
|
+
return
|
122
|
+
end
|
123
|
+
|
124
|
+
@users.delete(inputs[:id])
|
125
|
+
|
126
|
+
# Rails-style head response
|
127
|
+
head :no_content
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Option 2: Individual endpoint definitions with enhanced HTTP verb DSL
|
133
|
+
endpoint(
|
134
|
+
GET('/users/active')
|
135
|
+
.summary('Get active users')
|
136
|
+
.description('Retrieve a list of all active users in the system')
|
137
|
+
.tags('Users')
|
138
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Maximum number of results')
|
139
|
+
.ok(T.array(USER_SCHEMA))
|
140
|
+
.build
|
141
|
+
) do |inputs|
|
142
|
+
active_users = @users.values.select { |u| u[:active] }
|
143
|
+
limit = inputs[:limit] || 20
|
144
|
+
active_users.first(limit)
|
145
|
+
end
|
146
|
+
|
147
|
+
endpoint(
|
148
|
+
POST('/users/bulk')
|
149
|
+
.summary('Create multiple users')
|
150
|
+
.description('Create multiple users in a single request')
|
151
|
+
.tags('Users')
|
152
|
+
.json_body(T.array(USER_CREATE_SCHEMA))
|
153
|
+
.created(T.array(USER_SCHEMA))
|
154
|
+
.bad_request(T.hash({
|
155
|
+
"error" => T.string,
|
156
|
+
"failed_users" => T.array(T.hash({
|
157
|
+
"index" => T.integer,
|
158
|
+
"errors" => T.array(T.string)
|
159
|
+
}))
|
160
|
+
}))
|
161
|
+
.build
|
162
|
+
) do |inputs|
|
163
|
+
created_users = []
|
164
|
+
failed_users = []
|
165
|
+
|
166
|
+
inputs[:body].each_with_index do |user_data, index|
|
167
|
+
# Simple validation
|
168
|
+
errors = []
|
169
|
+
errors << 'Name is required' if user_data['name'].nil? || user_data['name'].empty?
|
170
|
+
errors << 'Email is required' if user_data['email'].nil? || user_data['email'].empty?
|
171
|
+
errors << 'Email already exists' if @users.values.any? { |u| u[:email] == user_data['email'] }
|
172
|
+
|
173
|
+
if errors.any?
|
174
|
+
failed_users << { index: index, errors: errors }
|
175
|
+
next
|
176
|
+
end
|
177
|
+
|
178
|
+
new_id = (@users.keys.max || 0) + 1
|
179
|
+
new_user = {
|
180
|
+
id: new_id,
|
181
|
+
name: user_data['name'],
|
182
|
+
email: user_data['email'],
|
183
|
+
active: user_data['active'] || true,
|
184
|
+
created_at: Time.now.iso8601,
|
185
|
+
updated_at: Time.now.iso8601
|
186
|
+
}
|
187
|
+
|
188
|
+
@users[new_id] = new_user
|
189
|
+
created_users << new_user
|
190
|
+
end
|
191
|
+
|
192
|
+
if failed_users.any?
|
193
|
+
render json: {
|
194
|
+
error: 'Some users could not be created',
|
195
|
+
failed_users: failed_users
|
196
|
+
}, status: :bad_request
|
197
|
+
return
|
198
|
+
end
|
199
|
+
|
200
|
+
response.status = 201
|
201
|
+
created_users
|
202
|
+
end
|
203
|
+
|
204
|
+
endpoint(
|
205
|
+
GET('/users/search')
|
206
|
+
.summary('Search users')
|
207
|
+
.description('Search users by name or email')
|
208
|
+
.tags('Users', 'Search')
|
209
|
+
.query(:q, T.string(min_length: 1), description: 'Search query')
|
210
|
+
.query(:fields, T.optional(T.array(T.string(enum: %w[name email]))), description: 'Fields to search in')
|
211
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Maximum number of results')
|
212
|
+
.ok(T.array(USER_SCHEMA))
|
213
|
+
.bad_request(T.hash({ "error" => T.string }))
|
214
|
+
.build
|
215
|
+
) do |inputs|
|
216
|
+
query = inputs[:q].downcase
|
217
|
+
fields = inputs[:fields] || %w[name email]
|
218
|
+
limit = inputs[:limit] || 20
|
219
|
+
|
220
|
+
results = @users.values.select do |user|
|
221
|
+
fields.any? do |field|
|
222
|
+
user[field.to_sym]&.downcase&.include?(query)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
results.first(limit)
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def setup_users_data
|
232
|
+
@users = {
|
233
|
+
1 => {
|
234
|
+
id: 1,
|
235
|
+
name: 'John Doe',
|
236
|
+
email: 'john@example.com',
|
237
|
+
active: true,
|
238
|
+
created_at: 1.week.ago.iso8601,
|
239
|
+
updated_at: 1.week.ago.iso8601
|
240
|
+
},
|
241
|
+
2 => {
|
242
|
+
id: 2,
|
243
|
+
name: 'Jane Smith',
|
244
|
+
email: 'jane@example.com',
|
245
|
+
active: true,
|
246
|
+
created_at: 3.days.ago.iso8601,
|
247
|
+
updated_at: 3.days.ago.iso8601
|
248
|
+
},
|
249
|
+
3 => {
|
250
|
+
id: 3,
|
251
|
+
name: 'Bob Wilson',
|
252
|
+
email: 'bob@example.com',
|
253
|
+
active: false,
|
254
|
+
created_at: 1.day.ago.iso8601,
|
255
|
+
updated_at: 1.day.ago.iso8601
|
256
|
+
}
|
257
|
+
}
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Example routes configuration for config/routes.rb:
|
262
|
+
#
|
263
|
+
# Rails.application.routes.draw do
|
264
|
+
# # Option 1: Automatic route generation for specific controller
|
265
|
+
# rapitapir_routes_for EnhancedUsersController
|
266
|
+
#
|
267
|
+
# # Option 2: Auto-discover all RapiTapir controllers
|
268
|
+
# rapitapir_auto_routes
|
269
|
+
#
|
270
|
+
# # Option 3: Manual routes (still works)
|
271
|
+
# resources :enhanced_users, only: [:index, :show, :create, :update, :destroy] do
|
272
|
+
# collection do
|
273
|
+
# get :active
|
274
|
+
# post :bulk
|
275
|
+
# get :search
|
276
|
+
# end
|
277
|
+
# end
|
278
|
+
# end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Test the Rails server startup
|
5
|
+
|
6
|
+
puts "🧪 Testing Rails Server Startup (Final)..."
|
7
|
+
puts "=" * 45
|
8
|
+
|
9
|
+
begin
|
10
|
+
puts "\n1. Loading Rails app..."
|
11
|
+
require_relative 'hello_world_app'
|
12
|
+
puts "✅ App loaded successfully"
|
13
|
+
|
14
|
+
puts "\n2. Testing direct app call..."
|
15
|
+
env = {
|
16
|
+
'REQUEST_METHOD' => 'GET',
|
17
|
+
'PATH_INFO' => '/hello',
|
18
|
+
'QUERY_STRING' => 'name=Test',
|
19
|
+
'HTTP_HOST' => 'localhost:9292',
|
20
|
+
'rack.url_scheme' => 'http',
|
21
|
+
'rack.input' => StringIO.new(''),
|
22
|
+
'rack.errors' => $stderr,
|
23
|
+
'rack.version' => [1, 3],
|
24
|
+
'rack.multithread' => true,
|
25
|
+
'rack.multiprocess' => false,
|
26
|
+
'rack.run_once' => false
|
27
|
+
}
|
28
|
+
|
29
|
+
require 'stringio'
|
30
|
+
env['rack.input'] = StringIO.new('')
|
31
|
+
|
32
|
+
status, headers, body = HelloWorldRailsApp.call(env)
|
33
|
+
puts "✅ App responds: status=#{status}"
|
34
|
+
|
35
|
+
puts "\n3. Testing WEBrick server creation..."
|
36
|
+
require 'webrick'
|
37
|
+
server = WEBrick::HTTPServer.new(
|
38
|
+
Port: 9293, # Different port for testing
|
39
|
+
Host: 'localhost'
|
40
|
+
)
|
41
|
+
puts "✅ WEBrick server created"
|
42
|
+
|
43
|
+
puts "\n🎉 Rails server setup is ready!"
|
44
|
+
puts "\n🚀 The server should start successfully."
|
45
|
+
puts "📝 Run: ruby examples/rails/hello_world_app.rb"
|
46
|
+
|
47
|
+
rescue => e
|
48
|
+
puts "❌ Error: #{e.message}"
|
49
|
+
puts e.backtrace[0..3].join("\n")
|
50
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Minimal Rails Application for Hello World RapiTapir Demo
|
4
|
+
# This creates a minimal Rails app to demonstrate the Hello World controller
|
5
|
+
|
6
|
+
require 'rails'
|
7
|
+
require 'action_controller/railtie'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
# Minimal Rails application
|
11
|
+
class HelloWorldRailsApp < Rails::Application
|
12
|
+
config.load_defaults Rails::VERSION::STRING.to_f
|
13
|
+
config.api_only = true
|
14
|
+
config.eager_load = false
|
15
|
+
config.cache_classes = false
|
16
|
+
config.secret_key_base = 'dummy_secret_for_demo'
|
17
|
+
|
18
|
+
# Disable unnecessary middleware for demo
|
19
|
+
config.middleware.delete ActionDispatch::Cookies
|
20
|
+
config.middleware.delete ActionDispatch::Session::CookieStore
|
21
|
+
config.middleware.delete ActionDispatch::Flash
|
22
|
+
|
23
|
+
# CORS for development (optional)
|
24
|
+
config.middleware.insert_before 0, Rack::Cors do
|
25
|
+
allow do
|
26
|
+
origins '*'
|
27
|
+
resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head]
|
28
|
+
end
|
29
|
+
end if defined?(Rack::Cors)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Initialize the Rails application
|
33
|
+
HelloWorldRailsApp.initialize!
|
34
|
+
|
35
|
+
# Load our controller
|
36
|
+
require_relative 'hello_world_controller'
|
37
|
+
|
38
|
+
# Load routes helper and include it in Rails routes
|
39
|
+
require_relative '../../lib/rapitapir/server/rails/routes'
|
40
|
+
require_relative '../../lib/rapitapir/server/rails/documentation_helpers'
|
41
|
+
ActionDispatch::Routing::Mapper.include(RapiTapir::Server::Rails::Routes)
|
42
|
+
ActionDispatch::Routing::Mapper.include(RapiTapir::Server::Rails::DocumentationHelpers)
|
43
|
+
|
44
|
+
# Define routes
|
45
|
+
HelloWorldRailsApp.routes.draw do
|
46
|
+
# Auto-generate routes from RapiTapir endpoints
|
47
|
+
rapitapir_routes_for HelloWorldController
|
48
|
+
|
49
|
+
# Documentation endpoints
|
50
|
+
add_documentation_routes(
|
51
|
+
HelloWorldController,
|
52
|
+
{
|
53
|
+
docs_path: '/docs',
|
54
|
+
openapi_path: '/openapi.json',
|
55
|
+
info: {
|
56
|
+
title: 'Hello World Rails API',
|
57
|
+
version: '1.0.0',
|
58
|
+
description: 'A beautiful Rails API built with RapiTapir - same elegant syntax as Sinatra!'
|
59
|
+
},
|
60
|
+
servers: [
|
61
|
+
{
|
62
|
+
url: 'http://localhost:9292',
|
63
|
+
description: 'Development server (Rails + RapiTapir)'
|
64
|
+
}
|
65
|
+
]
|
66
|
+
}
|
67
|
+
)
|
68
|
+
|
69
|
+
# Welcome page
|
70
|
+
root 'hello_world#welcome'
|
71
|
+
|
72
|
+
# Manual health check (for demo purposes)
|
73
|
+
get '/manual-health', to: proc { |env|
|
74
|
+
[200, {'Content-Type' => 'application/json'},
|
75
|
+
[{ status: 'healthy', message: 'Manual route working!' }.to_json]]
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
# If running this file directly
|
80
|
+
if __FILE__ == $PROGRAM_NAME
|
81
|
+
require 'rack'
|
82
|
+
|
83
|
+
puts "\n🌟 Hello World Rails API with RapiTapir"
|
84
|
+
puts "🚀 Clean syntax: class HelloWorldController < RapiTapir::Server::Rails::ControllerBase"
|
85
|
+
puts "🔗 Enhanced Rails integration with Sinatra-like elegance"
|
86
|
+
puts ""
|
87
|
+
puts "📋 Available endpoints:"
|
88
|
+
puts " GET / - Welcome message"
|
89
|
+
puts " GET /hello?name=YourName - Personalized greeting"
|
90
|
+
puts " GET /greet/:language - Multilingual greetings"
|
91
|
+
puts " POST /greetings - Create custom greeting"
|
92
|
+
puts " GET /health - Health check"
|
93
|
+
puts " GET /docs - 📖 Interactive API Documentation (Swagger UI)"
|
94
|
+
puts " GET /openapi.json - 🔧 OpenAPI 3.0 Specification"
|
95
|
+
puts ""
|
96
|
+
puts "🌐 Starting server on http://localhost:9292"
|
97
|
+
puts "❤️ Try: curl http://localhost:9292/hello?name=Developer"
|
98
|
+
puts "🌍 Try: curl http://localhost:9292/greet/spanish"
|
99
|
+
puts ""
|
100
|
+
puts "✨ Beautiful, type-safe Rails API with RapiTapir integration!"
|
101
|
+
puts ""
|
102
|
+
puts "🌐 Starting server on http://localhost:9292"
|
103
|
+
puts "📖 Try these endpoints:"
|
104
|
+
puts " curl 'http://localhost:9292/hello?name=Developer'"
|
105
|
+
puts " curl 'http://localhost:9292/greet/spanish'"
|
106
|
+
puts " curl -X POST 'http://localhost:9292/greetings' -H 'Content-Type: application/json' -d '{\"name\":\"Rails\"}'"
|
107
|
+
puts " open http://localhost:9292/docs # 📖 Interactive API Documentation!"
|
108
|
+
puts ""
|
109
|
+
|
110
|
+
# Start server using Rackup (modern Rack)
|
111
|
+
require 'rackup/handler/webrick'
|
112
|
+
|
113
|
+
puts "🚀 Starting server with Rackup..."
|
114
|
+
|
115
|
+
Rackup::Handler::WEBrick.run(HelloWorldRailsApp, Port: 9292, Host: 'localhost')
|
116
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RapiTapir Rails Integration - Hello World Example
|
4
|
+
#
|
5
|
+
# The most minimal example showing how to create a beautiful, type-safe API
|
6
|
+
# with automatic OpenAPI documentation using Rails and the enhanced RapiTapir integration!
|
7
|
+
|
8
|
+
require_relative '../../lib/rapitapir'
|
9
|
+
require_relative '../../lib/rapitapir/server/rails/controller_base'
|
10
|
+
|
11
|
+
# Your entire Rails API controller in under 20 lines! 🚀
|
12
|
+
class HelloWorldController < RapiTapir::Server::Rails::ControllerBase
|
13
|
+
|
14
|
+
# One-line API configuration - same as Sinatra!
|
15
|
+
rapitapir do
|
16
|
+
info(title: 'Hello World Rails API', version: '1.0.0')
|
17
|
+
development_defaults! # Enable docs, health checks, CORS, etc.
|
18
|
+
end
|
19
|
+
|
20
|
+
# Hello World endpoint - beautifully typed and documented using enhanced DSL
|
21
|
+
endpoint(
|
22
|
+
GET('/hello')
|
23
|
+
.query(:name, T.optional(T.string), description: 'Name to greet')
|
24
|
+
.summary('Say hello to someone')
|
25
|
+
.description('Returns a personalized greeting with Rails magic')
|
26
|
+
.tags('Greetings')
|
27
|
+
.ok(T.hash({
|
28
|
+
'message' => T.string,
|
29
|
+
'timestamp' => T.string,
|
30
|
+
'framework' => T.string
|
31
|
+
}))
|
32
|
+
.build
|
33
|
+
) do |inputs|
|
34
|
+
name = inputs[:name] || 'Rails Developer'
|
35
|
+
|
36
|
+
# Return data - let RapiTapir handle the rendering
|
37
|
+
{
|
38
|
+
message: "Hello, #{name}! Welcome to RapiTapir with Rails!",
|
39
|
+
timestamp: Time.now.iso8601,
|
40
|
+
framework: 'Rails + RapiTapir'
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Another endpoint showing path parameters with enhanced DSL
|
45
|
+
endpoint(
|
46
|
+
GET('/greet/:language')
|
47
|
+
.path_param(:language, T.string, description: 'Language for greeting')
|
48
|
+
.summary('Multilingual greeting')
|
49
|
+
.description('Get a greeting in different languages using Rails')
|
50
|
+
.tags('Greetings', 'i18n')
|
51
|
+
.ok(T.hash({
|
52
|
+
'greeting' => T.string,
|
53
|
+
'language' => T.string,
|
54
|
+
'powered_by' => T.string
|
55
|
+
}))
|
56
|
+
.not_found(T.hash({ 'error' => T.string }))
|
57
|
+
.build
|
58
|
+
) do |inputs|
|
59
|
+
greetings = {
|
60
|
+
'english' => 'Hello!',
|
61
|
+
'spanish' => '¡Hola!',
|
62
|
+
'french' => 'Bonjour!',
|
63
|
+
'italian' => 'Ciao!',
|
64
|
+
'german' => 'Hallo!',
|
65
|
+
'japanese' => 'こんにちは!',
|
66
|
+
'portuguese' => 'Olá!',
|
67
|
+
'russian' => 'Привет!'
|
68
|
+
}
|
69
|
+
|
70
|
+
language = inputs[:language].downcase
|
71
|
+
greeting = greetings[language]
|
72
|
+
|
73
|
+
if greeting.nil?
|
74
|
+
# Return error response structure
|
75
|
+
{ error: "Language '#{inputs[:language]}' not supported", _status: 404 }
|
76
|
+
else
|
77
|
+
# Return success response
|
78
|
+
{
|
79
|
+
greeting: greeting,
|
80
|
+
language: language.capitalize,
|
81
|
+
powered_by: 'RapiTapir + Rails'
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Demonstrate Rails-style POST endpoint with JSON body
|
87
|
+
endpoint(
|
88
|
+
POST('/greetings')
|
89
|
+
.summary('Create a custom greeting')
|
90
|
+
.description('Create a personalized greeting message')
|
91
|
+
.tags('Greetings')
|
92
|
+
.json_body(T.hash({
|
93
|
+
'name' => T.string(min_length: 1),
|
94
|
+
'greeting_style' => T.optional(T.string(enum: %w[formal casual friendly professional]))
|
95
|
+
}))
|
96
|
+
.created(T.hash({
|
97
|
+
'id' => T.integer,
|
98
|
+
'message' => T.string,
|
99
|
+
'style' => T.string,
|
100
|
+
'created_at' => T.string
|
101
|
+
}))
|
102
|
+
.bad_request(T.hash({
|
103
|
+
'error' => T.string,
|
104
|
+
'details' => T.array(T.string)
|
105
|
+
}))
|
106
|
+
.build
|
107
|
+
) do |inputs|
|
108
|
+
body = inputs[:body]
|
109
|
+
name = body['name']
|
110
|
+
style = body['greeting_style'] || 'friendly'
|
111
|
+
|
112
|
+
# Validation
|
113
|
+
errors = []
|
114
|
+
errors << 'Name cannot be empty' if name.nil? || name.strip.empty?
|
115
|
+
|
116
|
+
if errors.any?
|
117
|
+
# Return validation error
|
118
|
+
{ error: 'Validation failed', details: errors, _status: 400 }
|
119
|
+
else
|
120
|
+
# Style-based greetings
|
121
|
+
greetings = {
|
122
|
+
'formal' => "Good day, #{name}. It is a pleasure to make your acquaintance.",
|
123
|
+
'casual' => "Hey #{name}! What's up?",
|
124
|
+
'friendly' => "Hello there, #{name}! Hope you're having a great day!",
|
125
|
+
'professional' => "Hello #{name}, welcome to our platform."
|
126
|
+
}
|
127
|
+
|
128
|
+
# Simulate database save with incremental ID
|
129
|
+
greeting_id = rand(1000..9999)
|
130
|
+
|
131
|
+
# Return created response
|
132
|
+
{
|
133
|
+
id: greeting_id,
|
134
|
+
message: greetings[style],
|
135
|
+
style: style,
|
136
|
+
created_at: Time.now.iso8601,
|
137
|
+
_status: 201
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Health check endpoint (will be auto-generated in future versions)
|
143
|
+
endpoint(
|
144
|
+
GET('/health')
|
145
|
+
.summary('Health check')
|
146
|
+
.description('Check if the Rails API is running')
|
147
|
+
.tags('System')
|
148
|
+
.ok(T.hash({
|
149
|
+
'status' => T.string,
|
150
|
+
'timestamp' => T.string,
|
151
|
+
'framework' => T.string,
|
152
|
+
'ruby_version' => T.string
|
153
|
+
}))
|
154
|
+
.build
|
155
|
+
) do |inputs|
|
156
|
+
# Return health status data
|
157
|
+
{
|
158
|
+
status: 'healthy',
|
159
|
+
timestamp: Time.now.iso8601,
|
160
|
+
framework: "Rails #{Rails.version rescue 'Unknown'} + RapiTapir",
|
161
|
+
ruby_version: RUBY_VERSION
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
# Custom action demonstrating Rails conventions
|
166
|
+
def welcome
|
167
|
+
render json: {
|
168
|
+
message: 'Welcome to RapiTapir with Rails!',
|
169
|
+
documentation: 'Visit /docs for interactive API documentation',
|
170
|
+
examples: {
|
171
|
+
hello: '/hello?name=YourName',
|
172
|
+
greet: '/greet/spanish',
|
173
|
+
health: '/health'
|
174
|
+
}
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
# Rails-style before_action can still be used
|
181
|
+
# before_action :log_request, only: [:hello, :greet]
|
182
|
+
|
183
|
+
def log_request
|
184
|
+
Rails.logger.info "RapiTapir request: #{request.method} #{request.path}"
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example routes configuration for Hello World Rails API
|
4
|
+
# Place this in your config/routes.rb file
|
5
|
+
|
6
|
+
Rails.application.routes.draw do
|
7
|
+
# Option 1: Auto-generate routes from RapiTapir endpoints (Recommended)
|
8
|
+
rapitapir_routes_for HelloWorldController
|
9
|
+
|
10
|
+
# This will automatically generate:
|
11
|
+
# GET /hello hello_world#hello
|
12
|
+
# GET /greet/:language hello_world#greet
|
13
|
+
# POST /greetings hello_world#greetings
|
14
|
+
# GET /health hello_world#health
|
15
|
+
|
16
|
+
# Option 2: Manual routes (if you prefer explicit control)
|
17
|
+
# get '/hello', to: 'hello_world#hello'
|
18
|
+
# get '/greet/:language', to: 'hello_world#greet'
|
19
|
+
# post '/greetings', to: 'hello_world#create'
|
20
|
+
# get '/health', to: 'hello_world#health'
|
21
|
+
|
22
|
+
# Welcome page (custom action not defined by RapiTapir)
|
23
|
+
root 'hello_world#welcome'
|
24
|
+
|
25
|
+
# Future: Auto-generated documentation endpoints
|
26
|
+
# get '/docs', to: 'rapitapir_docs#index'
|
27
|
+
# get '/openapi.json', to: 'rapitapir_docs#openapi'
|
28
|
+
end
|