rapitapir 0.1.2 → 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
- metadata +74 -2
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Api::V1::UsersController < ApplicationController
|
4
|
+
rapitapir do
|
5
|
+
# User type definitions
|
6
|
+
user_type = T.hash(
|
7
|
+
id: T.integer,
|
8
|
+
email: T.string,
|
9
|
+
name: T.string,
|
10
|
+
bio: T.string.optional,
|
11
|
+
avatar_url: T.string.optional,
|
12
|
+
created_at: T.string,
|
13
|
+
updated_at: T.string,
|
14
|
+
posts_count: T.integer
|
15
|
+
)
|
16
|
+
|
17
|
+
create_user_type = T.hash(
|
18
|
+
email: T.string,
|
19
|
+
name: T.string,
|
20
|
+
bio: T.string.optional,
|
21
|
+
password: T.string
|
22
|
+
)
|
23
|
+
|
24
|
+
update_user_type = T.hash(
|
25
|
+
email: T.string.optional,
|
26
|
+
name: T.string.optional,
|
27
|
+
bio: T.string.optional
|
28
|
+
)
|
29
|
+
|
30
|
+
# List users with search and pagination
|
31
|
+
GET('/api/v1/users')
|
32
|
+
.in(query(:search, T.string.optional))
|
33
|
+
.in(query(:page, T.integer.default(1)))
|
34
|
+
.in(query(:per_page, T.integer.default(20)))
|
35
|
+
.in(query(:sort, T.enum(['name', 'email', 'created_at']).default('created_at')))
|
36
|
+
.in(query(:order, T.enum(['asc', 'desc']).default('desc')))
|
37
|
+
.out(json_body(
|
38
|
+
users: T.array(user_type),
|
39
|
+
pagination: T.hash(
|
40
|
+
page: T.integer,
|
41
|
+
per_page: T.integer,
|
42
|
+
total: T.integer,
|
43
|
+
total_pages: T.integer,
|
44
|
+
has_next: T.boolean,
|
45
|
+
has_prev: T.boolean
|
46
|
+
)
|
47
|
+
))
|
48
|
+
.summary("List users")
|
49
|
+
.description("Get a paginated list of users with optional search")
|
50
|
+
.tag("Users")
|
51
|
+
|
52
|
+
# Get user by ID
|
53
|
+
GET('/api/v1/users/:id')
|
54
|
+
.in(path(:id, T.integer))
|
55
|
+
.out(json_body(user: user_type))
|
56
|
+
.summary("Get user")
|
57
|
+
.description("Get a specific user by ID")
|
58
|
+
.tag("Users")
|
59
|
+
|
60
|
+
# Create new user
|
61
|
+
POST('/api/v1/users')
|
62
|
+
.in(json_body(create_user_type))
|
63
|
+
.out(json_body(user: user_type), 201)
|
64
|
+
.summary("Create user")
|
65
|
+
.description("Create a new user account")
|
66
|
+
.tag("Users")
|
67
|
+
|
68
|
+
# Update user
|
69
|
+
PUT('/api/v1/users/:id')
|
70
|
+
.in(path(:id, T.integer))
|
71
|
+
.in(json_body(update_user_type))
|
72
|
+
.out(json_body(user: user_type))
|
73
|
+
.summary("Update user")
|
74
|
+
.description("Update an existing user")
|
75
|
+
.tag("Users")
|
76
|
+
|
77
|
+
# Delete user
|
78
|
+
DELETE('/api/v1/users/:id')
|
79
|
+
.in(path(:id, T.integer))
|
80
|
+
.out(json_body(message: T.string))
|
81
|
+
.summary("Delete user")
|
82
|
+
.description("Delete a user account")
|
83
|
+
.tag("Users")
|
84
|
+
|
85
|
+
# Get user's posts
|
86
|
+
GET('/api/v1/users/:id/posts')
|
87
|
+
.in(path(:id, T.integer))
|
88
|
+
.in(query(:published, T.boolean.optional))
|
89
|
+
.in(query(:page, T.integer.default(1)))
|
90
|
+
.out(json_body(
|
91
|
+
posts: T.array(T.hash(
|
92
|
+
id: T.integer,
|
93
|
+
title: T.string,
|
94
|
+
excerpt: T.string,
|
95
|
+
published: T.boolean,
|
96
|
+
created_at: T.string
|
97
|
+
)),
|
98
|
+
pagination: T.hash(
|
99
|
+
page: T.integer,
|
100
|
+
total: T.integer,
|
101
|
+
total_pages: T.integer
|
102
|
+
)
|
103
|
+
))
|
104
|
+
.summary("Get user posts")
|
105
|
+
.description("Get all posts by a specific user")
|
106
|
+
.tag("Users")
|
107
|
+
end
|
108
|
+
|
109
|
+
def list_users
|
110
|
+
users_scope = User.includes(:posts)
|
111
|
+
|
112
|
+
# Apply search filter
|
113
|
+
if inputs[:search].present?
|
114
|
+
search_term = "%#{inputs[:search]}%"
|
115
|
+
users_scope = users_scope.where(
|
116
|
+
"name ILIKE ? OR email ILIKE ?", search_term, search_term
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Apply sorting
|
121
|
+
order_clause = "#{inputs[:sort]} #{inputs[:order]}"
|
122
|
+
users_scope = users_scope.order(order_clause)
|
123
|
+
|
124
|
+
# Pagination
|
125
|
+
page = inputs[:page]
|
126
|
+
per_page = [inputs[:per_page], 100].min # Cap at 100
|
127
|
+
|
128
|
+
users = users_scope.page(page).per(per_page)
|
129
|
+
|
130
|
+
{
|
131
|
+
users: users.map { |user| serialize_user(user) },
|
132
|
+
pagination: pagination_metadata(users_scope, page, per_page)
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_user
|
137
|
+
user = User.includes(:posts).find_by(id: inputs[:id])
|
138
|
+
return render_error("User not found", 404) unless user
|
139
|
+
|
140
|
+
{ user: serialize_user(user) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def create_user
|
144
|
+
user = User.new(user_create_params)
|
145
|
+
|
146
|
+
if user.save
|
147
|
+
render json: { user: serialize_user(user) }, status: 201
|
148
|
+
else
|
149
|
+
render_error("Validation failed", 422, errors: user.errors.full_messages)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def update_user
|
154
|
+
user = User.find_by(id: inputs[:id])
|
155
|
+
return render_error("User not found", 404) unless user
|
156
|
+
|
157
|
+
if user.update(user_update_params)
|
158
|
+
{ user: serialize_user(user) }
|
159
|
+
else
|
160
|
+
render_error("Validation failed", 422, errors: user.errors.full_messages)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def delete_user
|
165
|
+
user = User.find_by(id: inputs[:id])
|
166
|
+
return render_error("User not found", 404) unless user
|
167
|
+
|
168
|
+
user.destroy
|
169
|
+
{ message: "User deleted successfully" }
|
170
|
+
end
|
171
|
+
|
172
|
+
def get_user_posts
|
173
|
+
user = User.find_by(id: inputs[:id])
|
174
|
+
return render_error("User not found", 404) unless user
|
175
|
+
|
176
|
+
posts_scope = user.posts
|
177
|
+
posts_scope = posts_scope.where(published: inputs[:published]) if inputs.key?(:published)
|
178
|
+
|
179
|
+
page = inputs[:page]
|
180
|
+
posts = posts_scope.page(page).per(10)
|
181
|
+
|
182
|
+
{
|
183
|
+
posts: posts.map { |post| serialize_post_summary(post) },
|
184
|
+
pagination: pagination_metadata(posts_scope, page, 10)
|
185
|
+
}
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
def user_create_params
|
191
|
+
inputs.slice(:email, :name, :bio, :password)
|
192
|
+
end
|
193
|
+
|
194
|
+
def user_update_params
|
195
|
+
inputs.slice(:email, :name, :bio).compact
|
196
|
+
end
|
197
|
+
|
198
|
+
def serialize_user(user)
|
199
|
+
{
|
200
|
+
id: user.id,
|
201
|
+
email: user.email,
|
202
|
+
name: user.name,
|
203
|
+
bio: user.bio,
|
204
|
+
avatar_url: user.avatar_url,
|
205
|
+
created_at: user.created_at.iso8601,
|
206
|
+
updated_at: user.updated_at.iso8601,
|
207
|
+
posts_count: user.posts.count
|
208
|
+
}
|
209
|
+
end
|
210
|
+
|
211
|
+
def serialize_post_summary(post)
|
212
|
+
{
|
213
|
+
id: post.id,
|
214
|
+
title: post.title,
|
215
|
+
excerpt: post.content.truncate(200),
|
216
|
+
published: post.published,
|
217
|
+
created_at: post.created_at.iso8601
|
218
|
+
}
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ApplicationController < RapiTapir::Server::Rails::ControllerBase
|
4
|
+
# Global configuration for all controllers
|
5
|
+
rapitapir do
|
6
|
+
# Enable development features (automatic docs, etc.)
|
7
|
+
development_defaults! if Rails.env.development?
|
8
|
+
|
9
|
+
# Global error handling
|
10
|
+
error_out(json_body(error: T.string, details: T.string.optional), 500)
|
11
|
+
error_out(json_body(error: T.string), 401)
|
12
|
+
error_out(json_body(error: T.string), 403)
|
13
|
+
error_out(json_body(error: T.string), 404)
|
14
|
+
error_out(json_body(error: T.string, errors: T.array(T.string).optional), 422)
|
15
|
+
|
16
|
+
# Health check endpoint - no separate controller needed!
|
17
|
+
GET('/health')
|
18
|
+
.out(json_body(
|
19
|
+
status: T.string,
|
20
|
+
timestamp: T.string,
|
21
|
+
version: T.string,
|
22
|
+
environment: T.string,
|
23
|
+
database: T.string,
|
24
|
+
services: T.hash(redis: T.string)
|
25
|
+
))
|
26
|
+
.summary("Health check")
|
27
|
+
.description("Check API and service health")
|
28
|
+
.tag("System")
|
29
|
+
end
|
30
|
+
|
31
|
+
def health_check
|
32
|
+
{
|
33
|
+
status: 'ok',
|
34
|
+
timestamp: Time.current.iso8601,
|
35
|
+
version: '1.0.0',
|
36
|
+
environment: Rails.env,
|
37
|
+
database: database_status,
|
38
|
+
services: {
|
39
|
+
redis: redis_status
|
40
|
+
}
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
# Helper method for standardized error responses
|
47
|
+
def render_error(message, status, details: nil, errors: nil)
|
48
|
+
payload = { error: message }
|
49
|
+
payload[:details] = details if details
|
50
|
+
payload[:errors] = errors if errors
|
51
|
+
|
52
|
+
render json: payload, status: status
|
53
|
+
end
|
54
|
+
|
55
|
+
# Helper for pagination metadata
|
56
|
+
def pagination_metadata(collection, page, per_page)
|
57
|
+
total = collection.respond_to?(:count) ? collection.count : collection.size
|
58
|
+
{
|
59
|
+
page: page,
|
60
|
+
per_page: per_page,
|
61
|
+
total: total,
|
62
|
+
total_pages: (total.to_f / per_page).ceil,
|
63
|
+
has_next: page < (total.to_f / per_page).ceil,
|
64
|
+
has_prev: page > 1
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def database_status
|
71
|
+
ActiveRecord::Base.connection.execute('SELECT 1')
|
72
|
+
'connected'
|
73
|
+
rescue => e
|
74
|
+
Rails.logger.error "Database check failed: #{e.message}"
|
75
|
+
'disconnected'
|
76
|
+
end
|
77
|
+
|
78
|
+
def redis_status
|
79
|
+
# Example Redis check - uncomment if using Redis
|
80
|
+
# Redis.current.ping == 'PONG' ? 'connected' : 'disconnected'
|
81
|
+
'not_configured'
|
82
|
+
rescue => e
|
83
|
+
Rails.logger.error "Redis check failed: #{e.message}"
|
84
|
+
'disconnected'
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ApplicationController < RapiTapir::Server::Rails::ControllerBase
|
4
|
+
include RapiTapir::Server::Rails::DocumentationHelpers
|
5
|
+
|
6
|
+
rapitapir do
|
7
|
+
# Enable development features (automatic docs, etc.)
|
8
|
+
development_defaults! if Rails.env.development?
|
9
|
+
|
10
|
+
# Global error handling
|
11
|
+
error_out(json_body(error: T.string, details: T.string.optional), 500)
|
12
|
+
error_out(json_body(error: T.string), 401)
|
13
|
+
error_out(json_body(error: T.string), 403)
|
14
|
+
error_out(json_body(error: T.string), 404)
|
15
|
+
error_out(json_body(error: T.string, errors: T.array(T.string).optional), 422)
|
16
|
+
|
17
|
+
# Health check endpoint - no separate controller needed!
|
18
|
+
GET('/health')
|
19
|
+
.out(json_body(
|
20
|
+
status: T.string,
|
21
|
+
timestamp: T.string,
|
22
|
+
version: T.string,
|
23
|
+
environment: T.string,
|
24
|
+
database: T.string,
|
25
|
+
services: T.hash(redis: T.string)
|
26
|
+
))
|
27
|
+
.summary("Health check")
|
28
|
+
.description("Check API and service health")
|
29
|
+
.tag("System")
|
30
|
+
end
|
31
|
+
|
32
|
+
def health_check
|
33
|
+
{
|
34
|
+
status: 'ok',
|
35
|
+
timestamp: Time.current.iso8601,
|
36
|
+
version: '1.0.0',
|
37
|
+
environment: Rails.env,
|
38
|
+
database: database_status,
|
39
|
+
services: {
|
40
|
+
redis: redis_status
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# Helper method for standardized error responses
|
48
|
+
def render_error(message, status, details: nil, errors: nil)
|
49
|
+
payload = { error: message }
|
50
|
+
payload[:details] = details if details
|
51
|
+
payload[:errors] = errors if errors
|
52
|
+
|
53
|
+
render json: payload, status: status
|
54
|
+
end
|
55
|
+
|
56
|
+
# Helper for pagination metadata
|
57
|
+
def pagination_metadata(collection, page, per_page)
|
58
|
+
total = collection.respond_to?(:count) ? collection.count : collection.size
|
59
|
+
{
|
60
|
+
page: page,
|
61
|
+
per_page: per_page,
|
62
|
+
total: total,
|
63
|
+
total_pages: (total.to_f / per_page).ceil,
|
64
|
+
has_next: page < (total.to_f / per_page).ceil,
|
65
|
+
has_prev: page > 1
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def database_status
|
72
|
+
ActiveRecord::Base.connection.execute('SELECT 1')
|
73
|
+
'connected'
|
74
|
+
rescue => e
|
75
|
+
Rails.logger.error "Database check failed: #{e.message}"
|
76
|
+
'disconnected'
|
77
|
+
end
|
78
|
+
|
79
|
+
def redis_status
|
80
|
+
# Example Redis check - uncomment if using Redis
|
81
|
+
# Redis.current.ping == 'PONG' ? 'connected' : 'disconnected'
|
82
|
+
'not_configured'
|
83
|
+
rescue => e
|
84
|
+
Rails.logger.error "Redis check failed: #{e.message}"
|
85
|
+
'disconnected'
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DocumentationController < ApplicationController
|
4
|
+
skip_before_action :verify_authenticity_token, if: -> { request.format.json? }
|
5
|
+
|
6
|
+
def swagger_ui
|
7
|
+
render html: swagger_ui_html.html_safe
|
8
|
+
end
|
9
|
+
|
10
|
+
def openapi_spec
|
11
|
+
render json: generate_openapi_spec
|
12
|
+
end
|
13
|
+
|
14
|
+
def redoc
|
15
|
+
render html: redoc_html.html_safe
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def swagger_ui_html
|
21
|
+
<<~HTML
|
22
|
+
<!DOCTYPE html>
|
23
|
+
<html>
|
24
|
+
<head>
|
25
|
+
<title>#{Rails.application.class.name.split('::').first} API Documentation</title>
|
26
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
|
27
|
+
<style>
|
28
|
+
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
|
29
|
+
*, *:before, *:after { box-sizing: inherit; }
|
30
|
+
body { margin:0; background: #fafafa; }
|
31
|
+
</style>
|
32
|
+
</head>
|
33
|
+
<body>
|
34
|
+
<div id="swagger-ui"></div>
|
35
|
+
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
|
36
|
+
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script>
|
37
|
+
<script>
|
38
|
+
window.onload = function() {
|
39
|
+
const ui = SwaggerUIBundle({
|
40
|
+
url: '/openapi.json',
|
41
|
+
dom_id: '#swagger-ui',
|
42
|
+
deepLinking: true,
|
43
|
+
presets: [
|
44
|
+
SwaggerUIBundle.presets.apis,
|
45
|
+
SwaggerUIStandalonePreset
|
46
|
+
],
|
47
|
+
plugins: [
|
48
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
49
|
+
],
|
50
|
+
layout: "StandaloneLayout"
|
51
|
+
});
|
52
|
+
};
|
53
|
+
</script>
|
54
|
+
</body>
|
55
|
+
</html>
|
56
|
+
HTML
|
57
|
+
end
|
58
|
+
|
59
|
+
def redoc_html
|
60
|
+
<<~HTML
|
61
|
+
<!DOCTYPE html>
|
62
|
+
<html>
|
63
|
+
<head>
|
64
|
+
<title>#{Rails.application.class.name.split('::').first} API Documentation</title>
|
65
|
+
<meta charset="utf-8"/>
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
67
|
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
68
|
+
<style>
|
69
|
+
body { margin: 0; padding: 0; }
|
70
|
+
</style>
|
71
|
+
</head>
|
72
|
+
<body>
|
73
|
+
<redoc spec-url='/openapi.json'></redoc>
|
74
|
+
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0/bundles/redoc.standalone.js"></script>
|
75
|
+
</body>
|
76
|
+
</html>
|
77
|
+
HTML
|
78
|
+
end
|
79
|
+
|
80
|
+
def generate_openapi_spec
|
81
|
+
{
|
82
|
+
openapi: "3.0.3",
|
83
|
+
info: {
|
84
|
+
title: "#{Rails.application.class.name.split('::').first} API",
|
85
|
+
version: "1.0.0",
|
86
|
+
description: "API documentation generated by RapiTapir",
|
87
|
+
contact: {
|
88
|
+
name: "API Support",
|
89
|
+
email: "support@example.com"
|
90
|
+
}
|
91
|
+
},
|
92
|
+
servers: [
|
93
|
+
{
|
94
|
+
url: "#{request.protocol}#{request.host_with_port}",
|
95
|
+
description: "#{Rails.env.capitalize} server"
|
96
|
+
}
|
97
|
+
],
|
98
|
+
paths: generate_paths,
|
99
|
+
components: {
|
100
|
+
securitySchemes: {
|
101
|
+
bearerAuth: {
|
102
|
+
type: "http",
|
103
|
+
scheme: "bearer",
|
104
|
+
bearerFormat: "JWT"
|
105
|
+
}
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def generate_paths
|
112
|
+
paths = {}
|
113
|
+
|
114
|
+
# Get all RapiTapir controllers
|
115
|
+
rapitapir_controllers = [
|
116
|
+
Api::V1::UsersController,
|
117
|
+
Api::V1::PostsController
|
118
|
+
]
|
119
|
+
|
120
|
+
rapitapir_controllers.each do |controller|
|
121
|
+
next unless controller.respond_to?(:endpoints)
|
122
|
+
|
123
|
+
controller.endpoints.each do |endpoint|
|
124
|
+
path_key = endpoint.path.gsub(/:(\w+)/, '{\1}')
|
125
|
+
method = endpoint.method.downcase
|
126
|
+
|
127
|
+
paths[path_key] ||= {}
|
128
|
+
paths[path_key][method] = {
|
129
|
+
summary: endpoint.summary || "#{method.upcase} #{path_key}",
|
130
|
+
description: endpoint.description || "",
|
131
|
+
tags: endpoint.tags || [controller.name.demodulize.gsub('Controller', '')],
|
132
|
+
operationId: "#{method}_#{path_key.gsub(/[\/{}]/, '_').gsub(/_+/, '_').gsub(/^_|_$/, '')}",
|
133
|
+
responses: {
|
134
|
+
"200" => {
|
135
|
+
description: "Success"
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
# Add authentication if required
|
141
|
+
if endpoint.inputs.any? { |input| input.name == :authorization }
|
142
|
+
paths[path_key][method][:security] = [{ bearerAuth: [] }]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
paths
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HealthController < ApplicationController
|
4
|
+
skip_before_action :verify_authenticity_token
|
5
|
+
|
6
|
+
def check
|
7
|
+
render json: {
|
8
|
+
status: 'ok',
|
9
|
+
timestamp: Time.current.iso8601,
|
10
|
+
version: '1.0.0',
|
11
|
+
environment: Rails.env,
|
12
|
+
database: database_status,
|
13
|
+
services: services_status
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def database_status
|
20
|
+
ActiveRecord::Base.connection.execute('SELECT 1')
|
21
|
+
'connected'
|
22
|
+
rescue => e
|
23
|
+
Rails.logger.error "Database check failed: #{e.message}"
|
24
|
+
'disconnected'
|
25
|
+
end
|
26
|
+
|
27
|
+
def services_status
|
28
|
+
{
|
29
|
+
redis: redis_status,
|
30
|
+
# Add other service checks here
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def redis_status
|
35
|
+
# Example Redis check - uncomment if using Redis
|
36
|
+
# Redis.current.ping == 'PONG' ? 'connected' : 'disconnected'
|
37
|
+
'not_configured'
|
38
|
+
rescue => e
|
39
|
+
Rails.logger.error "Redis check failed: #{e.message}"
|
40
|
+
'disconnected'
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Rails.application.routes.draw do
|
4
|
+
# Health check (from ApplicationController)
|
5
|
+
rapitapir_routes_for ApplicationController
|
6
|
+
|
7
|
+
# API v1 routes - RapiTapir auto-generates routes from endpoint definitions
|
8
|
+
namespace :api do
|
9
|
+
namespace :v1 do
|
10
|
+
rapitapir_routes_for 'Api::V1::UsersController'
|
11
|
+
rapitapir_routes_for 'Api::V1::PostsController'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Documentation routes (automatically added by development_defaults! in development)
|
16
|
+
# Available at:
|
17
|
+
# - GET /docs -> Swagger UI
|
18
|
+
# - GET /openapi.json -> OpenAPI 3.0 specification
|
19
|
+
# - GET /redoc -> ReDoc alternative UI (if enabled)
|
20
|
+
|
21
|
+
# Catch-all for unmatched routes
|
22
|
+
match '*path', to: proc { |env|
|
23
|
+
[404, { 'Content-Type' => 'application/json' }, [{ error: 'Route not found' }.to_json]]
|
24
|
+
}, via: :all
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Simplified Rails application using RapiTapir best practices
|
4
|
+
# No separate health or documentation controllers needed!
|
5
|
+
|
6
|
+
Rails.application.routes.draw do
|
7
|
+
# Health check and docs are handled by RapiTapir automatically
|
8
|
+
rapitapir_routes_for ApplicationController
|
9
|
+
|
10
|
+
# API endpoints
|
11
|
+
namespace :api do
|
12
|
+
namespace :v1 do
|
13
|
+
rapitapir_routes_for 'Api::V1::UsersController'
|
14
|
+
rapitapir_routes_for 'Api::V1::PostsController'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# RapiTapir provides documentation routes automatically in development
|
19
|
+
if Rails.env.development?
|
20
|
+
# These are automatically added by development_defaults!
|
21
|
+
# /docs -> Swagger UI
|
22
|
+
# /openapi.json -> OpenAPI spec
|
23
|
+
# /redoc -> ReDoc alternative UI
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Rails.application.routes.draw do
|
4
|
+
# Use RapiTapir's built-in route generation for all controllers
|
5
|
+
rapitapir_routes_for ApplicationController # This includes /health
|
6
|
+
|
7
|
+
# API v1 routes
|
8
|
+
namespace :api do
|
9
|
+
namespace :v1 do
|
10
|
+
rapitapir_routes_for 'Api::V1::UsersController'
|
11
|
+
rapitapir_routes_for 'Api::V1::PostsController'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Documentation routes using RapiTapir's built-in helpers
|
16
|
+
if Rails.env.development?
|
17
|
+
# These use the DocumentationHelpers module that's already included
|
18
|
+
get '/docs', to: proc { |env|
|
19
|
+
request = ActionDispatch::Request.new(env)
|
20
|
+
html = ApplicationController.new.send(:generate_swagger_ui_html)
|
21
|
+
[200, { 'Content-Type' => 'text/html' }, [html]]
|
22
|
+
}
|
23
|
+
|
24
|
+
get '/openapi.json', to: proc { |env|
|
25
|
+
request = ActionDispatch::Request.new(env)
|
26
|
+
controllers = [ApplicationController, Api::V1::UsersController, Api::V1::PostsController]
|
27
|
+
spec = ApplicationController.new.send(:generate_openapi_spec_for_controllers, controllers)
|
28
|
+
[200, { 'Content-Type' => 'application/json' }, [JSON.pretty_generate(spec)]]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Catch-all for unmatched routes
|
33
|
+
match '*path', to: proc { |env|
|
34
|
+
[404, { 'Content-Type' => 'application/json' }, [{ error: 'Route not found' }.to_json]]
|
35
|
+
}, via: :all
|
36
|
+
end
|