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,510 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Real-world Rails API example demonstrating RapiTapir integration
|
4
|
+
# This simulates a blog API with users, posts, and comments
|
5
|
+
|
6
|
+
require 'bundler/inline'
|
7
|
+
|
8
|
+
gemfile do
|
9
|
+
source 'https://rubygems.org'
|
10
|
+
gem 'rails', '~> 7.0'
|
11
|
+
gem 'sqlite3'
|
12
|
+
gem 'puma'
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'rails/all'
|
16
|
+
require_relative '../../lib/rapitapir'
|
17
|
+
|
18
|
+
# Simulate ActiveRecord models
|
19
|
+
class User < ActiveRecord::Base
|
20
|
+
has_many :posts, dependent: :destroy
|
21
|
+
has_many :comments, dependent: :destroy
|
22
|
+
|
23
|
+
validates :email, presence: true, uniqueness: true
|
24
|
+
validates :name, presence: true
|
25
|
+
end
|
26
|
+
|
27
|
+
class Post < ActiveRecord::Base
|
28
|
+
belongs_to :user
|
29
|
+
has_many :comments, dependent: :destroy
|
30
|
+
|
31
|
+
validates :title, presence: true
|
32
|
+
validates :content, presence: true
|
33
|
+
end
|
34
|
+
|
35
|
+
class Comment < ActiveRecord::Base
|
36
|
+
belongs_to :user
|
37
|
+
belongs_to :post
|
38
|
+
|
39
|
+
validates :content, presence: true
|
40
|
+
end
|
41
|
+
|
42
|
+
# Rails Application Setup
|
43
|
+
class BlogApiApp < Rails::Application
|
44
|
+
config.api_only = true
|
45
|
+
config.eager_load = false
|
46
|
+
config.logger = Logger.new(STDOUT)
|
47
|
+
config.log_level = :info
|
48
|
+
|
49
|
+
# Database setup
|
50
|
+
config.active_record.database_selector = { reading: :primary }
|
51
|
+
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
|
52
|
+
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
|
53
|
+
end
|
54
|
+
|
55
|
+
Rails.application.initialize!
|
56
|
+
|
57
|
+
# Database setup
|
58
|
+
ActiveRecord::Base.establish_connection(
|
59
|
+
adapter: 'sqlite3',
|
60
|
+
database: ':memory:'
|
61
|
+
)
|
62
|
+
|
63
|
+
# Create tables
|
64
|
+
ActiveRecord::Schema.define do
|
65
|
+
create_table :users do |t|
|
66
|
+
t.string :name, null: false
|
67
|
+
t.string :email, null: false
|
68
|
+
t.text :bio
|
69
|
+
t.timestamps
|
70
|
+
end
|
71
|
+
|
72
|
+
create_table :posts do |t|
|
73
|
+
t.references :user, null: false, foreign_key: true
|
74
|
+
t.string :title, null: false
|
75
|
+
t.text :content, null: false
|
76
|
+
t.boolean :published, default: false
|
77
|
+
t.timestamps
|
78
|
+
end
|
79
|
+
|
80
|
+
create_table :comments do |t|
|
81
|
+
t.references :user, null: false, foreign_key: true
|
82
|
+
t.references :post, null: false, foreign_key: true
|
83
|
+
t.text :content, null: false
|
84
|
+
t.timestamps
|
85
|
+
end
|
86
|
+
|
87
|
+
add_index :users, :email, unique: true
|
88
|
+
end
|
89
|
+
|
90
|
+
# Seed some data
|
91
|
+
user1 = User.create!(name: "Alice Johnson", email: "alice@example.com", bio: "Tech blogger")
|
92
|
+
user2 = User.create!(name: "Bob Smith", email: "bob@example.com", bio: "Developer")
|
93
|
+
|
94
|
+
post1 = Post.create!(
|
95
|
+
user: user1,
|
96
|
+
title: "Getting Started with RapiTapir",
|
97
|
+
content: "RapiTapir makes API development in Ruby a breeze...",
|
98
|
+
published: true
|
99
|
+
)
|
100
|
+
|
101
|
+
post2 = Post.create!(
|
102
|
+
user: user1,
|
103
|
+
title: "Advanced API Patterns",
|
104
|
+
content: "Let's explore some advanced patterns...",
|
105
|
+
published: false
|
106
|
+
)
|
107
|
+
|
108
|
+
Comment.create!(
|
109
|
+
user: user2,
|
110
|
+
post: post1,
|
111
|
+
content: "Great article! Very helpful."
|
112
|
+
)
|
113
|
+
|
114
|
+
# RapiTapir Controllers
|
115
|
+
class UsersController < RapiTapir::Server::Rails::ControllerBase
|
116
|
+
rapitapir do
|
117
|
+
development_defaults!
|
118
|
+
|
119
|
+
# User schema for responses
|
120
|
+
user_schema = T.hash(
|
121
|
+
id: T.integer,
|
122
|
+
name: T.string,
|
123
|
+
email: T.string,
|
124
|
+
bio: T.string.optional,
|
125
|
+
created_at: T.string,
|
126
|
+
updated_at: T.string
|
127
|
+
)
|
128
|
+
|
129
|
+
# User list with pagination
|
130
|
+
GET('/users')
|
131
|
+
.in(query(:page, T.integer.default(1)))
|
132
|
+
.in(query(:per_page, T.integer.default(10)))
|
133
|
+
.out(json_body(
|
134
|
+
users: T.array(user_schema),
|
135
|
+
pagination: T.hash(
|
136
|
+
page: T.integer,
|
137
|
+
per_page: T.integer,
|
138
|
+
total: T.integer,
|
139
|
+
total_pages: T.integer
|
140
|
+
)
|
141
|
+
))
|
142
|
+
.summary("List all users")
|
143
|
+
.description("Get a paginated list of all users")
|
144
|
+
|
145
|
+
# Get specific user
|
146
|
+
GET('/users/:id')
|
147
|
+
.in(path(:id, T.integer))
|
148
|
+
.out(json_body(user: user_schema))
|
149
|
+
.error_out(json_body(error: T.string), 404)
|
150
|
+
.summary("Get user by ID")
|
151
|
+
|
152
|
+
# Create new user
|
153
|
+
POST('/users')
|
154
|
+
.in(json_body(
|
155
|
+
name: T.string,
|
156
|
+
email: T.string,
|
157
|
+
bio: T.string.optional
|
158
|
+
))
|
159
|
+
.out(json_body(user: user_schema), 201)
|
160
|
+
.error_out(json_body(errors: T.array(T.string)), 422)
|
161
|
+
.summary("Create a new user")
|
162
|
+
|
163
|
+
# Update user
|
164
|
+
PUT('/users/:id')
|
165
|
+
.in(path(:id, T.integer))
|
166
|
+
.in(json_body(
|
167
|
+
name: T.string.optional,
|
168
|
+
email: T.string.optional,
|
169
|
+
bio: T.string.optional
|
170
|
+
))
|
171
|
+
.out(json_body(user: user_schema))
|
172
|
+
.error_out(json_body(error: T.string), 404)
|
173
|
+
.error_out(json_body(errors: T.array(T.string)), 422)
|
174
|
+
.summary("Update user")
|
175
|
+
|
176
|
+
# Delete user
|
177
|
+
DELETE('/users/:id')
|
178
|
+
.in(path(:id, T.integer))
|
179
|
+
.out(json_body(message: T.string))
|
180
|
+
.error_out(json_body(error: T.string), 404)
|
181
|
+
.summary("Delete user")
|
182
|
+
end
|
183
|
+
|
184
|
+
def list_users
|
185
|
+
page = inputs[:page]
|
186
|
+
per_page = [inputs[:per_page], 50].min # Cap at 50
|
187
|
+
|
188
|
+
users_scope = User.all
|
189
|
+
total = users_scope.count
|
190
|
+
users = users_scope.offset((page - 1) * per_page).limit(per_page)
|
191
|
+
|
192
|
+
{
|
193
|
+
users: users.map(&method(:serialize_user)),
|
194
|
+
pagination: {
|
195
|
+
page: page,
|
196
|
+
per_page: per_page,
|
197
|
+
total: total,
|
198
|
+
total_pages: (total.to_f / per_page).ceil
|
199
|
+
}
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
def get_user
|
204
|
+
user = User.find_by(id: inputs[:id])
|
205
|
+
return render_error("User not found", 404) unless user
|
206
|
+
|
207
|
+
{ user: serialize_user(user) }
|
208
|
+
end
|
209
|
+
|
210
|
+
def create_user
|
211
|
+
user = User.new(user_params)
|
212
|
+
|
213
|
+
if user.save
|
214
|
+
render json: { user: serialize_user(user) }, status: 201
|
215
|
+
else
|
216
|
+
render json: { errors: user.errors.full_messages }, status: 422
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def update_user
|
221
|
+
user = User.find_by(id: inputs[:id])
|
222
|
+
return render_error("User not found", 404) unless user
|
223
|
+
|
224
|
+
if user.update(user_params)
|
225
|
+
{ user: serialize_user(user) }
|
226
|
+
else
|
227
|
+
render json: { errors: user.errors.full_messages }, status: 422
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def delete_user
|
232
|
+
user = User.find_by(id: inputs[:id])
|
233
|
+
return render_error("User not found", 404) unless user
|
234
|
+
|
235
|
+
user.destroy
|
236
|
+
{ message: "User deleted successfully" }
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def user_params
|
242
|
+
inputs.slice(:name, :email, :bio).compact
|
243
|
+
end
|
244
|
+
|
245
|
+
def serialize_user(user)
|
246
|
+
{
|
247
|
+
id: user.id,
|
248
|
+
name: user.name,
|
249
|
+
email: user.email,
|
250
|
+
bio: user.bio,
|
251
|
+
created_at: user.created_at.iso8601,
|
252
|
+
updated_at: user.updated_at.iso8601
|
253
|
+
}
|
254
|
+
end
|
255
|
+
|
256
|
+
def render_error(message, status)
|
257
|
+
render json: { error: message }, status: status
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
class PostsController < RapiTapir::Server::Rails::ControllerBase
|
262
|
+
rapitapir do
|
263
|
+
development_defaults!
|
264
|
+
|
265
|
+
# Post schema
|
266
|
+
post_schema = T.hash(
|
267
|
+
id: T.integer,
|
268
|
+
title: T.string,
|
269
|
+
content: T.string,
|
270
|
+
published: T.boolean,
|
271
|
+
user_id: T.integer,
|
272
|
+
user: T.hash(
|
273
|
+
id: T.integer,
|
274
|
+
name: T.string,
|
275
|
+
email: T.string
|
276
|
+
),
|
277
|
+
created_at: T.string,
|
278
|
+
updated_at: T.string
|
279
|
+
)
|
280
|
+
|
281
|
+
# List posts with filtering
|
282
|
+
GET('/posts')
|
283
|
+
.in(query(:published, T.boolean.optional))
|
284
|
+
.in(query(:user_id, T.integer.optional))
|
285
|
+
.in(query(:page, T.integer.default(1)))
|
286
|
+
.out(json_body(
|
287
|
+
posts: T.array(post_schema),
|
288
|
+
pagination: T.hash(
|
289
|
+
page: T.integer,
|
290
|
+
total: T.integer,
|
291
|
+
total_pages: T.integer
|
292
|
+
)
|
293
|
+
))
|
294
|
+
.summary("List posts")
|
295
|
+
.description("Get posts with optional filtering by published status and user")
|
296
|
+
|
297
|
+
# Get specific post
|
298
|
+
GET('/posts/:id')
|
299
|
+
.in(path(:id, T.integer))
|
300
|
+
.out(json_body(post: post_schema))
|
301
|
+
.error_out(json_body(error: T.string), 404)
|
302
|
+
.summary("Get post by ID")
|
303
|
+
|
304
|
+
# Create post
|
305
|
+
POST('/posts')
|
306
|
+
.in(json_body(
|
307
|
+
title: T.string,
|
308
|
+
content: T.string,
|
309
|
+
published: T.boolean.default(false),
|
310
|
+
user_id: T.integer
|
311
|
+
))
|
312
|
+
.out(json_body(post: post_schema), 201)
|
313
|
+
.error_out(json_body(errors: T.array(T.string)), 422)
|
314
|
+
.summary("Create a new post")
|
315
|
+
|
316
|
+
# Update post
|
317
|
+
PUT('/posts/:id')
|
318
|
+
.in(path(:id, T.integer))
|
319
|
+
.in(json_body(
|
320
|
+
title: T.string.optional,
|
321
|
+
content: T.string.optional,
|
322
|
+
published: T.boolean.optional
|
323
|
+
))
|
324
|
+
.out(json_body(post: post_schema))
|
325
|
+
.error_out(json_body(errors: T.array(T.string)), 422)
|
326
|
+
.summary("Update post")
|
327
|
+
end
|
328
|
+
|
329
|
+
def list_posts
|
330
|
+
posts_scope = Post.includes(:user)
|
331
|
+
|
332
|
+
# Apply filters
|
333
|
+
posts_scope = posts_scope.where(published: inputs[:published]) if inputs[:published]
|
334
|
+
posts_scope = posts_scope.where(user_id: inputs[:user_id]) if inputs[:user_id]
|
335
|
+
|
336
|
+
# Pagination
|
337
|
+
page = inputs[:page]
|
338
|
+
per_page = 10
|
339
|
+
total = posts_scope.count
|
340
|
+
posts = posts_scope.offset((page - 1) * per_page).limit(per_page)
|
341
|
+
|
342
|
+
{
|
343
|
+
posts: posts.map(&method(:serialize_post)),
|
344
|
+
pagination: {
|
345
|
+
page: page,
|
346
|
+
total: total,
|
347
|
+
total_pages: (total.to_f / per_page).ceil
|
348
|
+
}
|
349
|
+
}
|
350
|
+
end
|
351
|
+
|
352
|
+
def get_post
|
353
|
+
post = Post.includes(:user).find_by(id: inputs[:id])
|
354
|
+
return render_error("Post not found", 404) unless post
|
355
|
+
|
356
|
+
{ post: serialize_post(post) }
|
357
|
+
end
|
358
|
+
|
359
|
+
def create_post
|
360
|
+
post = Post.new(post_params)
|
361
|
+
|
362
|
+
if post.save
|
363
|
+
render json: { post: serialize_post(post.reload) }, status: 201
|
364
|
+
else
|
365
|
+
render json: { errors: post.errors.full_messages }, status: 422
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def update_post
|
370
|
+
post = Post.find_by(id: inputs[:id])
|
371
|
+
return render_error("Post not found", 404) unless post
|
372
|
+
|
373
|
+
if post.update(post_params.compact)
|
374
|
+
{ post: serialize_post(post.reload) }
|
375
|
+
else
|
376
|
+
render json: { errors: post.errors.full_messages }, status: 422
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
private
|
381
|
+
|
382
|
+
def post_params
|
383
|
+
inputs.slice(:title, :content, :published, :user_id)
|
384
|
+
end
|
385
|
+
|
386
|
+
def serialize_post(post)
|
387
|
+
{
|
388
|
+
id: post.id,
|
389
|
+
title: post.title,
|
390
|
+
content: post.content,
|
391
|
+
published: post.published,
|
392
|
+
user_id: post.user_id,
|
393
|
+
user: {
|
394
|
+
id: post.user.id,
|
395
|
+
name: post.user.name,
|
396
|
+
email: post.user.email
|
397
|
+
},
|
398
|
+
created_at: post.created_at.iso8601,
|
399
|
+
updated_at: post.updated_at.iso8601
|
400
|
+
}
|
401
|
+
end
|
402
|
+
|
403
|
+
def render_error(message, status)
|
404
|
+
render json: { error: message }, status: status
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# Application routing
|
409
|
+
Rails.application.routes.draw do
|
410
|
+
# Mount RapiTapir controllers
|
411
|
+
rapitapir_routes_for UsersController
|
412
|
+
rapitapir_routes_for PostsController
|
413
|
+
|
414
|
+
# Health check
|
415
|
+
get '/health', to: proc { |env| [200, {}, ['OK']] }
|
416
|
+
|
417
|
+
# API documentation
|
418
|
+
get '/docs', to: proc { |env|
|
419
|
+
[200, { 'Content-Type' => 'text/html' }, [generate_docs_html]]
|
420
|
+
}
|
421
|
+
|
422
|
+
get '/openapi.json', to: proc { |env|
|
423
|
+
[200, { 'Content-Type' => 'application/json' }, [generate_openapi_spec]]
|
424
|
+
}
|
425
|
+
end
|
426
|
+
|
427
|
+
def generate_docs_html
|
428
|
+
<<~HTML
|
429
|
+
<!DOCTYPE html>
|
430
|
+
<html>
|
431
|
+
<head>
|
432
|
+
<title>Blog API Documentation</title>
|
433
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3.25.0/swagger-ui.css" />
|
434
|
+
</head>
|
435
|
+
<body>
|
436
|
+
<div id="swagger-ui"></div>
|
437
|
+
<script src="https://unpkg.com/swagger-ui-dist@3.25.0/swagger-ui-bundle.js"></script>
|
438
|
+
<script>
|
439
|
+
SwaggerUIBundle({
|
440
|
+
url: '/openapi.json',
|
441
|
+
dom_id: '#swagger-ui',
|
442
|
+
presets: [
|
443
|
+
SwaggerUIBundle.presets.apis,
|
444
|
+
SwaggerUIBundle.presets.standalone
|
445
|
+
]
|
446
|
+
});
|
447
|
+
</script>
|
448
|
+
</body>
|
449
|
+
</html>
|
450
|
+
HTML
|
451
|
+
end
|
452
|
+
|
453
|
+
def generate_openapi_spec
|
454
|
+
require 'json'
|
455
|
+
|
456
|
+
spec = {
|
457
|
+
openapi: "3.0.0",
|
458
|
+
info: {
|
459
|
+
title: "Blog API",
|
460
|
+
version: "1.0.0",
|
461
|
+
description: "A real-world blog API built with RapiTapir and Rails"
|
462
|
+
},
|
463
|
+
servers: [
|
464
|
+
{ url: "http://localhost:3000", description: "Development server" }
|
465
|
+
],
|
466
|
+
paths: {}
|
467
|
+
}
|
468
|
+
|
469
|
+
# Add endpoints from controllers
|
470
|
+
[UsersController, PostsController].each do |controller|
|
471
|
+
controller.endpoints.each do |endpoint|
|
472
|
+
path_key = endpoint.path.gsub(/:(\w+)/, '{\1}')
|
473
|
+
method = endpoint.method.downcase
|
474
|
+
|
475
|
+
spec[:paths][path_key] ||= {}
|
476
|
+
spec[:paths][path_key][method] = {
|
477
|
+
summary: endpoint.summary || "#{method.upcase} #{path_key}",
|
478
|
+
description: endpoint.description || "",
|
479
|
+
responses: {
|
480
|
+
"200" => { description: "Success" }
|
481
|
+
}
|
482
|
+
}
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
JSON.pretty_generate(spec)
|
487
|
+
end
|
488
|
+
|
489
|
+
# Start the server
|
490
|
+
if __FILE__ == $0
|
491
|
+
puts "🚀 Starting Blog API server on http://localhost:3000"
|
492
|
+
puts "📚 API Documentation: http://localhost:3000/docs"
|
493
|
+
puts "📋 OpenAPI Spec: http://localhost:3000/openapi.json"
|
494
|
+
puts ""
|
495
|
+
puts "Sample endpoints:"
|
496
|
+
puts " GET /users - List users"
|
497
|
+
puts " POST /users - Create user"
|
498
|
+
puts " GET /users/1 - Get user"
|
499
|
+
puts " PUT /users/1 - Update user"
|
500
|
+
puts " DELETE /users/1 - Delete user"
|
501
|
+
puts " GET /posts - List posts"
|
502
|
+
puts " GET /posts?published=true&user_id=1"
|
503
|
+
puts " POST /posts - Create post"
|
504
|
+
puts " GET /posts/1 - Get post"
|
505
|
+
puts " PUT /posts/1 - Update post"
|
506
|
+
puts ""
|
507
|
+
|
508
|
+
require 'rack'
|
509
|
+
Rack::Handler::WEBrick.run(Rails.application, Port: 3000)
|
510
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Test server startup without actually running it
|
5
|
+
|
6
|
+
puts "🧪 Testing Rails Server Startup..."
|
7
|
+
puts "=" * 40
|
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 Rack compatibility..."
|
15
|
+
require 'rack'
|
16
|
+
puts "✅ Rack loaded: #{Rack::VERSION}"
|
17
|
+
|
18
|
+
puts "\n3. Creating Rack::Server instance..."
|
19
|
+
server = Rack::Server.new(
|
20
|
+
app: HelloWorldRailsApp,
|
21
|
+
Port: 9292,
|
22
|
+
Host: 'localhost',
|
23
|
+
environment: 'development'
|
24
|
+
)
|
25
|
+
puts "✅ Rack::Server created: #{server.class}"
|
26
|
+
|
27
|
+
puts "\n4. Testing app call method..."
|
28
|
+
env = {
|
29
|
+
'REQUEST_METHOD' => 'GET',
|
30
|
+
'PATH_INFO' => '/hello',
|
31
|
+
'QUERY_STRING' => 'name=Test',
|
32
|
+
'HTTP_HOST' => 'localhost:9292',
|
33
|
+
'rack.url_scheme' => 'http'
|
34
|
+
}
|
35
|
+
|
36
|
+
response = HelloWorldRailsApp.call(env)
|
37
|
+
puts "✅ App responds to requests: status=#{response[0]}"
|
38
|
+
|
39
|
+
puts "\n🎉 Rails server setup appears to be working!"
|
40
|
+
puts "\n🚀 To start the server manually:"
|
41
|
+
puts " ruby examples/rails/hello_world_app.rb"
|
42
|
+
|
43
|
+
rescue => e
|
44
|
+
puts "❌ Error: #{e.message}"
|
45
|
+
puts e.backtrace[0..3].join("\n")
|
46
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Test endpoint processing directly
|
5
|
+
|
6
|
+
puts "🧪 Testing Direct Endpoint Processing..."
|
7
|
+
puts "=" * 42
|
8
|
+
|
9
|
+
begin
|
10
|
+
require_relative 'hello_world_app'
|
11
|
+
|
12
|
+
# Create a controller instance
|
13
|
+
controller = HelloWorldController.new
|
14
|
+
|
15
|
+
# Mock Rails request environment
|
16
|
+
env = {
|
17
|
+
'REQUEST_METHOD' => 'GET',
|
18
|
+
'PATH_INFO' => '/hello',
|
19
|
+
'QUERY_STRING' => 'name=Test',
|
20
|
+
'rack.input' => StringIO.new(''),
|
21
|
+
'rack.errors' => $stderr
|
22
|
+
}
|
23
|
+
|
24
|
+
# Create a mock request object
|
25
|
+
require 'stringio'
|
26
|
+
request = ActionDispatch::Request.new(env)
|
27
|
+
|
28
|
+
# Allow the controller to access request
|
29
|
+
controller.instance_variable_set(:@_request, request)
|
30
|
+
|
31
|
+
puts "\n1️⃣ Testing process_rapitapir_endpoint for :get_hello..."
|
32
|
+
|
33
|
+
# Call process_rapitapir_endpoint with the specific action
|
34
|
+
result = controller.send(:process_rapitapir_endpoint, :get_hello)
|
35
|
+
puts "✅ Success: #{result}"
|
36
|
+
|
37
|
+
rescue => e
|
38
|
+
puts "❌ Error during processing: #{e.message}"
|
39
|
+
puts "Backtrace:"
|
40
|
+
puts e.backtrace[0..10].join("\n")
|
41
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Test script for Hello World Rails RapiTapir example
|
5
|
+
|
6
|
+
require_relative 'hello_world_app'
|
7
|
+
|
8
|
+
puts "\n🧪 Testing Hello World Rails RapiTapir Controller"
|
9
|
+
puts "=" * 50
|
10
|
+
|
11
|
+
# Test 1: Basic functionality
|
12
|
+
puts "\n1. Testing controller class inheritance..."
|
13
|
+
begin
|
14
|
+
controller = HelloWorldController.new
|
15
|
+
puts "✅ HelloWorldController inherits from ControllerBase"
|
16
|
+
rescue => e
|
17
|
+
puts "❌ Error: #{e.message}"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Test 2: T shortcut availability
|
21
|
+
puts "\n2. Testing T shortcut availability..."
|
22
|
+
begin
|
23
|
+
string_type = HelloWorldController::T.string
|
24
|
+
puts "✅ T shortcut works: #{string_type.class}"
|
25
|
+
rescue => e
|
26
|
+
puts "❌ Error: #{e.message}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Test 3: HTTP verb methods
|
30
|
+
puts "\n3. Testing HTTP verb methods..."
|
31
|
+
begin
|
32
|
+
get_builder = HelloWorldController.GET('/test')
|
33
|
+
puts "✅ GET method works: #{get_builder.class}"
|
34
|
+
rescue => e
|
35
|
+
puts "❌ Error: #{e.message}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Test 4: Route generation
|
39
|
+
puts "\n4. Testing route generation..."
|
40
|
+
begin
|
41
|
+
router = Object.new
|
42
|
+
router.extend(RapiTapir::Server::Rails::Routes)
|
43
|
+
|
44
|
+
# Mock route methods
|
45
|
+
def router.get(path, options = {}); puts " Generated: GET #{path} => #{options[:to]}"; end
|
46
|
+
def router.post(path, options = {}); puts " Generated: POST #{path} => #{options[:to]}"; end
|
47
|
+
|
48
|
+
puts " Generating routes for HelloWorldController:"
|
49
|
+
|
50
|
+
# Check if controller has endpoints
|
51
|
+
if HelloWorldController.respond_to?(:rapitapir_endpoints)
|
52
|
+
router.rapitapir_routes_for(HelloWorldController)
|
53
|
+
puts "✅ Route generation works"
|
54
|
+
else
|
55
|
+
puts "❌ Controller doesn't have rapitapir_endpoints method"
|
56
|
+
end
|
57
|
+
rescue => e
|
58
|
+
puts "❌ Error: #{e.message}"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Test 5: Schema validation
|
62
|
+
puts "\n5. Testing schema definitions..."
|
63
|
+
begin
|
64
|
+
# Test the schema types used in the controller
|
65
|
+
schema = HelloWorldController::T.hash({
|
66
|
+
'message' => HelloWorldController::T.string,
|
67
|
+
'timestamp' => HelloWorldController::T.string
|
68
|
+
})
|
69
|
+
puts "✅ Schema definition works: #{schema.class}"
|
70
|
+
rescue => e
|
71
|
+
puts "❌ Error: #{e.message}"
|
72
|
+
end
|
73
|
+
|
74
|
+
puts "\n🎉 Rails Hello World Controller Tests Complete!"
|
75
|
+
puts "\n🚀 To run the server:"
|
76
|
+
puts " ruby examples/rails/hello_world_app.rb"
|
77
|
+
puts "\n📖 To test endpoints manually:"
|
78
|
+
puts " curl http://localhost:9292/hello?name=Developer"
|
79
|
+
puts " curl http://localhost:9292/greet/spanish"
|
80
|
+
puts " curl -X POST http://localhost:9292/greetings -H 'Content-Type: application/json' -d '{\"name\":\"Rails\"}'"
|