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
@@ -14,6 +14,11 @@ module RapiTapir
|
|
14
14
|
# inputs: [path_param(:id, :integer)],
|
15
15
|
# outputs: [json_output(user_schema)]
|
16
16
|
# )
|
17
|
+
# method: :get,
|
18
|
+
# path: '/users/{id}',
|
19
|
+
# inputs: [path_param(:id, :integer)],
|
20
|
+
# outputs: [json_output(user_schema)]
|
21
|
+
# )
|
17
22
|
class Endpoint
|
18
23
|
HTTP_METHODS = %i[get post put patch delete options head].freeze
|
19
24
|
|
@@ -77,6 +82,58 @@ module RapiTapir
|
|
77
82
|
with_metadata(deprecated: flag_value)
|
78
83
|
end
|
79
84
|
|
85
|
+
# Mark this endpoint for MCP export
|
86
|
+
def mcp_export
|
87
|
+
with_metadata(mcp_export: true)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns true if this endpoint is marked for MCP export
|
91
|
+
def mcp_export?
|
92
|
+
metadata[:mcp_export] == true
|
93
|
+
end
|
94
|
+
|
95
|
+
# Configure RAG inference for this endpoint
|
96
|
+
def rag_inference(llm:, retrieval:, context_fields: [], config: {})
|
97
|
+
rag_config = {
|
98
|
+
llm: llm,
|
99
|
+
retrieval: retrieval,
|
100
|
+
context_fields: context_fields,
|
101
|
+
config: config
|
102
|
+
}
|
103
|
+
with_metadata(rag_inference: rag_config)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns true if this endpoint has RAG inference configured
|
107
|
+
def rag_inference?
|
108
|
+
metadata.key?(:rag_inference)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Get RAG configuration for this endpoint
|
112
|
+
def rag_config
|
113
|
+
metadata[:rag_inference]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Configure LLM instruction generation for this endpoint
|
117
|
+
def llm_instruction(purpose:, fields: :all, **options)
|
118
|
+
llm_config = {
|
119
|
+
purpose: purpose,
|
120
|
+
fields: fields,
|
121
|
+
options: options
|
122
|
+
}
|
123
|
+
|
124
|
+
with_metadata(llm_instruction: llm_config)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Check if this endpoint has LLM instruction generation configured
|
128
|
+
def llm_instruction?
|
129
|
+
metadata.key?(:llm_instruction)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Get LLM instruction configuration for this endpoint
|
133
|
+
def llm_instruction_config
|
134
|
+
metadata[:llm_instruction]
|
135
|
+
end
|
136
|
+
|
80
137
|
# Validate input/output types for a given input/output hash
|
81
138
|
# Raises validation errors if invalid, returns true if valid
|
82
139
|
def validate!(input_hash = {}, output_hash = {}) # rubocop:disable Naming/PredicateMethod
|
@@ -141,14 +198,10 @@ module RapiTapir
|
|
141
198
|
def validate_outputs!(output_hash)
|
142
199
|
outputs.each do |output|
|
143
200
|
if output.type.is_a?(Hash)
|
144
|
-
unless output.valid_type?(output_hash)
|
145
|
-
raise TypeError, "Invalid output hash: expected #{output.type}, got #{output_hash}"
|
146
|
-
end
|
201
|
+
raise TypeError, "Invalid output hash: expected #{output.type}, got #{output_hash}" unless output.valid_type?(output_hash)
|
147
202
|
else
|
148
203
|
output_hash.each do |k, v|
|
149
|
-
unless output.valid_type?(v)
|
150
|
-
raise TypeError, "Invalid type for output '#{k}': expected #{output.type}, got #{v.class}"
|
151
|
-
end
|
204
|
+
raise TypeError, "Invalid type for output '#{k}': expected #{output.type}, got #{v.class}" unless output.valid_type?(v)
|
152
205
|
end
|
153
206
|
end
|
154
207
|
end
|
@@ -210,17 +210,13 @@ module RapiTapir
|
|
210
210
|
|
211
211
|
def validate_security_schemes!
|
212
212
|
security_schemes.each do |scheme|
|
213
|
-
unless %i[bearer api_key basic].include?(scheme.options[:auth_type])
|
214
|
-
raise ArgumentError, "Unknown authentication type: #{scheme.options[:auth_type]}"
|
215
|
-
end
|
213
|
+
raise ArgumentError, "Unknown authentication type: #{scheme.options[:auth_type]}" unless %i[bearer api_key basic].include?(scheme.options[:auth_type])
|
216
214
|
end
|
217
215
|
end
|
218
216
|
|
219
217
|
def validate_type_consistency!
|
220
218
|
inputs.each do |input|
|
221
|
-
unless input.type.respond_to?(:validate)
|
222
|
-
raise ArgumentError, "Input '#{input.name}' type does not support validation"
|
223
|
-
end
|
219
|
+
raise ArgumentError, "Input '#{input.name}' type does not support validation" unless input.type.respond_to?(:validate)
|
224
220
|
end
|
225
221
|
|
226
222
|
outputs.each do |output|
|
@@ -159,6 +159,9 @@ module RapiTapir
|
|
159
159
|
copy_with(errors: @errors + [error])
|
160
160
|
end
|
161
161
|
|
162
|
+
# Alias for compatibility with Core::Endpoint
|
163
|
+
alias error_out error_response
|
164
|
+
|
162
165
|
def bad_request(type_def = nil, **options)
|
163
166
|
error_response(400, type_def, **options)
|
164
167
|
end
|
@@ -311,6 +314,56 @@ module RapiTapir
|
|
311
314
|
**options
|
312
315
|
)
|
313
316
|
end
|
317
|
+
|
318
|
+
public
|
319
|
+
|
320
|
+
# AI/MCP Extensions
|
321
|
+
def mcp_export
|
322
|
+
copy_with(metadata: @metadata.merge(mcp_export: true))
|
323
|
+
end
|
324
|
+
|
325
|
+
def mcp_export?
|
326
|
+
@metadata[:mcp_export] == true
|
327
|
+
end
|
328
|
+
|
329
|
+
def rag_inference(llm:, retrieval:, context_fields: [], config: {})
|
330
|
+
copy_with(metadata: @metadata.merge(
|
331
|
+
rag_inference: true,
|
332
|
+
rag_config: {
|
333
|
+
llm: llm,
|
334
|
+
retrieval: retrieval,
|
335
|
+
context_fields: context_fields,
|
336
|
+
config: config
|
337
|
+
}
|
338
|
+
))
|
339
|
+
end
|
340
|
+
|
341
|
+
def rag_inference?
|
342
|
+
@metadata[:rag_inference] == true
|
343
|
+
end
|
344
|
+
|
345
|
+
def rag_config
|
346
|
+
@metadata[:rag_config]
|
347
|
+
end
|
348
|
+
|
349
|
+
# LLM Instruction Generation
|
350
|
+
def llm_instruction(purpose:, fields: :all, **options)
|
351
|
+
copy_with(metadata: @metadata.merge(
|
352
|
+
llm_instruction: {
|
353
|
+
purpose: purpose,
|
354
|
+
fields: fields,
|
355
|
+
options: options
|
356
|
+
}
|
357
|
+
))
|
358
|
+
end
|
359
|
+
|
360
|
+
def llm_instruction?
|
361
|
+
@metadata.key?(:llm_instruction)
|
362
|
+
end
|
363
|
+
|
364
|
+
def llm_instruction_config
|
365
|
+
@metadata[:llm_instruction]
|
366
|
+
end
|
314
367
|
end
|
315
368
|
end
|
316
369
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
# Global endpoint registry for collecting and managing endpoints
|
5
|
+
# Provides a central place to register and discover endpoints for CLI operations
|
6
|
+
class EndpointRegistry
|
7
|
+
@endpoints = []
|
8
|
+
@mutex = Mutex.new
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Register an endpoint in the global registry
|
12
|
+
def register(endpoint)
|
13
|
+
@mutex.synchronize do
|
14
|
+
@endpoints << endpoint unless @endpoints.include?(endpoint)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get all registered endpoints
|
19
|
+
def all
|
20
|
+
@mutex.synchronize { @endpoints.dup }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get only endpoints marked for MCP export
|
24
|
+
def mcp_endpoints
|
25
|
+
@mutex.synchronize { @endpoints.select(&:mcp_export?) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Clear all registered endpoints (useful for testing)
|
29
|
+
def clear!
|
30
|
+
@mutex.synchronize { @endpoints.clear }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Register multiple endpoints at once
|
34
|
+
def register_all(endpoints)
|
35
|
+
endpoints.each { |endpoint| register(endpoint) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Find endpoints by method and path pattern
|
39
|
+
def find_by(method: nil, path: nil)
|
40
|
+
results = all
|
41
|
+
results = results.select { |ep| ep.method == method } if method
|
42
|
+
results = results.select { |ep| ep.path&.include?(path) } if path
|
43
|
+
results
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -73,8 +73,8 @@ module RapiTapir
|
|
73
73
|
register_default_checks
|
74
74
|
end
|
75
75
|
|
76
|
-
def register(name, description = nil, &
|
77
|
-
@checks << Check.new(name, description, &
|
76
|
+
def register(name, description = nil, &)
|
77
|
+
@checks << Check.new(name, description, &)
|
78
78
|
end
|
79
79
|
|
80
80
|
def run_all
|
@@ -210,9 +210,9 @@ module RapiTapir
|
|
210
210
|
@endpoint_path = endpoint
|
211
211
|
end
|
212
212
|
|
213
|
-
def register(name, description = nil, &
|
213
|
+
def register(name, description = nil, &)
|
214
214
|
@registry ||= Registry.new
|
215
|
-
@registry.register(name, description, &
|
215
|
+
@registry.register(name, description, &)
|
216
216
|
end
|
217
217
|
|
218
218
|
def endpoint
|
@@ -22,24 +22,24 @@ module RapiTapir
|
|
22
22
|
@logger.formatter = @formatter
|
23
23
|
end
|
24
24
|
|
25
|
-
def debug(message = nil, **fields, &
|
26
|
-
log(:debug, message, **fields, &
|
25
|
+
def debug(message = nil, **fields, &)
|
26
|
+
log(:debug, message, **fields, &)
|
27
27
|
end
|
28
28
|
|
29
|
-
def info(message = nil, **fields, &
|
30
|
-
log(:info, message, **fields, &
|
29
|
+
def info(message = nil, **fields, &)
|
30
|
+
log(:info, message, **fields, &)
|
31
31
|
end
|
32
32
|
|
33
|
-
def warn(message = nil, **fields, &
|
34
|
-
log(:warn, message, **fields, &
|
33
|
+
def warn(message = nil, **fields, &)
|
34
|
+
log(:warn, message, **fields, &)
|
35
35
|
end
|
36
36
|
|
37
|
-
def error(message = nil, **fields, &
|
38
|
-
log(:error, message, **fields, &
|
37
|
+
def error(message = nil, **fields, &)
|
38
|
+
log(:error, message, **fields, &)
|
39
39
|
end
|
40
40
|
|
41
|
-
def fatal(message = nil, **fields, &
|
42
|
-
log(:fatal, message, **fields, &
|
41
|
+
def fatal(message = nil, **fields, &)
|
42
|
+
log(:fatal, message, **fields, &)
|
43
43
|
end
|
44
44
|
|
45
45
|
def log_request(**options)
|
data/lib/rapitapir/schema.rb
CHANGED
@@ -19,9 +19,7 @@ module RapiTapir
|
|
19
19
|
|
20
20
|
# Register an endpoint with the adapter
|
21
21
|
def register_endpoint(endpoint, handler)
|
22
|
-
unless endpoint.is_a?(RapiTapir::Core::Endpoint)
|
23
|
-
raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint'
|
24
|
-
end
|
22
|
+
raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint' unless endpoint.is_a?(RapiTapir::Core::Endpoint)
|
25
23
|
raise ArgumentError, 'Handler must respond to call' unless handler.respond_to?(:call)
|
26
24
|
|
27
25
|
endpoint.validate!
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Server
|
5
|
+
module Rails
|
6
|
+
# Simple configuration class for Rails integration
|
7
|
+
# Provides basic configuration options needed for Rails controllers
|
8
|
+
class Configuration
|
9
|
+
attr_accessor :docs_path, :openapi_path, :health_check_enabled, :health_check_path
|
10
|
+
attr_reader :api_info, :servers, :public_paths, :auth_schemes
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@api_info = {
|
14
|
+
title: 'API Documentation',
|
15
|
+
description: 'Generated with RapiTapir',
|
16
|
+
version: '1.0.0'
|
17
|
+
}
|
18
|
+
@servers = []
|
19
|
+
@public_paths = []
|
20
|
+
@auth_schemes = {}
|
21
|
+
@docs_path = '/docs'
|
22
|
+
@openapi_path = '/openapi.json'
|
23
|
+
@health_check_enabled = false
|
24
|
+
@health_check_path = '/health'
|
25
|
+
end
|
26
|
+
|
27
|
+
# API Information configuration
|
28
|
+
def info(title: nil, description: nil, version: nil, **options)
|
29
|
+
@api_info[:title] = title if title
|
30
|
+
@api_info[:description] = description if description
|
31
|
+
@api_info[:version] = version if version
|
32
|
+
@api_info.merge!(options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Server configuration
|
36
|
+
def server(url:, description: nil)
|
37
|
+
@servers << { url: url, description: description }.compact
|
38
|
+
end
|
39
|
+
|
40
|
+
# Add paths that don't require authentication
|
41
|
+
def add_public_paths(*paths)
|
42
|
+
@public_paths.concat(paths.flatten.map(&:to_s))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Authentication scheme management
|
46
|
+
def add_auth_scheme(name, scheme)
|
47
|
+
@auth_schemes[name] = scheme
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_auth_scheme(name)
|
51
|
+
@auth_schemes[name]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Enable development defaults (docs endpoints, etc.)
|
55
|
+
def development_defaults!
|
56
|
+
# This would set up automatic documentation endpoints
|
57
|
+
# For now, just enable health checks
|
58
|
+
@health_check_enabled = true
|
59
|
+
|
60
|
+
# Enable docs
|
61
|
+
enable_docs
|
62
|
+
end
|
63
|
+
|
64
|
+
# Enable documentation endpoints
|
65
|
+
def enable_docs(path: '/docs', openapi_path: '/openapi.json')
|
66
|
+
@docs_path = path
|
67
|
+
@openapi_path = openapi_path
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check if docs are enabled
|
71
|
+
def docs_enabled?
|
72
|
+
!@docs_path.nil?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../rails_controller'
|
4
|
+
require_relative '../../dsl/http_verbs'
|
5
|
+
require_relative '../../types'
|
6
|
+
require_relative 'resource_builder'
|
7
|
+
require_relative 'configuration'
|
8
|
+
|
9
|
+
module RapiTapir
|
10
|
+
module Server
|
11
|
+
module Rails
|
12
|
+
# Enhanced Rails controller base class providing Sinatra-like experience
|
13
|
+
#
|
14
|
+
# This class bridges the gap between Sinatra's elegant DSL and Rails conventions,
|
15
|
+
# offering the same clean syntax that Sinatra developers enjoy.
|
16
|
+
#
|
17
|
+
# @example Basic usage
|
18
|
+
# class UsersController < RapiTapir::Server::Rails::ControllerBase
|
19
|
+
# rapitapir do
|
20
|
+
# info(title: 'Users API', version: '1.0.0')
|
21
|
+
# development_defaults!
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# USER_SCHEMA = T.hash({
|
25
|
+
# "id" => T.integer,
|
26
|
+
# "name" => T.string,
|
27
|
+
# "email" => T.email
|
28
|
+
# })
|
29
|
+
#
|
30
|
+
# endpoint(
|
31
|
+
# GET('/users')
|
32
|
+
# .summary('List users')
|
33
|
+
# .ok(T.array(USER_SCHEMA))
|
34
|
+
# .build
|
35
|
+
# ) { User.all.map(&:attributes) }
|
36
|
+
# end
|
37
|
+
class ControllerBase < ActionController::Base
|
38
|
+
include RapiTapir::Server::Rails::Controller
|
39
|
+
extend RapiTapir::DSL::HTTPVerbs
|
40
|
+
|
41
|
+
# Make T shortcut available in Rails controllers
|
42
|
+
T = RapiTapir::Types
|
43
|
+
|
44
|
+
class << self
|
45
|
+
# Configure RapiTapir for this controller
|
46
|
+
# Similar to Sinatra's rapitapir block
|
47
|
+
#
|
48
|
+
# @param block [Proc] Configuration block
|
49
|
+
# @example
|
50
|
+
# rapitapir do
|
51
|
+
# info(title: 'My API', version: '1.0.0')
|
52
|
+
# development_defaults!
|
53
|
+
# end
|
54
|
+
def rapitapir(&)
|
55
|
+
@rapitapir_config = Configuration.new
|
56
|
+
@rapitapir_config.instance_eval(&) if block_given?
|
57
|
+
setup_default_features
|
58
|
+
end
|
59
|
+
|
60
|
+
# Define an endpoint with automatic action generation
|
61
|
+
# This method creates both the endpoint definition and the corresponding Rails action
|
62
|
+
#
|
63
|
+
# @param endpoint_definition [RapiTapir::Core::Endpoint] The endpoint definition
|
64
|
+
# @param block [Proc] The endpoint handler
|
65
|
+
# @example
|
66
|
+
# endpoint(
|
67
|
+
# GET('/users/:id')
|
68
|
+
# .summary('Get user')
|
69
|
+
# .path_param(:id, T.integer)
|
70
|
+
# .ok(USER_SCHEMA)
|
71
|
+
# .build
|
72
|
+
# ) do |inputs|
|
73
|
+
# User.find(inputs[:id]).attributes
|
74
|
+
# end
|
75
|
+
def endpoint(endpoint_definition, &)
|
76
|
+
action_name = derive_action_name(endpoint_definition.path, endpoint_definition.method)
|
77
|
+
|
78
|
+
# Register endpoint with the Rails controller mixin
|
79
|
+
rapitapir_endpoint(action_name, endpoint_definition, &)
|
80
|
+
|
81
|
+
# Auto-generate Rails controller action that passes the correct action name
|
82
|
+
define_method(action_name) do
|
83
|
+
process_rapitapir_endpoint(action_name)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Create RESTful resource endpoints with CRUD operations
|
88
|
+
# Mirrors Sinatra's api_resource functionality for Rails
|
89
|
+
#
|
90
|
+
# @param path [String] Base path for the resource
|
91
|
+
# @param schema [Object] Schema for the resource
|
92
|
+
# @param block [Proc] Resource configuration block
|
93
|
+
# @example
|
94
|
+
# api_resource '/users', schema: USER_SCHEMA do
|
95
|
+
# crud do
|
96
|
+
# index { User.all.map(&:attributes) }
|
97
|
+
# show { |inputs| User.find(inputs[:id]).attributes }
|
98
|
+
# create { |inputs| User.create!(inputs[:body]).attributes }
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
def api_resource(path, schema:, &block)
|
102
|
+
resource_builder = ResourceBuilder.new(self, path, schema)
|
103
|
+
resource_builder.instance_eval(&block)
|
104
|
+
|
105
|
+
# Generate all CRUD endpoints and actions automatically
|
106
|
+
resource_builder.endpoints.each do |endpoint_def, handler|
|
107
|
+
endpoint(endpoint_def, &handler)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Enhanced HTTP verb methods that return FluentEndpointBuilder
|
112
|
+
# These override the mixin methods to ensure proper scoping
|
113
|
+
#
|
114
|
+
# @param path [String] The endpoint path
|
115
|
+
# @return [RapiTapir::DSL::FluentEndpointBuilder] Builder for method chaining
|
116
|
+
def GET(path)
|
117
|
+
RapiTapir.get(path)
|
118
|
+
end
|
119
|
+
|
120
|
+
def POST(path)
|
121
|
+
RapiTapir.post(path)
|
122
|
+
end
|
123
|
+
|
124
|
+
def PUT(path)
|
125
|
+
RapiTapir.put(path)
|
126
|
+
end
|
127
|
+
|
128
|
+
def PATCH(path)
|
129
|
+
RapiTapir.patch(path)
|
130
|
+
end
|
131
|
+
|
132
|
+
def DELETE(path)
|
133
|
+
RapiTapir.delete(path)
|
134
|
+
end
|
135
|
+
|
136
|
+
def HEAD(path)
|
137
|
+
RapiTapir.head(path)
|
138
|
+
end
|
139
|
+
|
140
|
+
def OPTIONS(path)
|
141
|
+
RapiTapir.options(path)
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# Set up default features similar to Sinatra extension
|
147
|
+
def setup_default_features
|
148
|
+
# Future: Auto-setup CORS, docs, health checks like Sinatra
|
149
|
+
# For now, we'll focus on the core functionality
|
150
|
+
end
|
151
|
+
|
152
|
+
# Derive Rails action name from HTTP method and path
|
153
|
+
# Follows Rails conventions for RESTful routes
|
154
|
+
#
|
155
|
+
# @param path [String] The endpoint path
|
156
|
+
# @param method [Symbol] The HTTP method
|
157
|
+
# @return [Symbol] The Rails action name
|
158
|
+
def derive_action_name(path, method)
|
159
|
+
# For custom endpoints, always derive from path to avoid conflicts
|
160
|
+
path_segments = path.split('/').reject(&:empty?)
|
161
|
+
|
162
|
+
return method.to_s.downcase.to_sym if path_segments.empty?
|
163
|
+
|
164
|
+
# Get the main path segment (not parameters)
|
165
|
+
main_segment = nil
|
166
|
+
path_segments.each do |segment|
|
167
|
+
unless segment.start_with?(':') || segment.start_with?('{')
|
168
|
+
main_segment = segment
|
169
|
+
break
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
if main_segment
|
174
|
+
# Create action name from method + segment to ensure uniqueness
|
175
|
+
:"#{method.to_s.downcase}_#{main_segment}"
|
176
|
+
else
|
177
|
+
# Fallback to method name
|
178
|
+
method.to_s.downcase.to_sym
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../openapi/schema_generator'
|
4
|
+
require_relative '../../sinatra/swagger_ui_generator'
|
5
|
+
|
6
|
+
module RapiTapir
|
7
|
+
module Server
|
8
|
+
module Rails
|
9
|
+
# Documentation helpers for Rails controllers
|
10
|
+
module DocumentationHelpers
|
11
|
+
# Generate OpenAPI specification from Rails controller endpoints
|
12
|
+
def generate_openapi_spec_for_controller(controller_class, config = {})
|
13
|
+
endpoints = []
|
14
|
+
|
15
|
+
endpoints = controller_class.rapitapir_endpoints.values.map { |ep_config| ep_config[:endpoint] } if controller_class.respond_to?(:rapitapir_endpoints)
|
16
|
+
|
17
|
+
info = {
|
18
|
+
title: 'API Documentation',
|
19
|
+
version: '1.0.0',
|
20
|
+
description: 'Generated with RapiTapir for Rails'
|
21
|
+
}.merge(config[:info] || {})
|
22
|
+
|
23
|
+
servers = config[:servers] || [
|
24
|
+
{
|
25
|
+
url: 'http://localhost:9292',
|
26
|
+
description: 'Development server (Rails)'
|
27
|
+
}
|
28
|
+
]
|
29
|
+
|
30
|
+
generator = RapiTapir::OpenAPI::SchemaGenerator.new(
|
31
|
+
endpoints: endpoints,
|
32
|
+
info: info,
|
33
|
+
servers: servers
|
34
|
+
)
|
35
|
+
|
36
|
+
generator.generate
|
37
|
+
end
|
38
|
+
|
39
|
+
# Generate Swagger UI HTML for Rails
|
40
|
+
def generate_swagger_ui_html(openapi_path, api_info)
|
41
|
+
RapiTapir::Sinatra::SwaggerUIGenerator.new(openapi_path, api_info).generate
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create documentation routes for a Rails controller
|
45
|
+
def add_documentation_routes(controller_class, config = {})
|
46
|
+
docs_path = config[:docs_path] || '/docs'
|
47
|
+
openapi_path = config[:openapi_path] || '/openapi.json'
|
48
|
+
|
49
|
+
# OpenAPI JSON endpoint
|
50
|
+
get openapi_path, to: proc { |_env|
|
51
|
+
spec = RapiTapir::Server::Rails::DocumentationHelpers.generate_openapi_spec_for_controller(controller_class, config)
|
52
|
+
[
|
53
|
+
200,
|
54
|
+
{ 'Content-Type' => 'application/json' },
|
55
|
+
[JSON.pretty_generate(spec)]
|
56
|
+
]
|
57
|
+
}
|
58
|
+
|
59
|
+
# Swagger UI endpoint
|
60
|
+
get docs_path, to: proc { |_env|
|
61
|
+
api_info = config[:info] || { title: 'API Documentation' }
|
62
|
+
html = RapiTapir::Server::Rails::DocumentationHelpers.generate_swagger_ui_html(openapi_path, api_info)
|
63
|
+
[
|
64
|
+
200,
|
65
|
+
{ 'Content-Type' => 'text/html' },
|
66
|
+
[html]
|
67
|
+
]
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
# Module methods for static access
|
72
|
+
module_function :generate_openapi_spec_for_controller, :generate_swagger_ui_html
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|