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,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Server
|
5
|
+
module Rails
|
6
|
+
# Resource builder for creating RESTful API endpoints in Rails controllers
|
7
|
+
# Provides the same functionality as Sinatra::ResourceBuilder but adapted for Rails
|
8
|
+
class ResourceBuilder
|
9
|
+
attr_reader :endpoints
|
10
|
+
|
11
|
+
def initialize(controller_class, base_path, schema, **options)
|
12
|
+
@controller_class = controller_class
|
13
|
+
@base_path = base_path.chomp('/')
|
14
|
+
@schema = schema
|
15
|
+
@options = options
|
16
|
+
@endpoints = []
|
17
|
+
end
|
18
|
+
|
19
|
+
# Enable standard CRUD operations
|
20
|
+
# @param except [Array<Symbol>] Operations to exclude
|
21
|
+
# @param only [Array<Symbol>] Only include these operations
|
22
|
+
# @param block [Proc] Block defining the CRUD handlers
|
23
|
+
def crud(except: [], only: nil, **handlers, &block)
|
24
|
+
operations = only || %i[index show create update destroy]
|
25
|
+
operations -= except if except.any?
|
26
|
+
|
27
|
+
# If a block is given, evaluate it in the context of this ResourceBuilder
|
28
|
+
if block_given?
|
29
|
+
instance_eval(&block)
|
30
|
+
else
|
31
|
+
# Legacy style with handlers hash
|
32
|
+
operations.each do |operation|
|
33
|
+
send(operation, &handlers[operation]) if respond_to?(operation, true)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# List all resources (GET /resources)
|
39
|
+
# @param options [Hash] Endpoint options
|
40
|
+
# @param handler [Proc] Request handler
|
41
|
+
def index(**options, &handler)
|
42
|
+
handler ||= proc { [] }
|
43
|
+
|
44
|
+
endpoint_def = @controller_class.GET(@base_path)
|
45
|
+
.summary(options[:summary] || "List all #{resource_name}")
|
46
|
+
.description(options[:description] || "Retrieve a list of #{resource_name}")
|
47
|
+
.then { |ep| add_pagination_params(ep) }
|
48
|
+
.ok(RapiTapir::Types.array(@schema))
|
49
|
+
.build
|
50
|
+
|
51
|
+
@endpoints << [endpoint_def, handler]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get specific resource (GET /resources/:id)
|
55
|
+
# @param options [Hash] Endpoint options
|
56
|
+
# @param handler [Proc] Request handler
|
57
|
+
def show(**options, &handler)
|
58
|
+
handler ||= proc { {} }
|
59
|
+
|
60
|
+
endpoint_def = @controller_class.GET("#{@base_path}/:id")
|
61
|
+
.summary(options[:summary] || "Get #{resource_name}")
|
62
|
+
.description(options[:description] || "Retrieve a specific #{resource_name} by ID")
|
63
|
+
.path_param(:id, :integer, description: "#{resource_name.capitalize} ID")
|
64
|
+
.ok(@schema)
|
65
|
+
.not_found(error_schema, description: "#{resource_name.capitalize} not found")
|
66
|
+
.build
|
67
|
+
|
68
|
+
@endpoints << [endpoint_def, handler]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Create new resource (POST /resources)
|
72
|
+
# @param options [Hash] Endpoint options
|
73
|
+
# @param handler [Proc] Request handler
|
74
|
+
def create(**options, &handler)
|
75
|
+
handler ||= proc { {} }
|
76
|
+
|
77
|
+
endpoint_def = @controller_class.POST(@base_path)
|
78
|
+
.summary(options[:summary] || "Create #{resource_name}")
|
79
|
+
.description(options[:description] || "Create a new #{resource_name}")
|
80
|
+
.json_body(@schema)
|
81
|
+
.created(@schema)
|
82
|
+
.bad_request(validation_error_schema, description: 'Validation error')
|
83
|
+
.build
|
84
|
+
|
85
|
+
@endpoints << [endpoint_def, handler]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Update resource (PUT /resources/:id)
|
89
|
+
# @param options [Hash] Endpoint options
|
90
|
+
# @param handler [Proc] Request handler
|
91
|
+
def update(**options, &handler)
|
92
|
+
handler ||= proc { {} }
|
93
|
+
|
94
|
+
endpoint_def = @controller_class.PUT("#{@base_path}/:id")
|
95
|
+
.summary(options[:summary] || "Update #{resource_name}")
|
96
|
+
.description(options[:description] || "Update an existing #{resource_name}")
|
97
|
+
.path_param(:id, RapiTapir::Types.integer, description: "#{resource_name.capitalize} ID")
|
98
|
+
.json_body(@schema)
|
99
|
+
.ok(@schema)
|
100
|
+
.not_found(error_schema, description: "#{resource_name.capitalize} not found")
|
101
|
+
.bad_request(validation_error_schema, description: 'Validation error')
|
102
|
+
.build
|
103
|
+
|
104
|
+
@endpoints << [endpoint_def, handler]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Delete resource (DELETE /resources/:id)
|
108
|
+
# @param options [Hash] Endpoint options
|
109
|
+
# @param handler [Proc] Request handler
|
110
|
+
def destroy(**options, &handler)
|
111
|
+
handler ||= proc { head :no_content }
|
112
|
+
|
113
|
+
endpoint_def = @controller_class.DELETE("#{@base_path}/:id")
|
114
|
+
.summary(options[:summary] || "Delete #{resource_name}")
|
115
|
+
.description(options[:description] || "Delete a #{resource_name}")
|
116
|
+
.path_param(:id, :integer, description: "#{resource_name.capitalize} ID")
|
117
|
+
.no_content
|
118
|
+
.not_found(error_schema, description: "#{resource_name.capitalize} not found")
|
119
|
+
.build
|
120
|
+
|
121
|
+
@endpoints << [endpoint_def, handler]
|
122
|
+
end
|
123
|
+
|
124
|
+
# Add custom endpoint to the resource
|
125
|
+
# @param method [Symbol] HTTP method
|
126
|
+
# @param path_suffix [String] Path suffix to append to base path
|
127
|
+
# @param options [Hash] Endpoint options
|
128
|
+
# @param handler [Proc] Request handler
|
129
|
+
def custom(method, path_suffix, **options, &handler)
|
130
|
+
full_path = path_suffix.start_with?('/') ? path_suffix : "#{@base_path}/#{path_suffix}"
|
131
|
+
|
132
|
+
endpoint_def = @controller_class.public_send(method.to_s.upcase, full_path)
|
133
|
+
.summary(options[:summary] || "Custom #{method.upcase} #{path_suffix}")
|
134
|
+
.description(options[:description] || "Custom endpoint for #{resource_name}")
|
135
|
+
|
136
|
+
# Apply any custom configuration
|
137
|
+
endpoint_def = options[:configure].call(endpoint_def) if options[:configure]
|
138
|
+
|
139
|
+
endpoint_def = endpoint_def.build
|
140
|
+
|
141
|
+
@endpoints << [endpoint_def, handler]
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# Add pagination query parameters to an endpoint
|
147
|
+
# @param endpoint [RapiTapir::DSL::FluentEndpointBuilder] The endpoint builder
|
148
|
+
# @return [RapiTapir::DSL::FluentEndpointBuilder] The modified endpoint builder
|
149
|
+
def add_pagination_params(endpoint)
|
150
|
+
endpoint.query(:limit, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 1, maximum: 100)),
|
151
|
+
description: 'Maximum number of results')
|
152
|
+
.query(:offset, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 0)),
|
153
|
+
description: 'Number of results to skip')
|
154
|
+
end
|
155
|
+
|
156
|
+
# Get the singular resource name from the base path
|
157
|
+
# @return [String] The singular resource name
|
158
|
+
def resource_name
|
159
|
+
@resource_name ||= @base_path.split('/').last.chomp('s')
|
160
|
+
end
|
161
|
+
|
162
|
+
# Standard error schema for 404 responses
|
163
|
+
# @return [RapiTapir::Types::Hash] Error schema
|
164
|
+
def error_schema
|
165
|
+
RapiTapir::Types.hash({
|
166
|
+
'error' => RapiTapir::Types.string
|
167
|
+
})
|
168
|
+
end
|
169
|
+
|
170
|
+
# Validation error schema for 400 responses
|
171
|
+
# @return [RapiTapir::Types::Hash] Validation error schema
|
172
|
+
def validation_error_schema
|
173
|
+
RapiTapir::Types.hash({
|
174
|
+
'error' => RapiTapir::Types.string,
|
175
|
+
'details' => RapiTapir::Types.optional(RapiTapir::Types.array(RapiTapir::Types.string))
|
176
|
+
})
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Server
|
5
|
+
module Rails
|
6
|
+
# Rails routes integration module
|
7
|
+
# Provides helpers for automatically generating routes from RapiTapir endpoint definitions
|
8
|
+
module Routes
|
9
|
+
# Generate routes for a RapiTapir Rails controller
|
10
|
+
# @param controller_class [Class] The controller class with RapiTapir endpoints
|
11
|
+
# @example
|
12
|
+
# Rails.application.routes.draw do
|
13
|
+
# rapitapir_routes_for UsersController
|
14
|
+
# end
|
15
|
+
def rapitapir_routes_for(controller_class)
|
16
|
+
raise ArgumentError, "#{controller_class} must include RapiTapir::Server::Rails::Controller" unless controller_class.respond_to?(:rapitapir_endpoints)
|
17
|
+
|
18
|
+
controller_name = controller_class.controller_name
|
19
|
+
|
20
|
+
controller_class.rapitapir_endpoints.each do |action, endpoint_config|
|
21
|
+
endpoint_def = endpoint_config[:endpoint]
|
22
|
+
method = endpoint_def.method.downcase
|
23
|
+
path = convert_rapitapir_path_to_rails(endpoint_def.path)
|
24
|
+
|
25
|
+
# Generate the Rails route
|
26
|
+
public_send(method, path, to: "#{controller_name}##{action}",
|
27
|
+
as: route_name(controller_name, action, method))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Generate all routes for multiple controllers
|
32
|
+
# @param controllers [Array<Class>] Array of controller classes
|
33
|
+
# @example
|
34
|
+
# Rails.application.routes.draw do
|
35
|
+
# rapitapir_routes_for_all [UsersController, BooksController, OrdersController]
|
36
|
+
# end
|
37
|
+
def rapitapir_routes_for_all(controllers)
|
38
|
+
controllers.each do |controller_class|
|
39
|
+
rapitapir_routes_for(controller_class)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Auto-discover and register all RapiTapir controllers in the application
|
44
|
+
# @example
|
45
|
+
# Rails.application.routes.draw do
|
46
|
+
# rapitapir_auto_routes
|
47
|
+
# end
|
48
|
+
def rapitapir_auto_routes
|
49
|
+
# Find all controllers that include RapiTapir
|
50
|
+
controllers = ApplicationController.descendants.select do |klass|
|
51
|
+
klass.included_modules.include?(RapiTapir::Server::Rails::Controller) ||
|
52
|
+
klass < RapiTapir::Server::Rails::ControllerBase
|
53
|
+
end
|
54
|
+
|
55
|
+
rapitapir_routes_for_all(controllers)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Convert RapiTapir path format to Rails route format
|
61
|
+
# @param rapitapir_path [String] Path with RapiTapir parameter syntax
|
62
|
+
# @return [String] Path with Rails parameter syntax
|
63
|
+
# @example
|
64
|
+
# convert_rapitapir_path_to_rails('/users/:id') # => '/users/:id'
|
65
|
+
# convert_rapitapir_path_to_rails('/users/{id}') # => '/users/:id'
|
66
|
+
def convert_rapitapir_path_to_rails(rapitapir_path)
|
67
|
+
# Convert {id} format to :id format for Rails
|
68
|
+
rapitapir_path.gsub(/\{(\w+)\}/, ':\1')
|
69
|
+
end
|
70
|
+
|
71
|
+
# Generate a route name for the endpoint
|
72
|
+
# @param controller_name [String] The controller name
|
73
|
+
# @param action [Symbol] The action name
|
74
|
+
# @param method [String] The HTTP method
|
75
|
+
# @return [Symbol] The route name
|
76
|
+
def route_name(controller_name, action, method)
|
77
|
+
base_name = controller_name.chomp('_controller')
|
78
|
+
|
79
|
+
case action
|
80
|
+
when :index
|
81
|
+
base_name.to_sym
|
82
|
+
when :show
|
83
|
+
singularize_name(base_name).to_sym
|
84
|
+
when :create
|
85
|
+
base_name.to_sym
|
86
|
+
when :update
|
87
|
+
singularize_name(base_name).to_sym
|
88
|
+
when :destroy
|
89
|
+
singularize_name(base_name).to_sym
|
90
|
+
else
|
91
|
+
:"#{method}_#{base_name}_#{action}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Simple singularization for route names
|
96
|
+
# @param name [String] The plural name
|
97
|
+
# @return [String] The singular name
|
98
|
+
def singularize_name(name)
|
99
|
+
# Simple singularization logic
|
100
|
+
if name.end_with?('ies')
|
101
|
+
"#{name.chomp('ies')}y"
|
102
|
+
elsif name.end_with?('s')
|
103
|
+
name.chomp('s')
|
104
|
+
else
|
105
|
+
name
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# NOTE: Rails routes extension is handled in the rails_integration.rb file
|
114
|
+
# to ensure proper timing with Rails application initialization
|
@@ -3,15 +3,22 @@
|
|
3
3
|
require_relative 'rails_controller'
|
4
4
|
require_relative 'rails_adapter_class'
|
5
5
|
|
6
|
+
# Load enhanced Rails integration components
|
7
|
+
require_relative 'rails/controller_base'
|
8
|
+
require_relative 'rails/configuration'
|
9
|
+
require_relative 'rails/routes'
|
10
|
+
require_relative 'rails/resource_builder'
|
11
|
+
require_relative 'rails/documentation_helpers'
|
12
|
+
|
6
13
|
module RapiTapir
|
7
14
|
module Server
|
8
15
|
# Rails integration module for RapiTapir
|
9
16
|
#
|
10
17
|
# Provides controller concerns and adapters for seamless integration with Rails applications.
|
11
|
-
#
|
18
|
+
# Includes the enhanced Rails integration with ControllerBase and auto-routing.
|
12
19
|
module Rails
|
13
|
-
# Main module that includes both Controller concern and
|
14
|
-
# This maintains backward compatibility while
|
20
|
+
# Main module that includes both legacy Controller concern and enhanced ControllerBase
|
21
|
+
# This maintains backward compatibility while providing the new enhanced features
|
15
22
|
end
|
16
23
|
end
|
17
24
|
end
|
@@ -13,9 +13,7 @@ module RapiTapir
|
|
13
13
|
|
14
14
|
# Register an endpoint and generate Rails routes
|
15
15
|
def register_endpoint(endpoint, controller_class, action_name)
|
16
|
-
unless endpoint.is_a?(RapiTapir::Core::Endpoint)
|
17
|
-
raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint'
|
18
|
-
end
|
16
|
+
raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint' unless endpoint.is_a?(RapiTapir::Core::Endpoint)
|
19
17
|
|
20
18
|
endpoint.validate!
|
21
19
|
|
@@ -19,9 +19,7 @@ module RapiTapir
|
|
19
19
|
class_methods do
|
20
20
|
# Register an endpoint for this controller
|
21
21
|
def rapitapir_endpoint(action_name, 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
|
|
26
24
|
endpoint.validate!
|
27
25
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Rails integration loader - ensures proper module initialization
|
4
|
+
# Only loads if Rails and ActiveSupport are available
|
5
|
+
|
6
|
+
# First, ensure the module structure exists
|
7
|
+
module RapiTapir
|
8
|
+
module Server
|
9
|
+
module Rails
|
10
|
+
# Rails integration module for RapiTapir
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Define a method to load Rails integration when Rails becomes available
|
15
|
+
def self.load_rails_integration!
|
16
|
+
return if @rails_integration_loaded
|
17
|
+
return unless defined?(Rails) && defined?(ActiveSupport)
|
18
|
+
|
19
|
+
# Remove fallback ControllerBase if it exists
|
20
|
+
RapiTapir::Server::Rails.send(:remove_const, :ControllerBase) if RapiTapir::Server::Rails.const_defined?(:ControllerBase)
|
21
|
+
|
22
|
+
# Load Rails integration components in the correct order
|
23
|
+
require_relative 'rails_controller'
|
24
|
+
require_relative 'rails_adapter_class'
|
25
|
+
require_relative 'rails/configuration'
|
26
|
+
require_relative 'rails/routes'
|
27
|
+
require_relative 'rails/resource_builder'
|
28
|
+
require_relative 'rails/documentation_helpers'
|
29
|
+
require_relative 'rails/controller_base'
|
30
|
+
|
31
|
+
# Create Railtie for Rails integration
|
32
|
+
if defined?(Rails::Railtie) && !Object.const_defined?(:RapiTapirRailtie)
|
33
|
+
Object.const_set(:RapiTapirRailtie, Class.new(Rails::Railtie) do
|
34
|
+
initializer 'rapitapir.extend_routes', after: :initialize_routes do |app|
|
35
|
+
app.routes&.extend(RapiTapir::Server::Rails::Routes)
|
36
|
+
end
|
37
|
+
end)
|
38
|
+
end
|
39
|
+
|
40
|
+
@rails_integration_loaded = true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Load immediately if Rails is already available
|
45
|
+
if defined?(Rails) && defined?(ActiveSupport)
|
46
|
+
RapiTapir.load_rails_integration!
|
47
|
+
else
|
48
|
+
# Rails not available - provide minimal fallback
|
49
|
+
module RapiTapir
|
50
|
+
module Server
|
51
|
+
module Rails
|
52
|
+
class ControllerBase
|
53
|
+
def self.method_missing(method, ...)
|
54
|
+
# Try to load Rails integration when accessed
|
55
|
+
RapiTapir.load_rails_integration!
|
56
|
+
if defined?(RapiTapir::Server::Rails::ControllerBase) && self.class != RapiTapir::Server::Rails::ControllerBase
|
57
|
+
return RapiTapir::Server::Rails::ControllerBase.send(method, ...)
|
58
|
+
end
|
59
|
+
|
60
|
+
raise NameError,
|
61
|
+
'Rails integration not available. Please ensure Rails is loaded before requiring RapiTapir.'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -8,14 +8,27 @@ module RapiTapir
|
|
8
8
|
private
|
9
9
|
|
10
10
|
def render_rapitapir_response(result, endpoint)
|
11
|
+
# Extract custom status code if provided
|
12
|
+
status_code = 200
|
13
|
+
response_data = result
|
14
|
+
|
15
|
+
if result.is_a?(Hash) && result.key?(:_status)
|
16
|
+
status_code = result[:_status]
|
17
|
+
response_data = result.except(:_status)
|
18
|
+
elsif result.is_a?(Hash) && result.key?('_status')
|
19
|
+
status_code = result['_status']
|
20
|
+
response_data = result.except('_status')
|
21
|
+
else
|
22
|
+
status_code = determine_rails_status_code(endpoint)
|
23
|
+
end
|
24
|
+
|
11
25
|
output = endpoint.outputs.find { |o| o.kind == :json } || endpoint.outputs.first
|
12
|
-
status_code = determine_rails_status_code(endpoint)
|
13
26
|
|
14
27
|
if output&.kind == :xml
|
15
|
-
render xml:
|
28
|
+
render xml: response_data, status: status_code
|
16
29
|
else
|
17
30
|
# Default to JSON for nil output, :json kind, or unknown kinds
|
18
|
-
render json:
|
31
|
+
render json: response_data, status: status_code
|
19
32
|
end
|
20
33
|
end
|
21
34
|
|
@@ -24,9 +24,7 @@ module RapiTapir
|
|
24
24
|
|
25
25
|
# Register an endpoint with automatic Sinatra route creation
|
26
26
|
def register_endpoint(endpoint, handler = nil, &block)
|
27
|
-
unless endpoint.is_a?(RapiTapir::Core::Endpoint)
|
28
|
-
raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint'
|
29
|
-
end
|
27
|
+
raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint' unless endpoint.is_a?(RapiTapir::Core::Endpoint)
|
30
28
|
|
31
29
|
endpoint.validate!
|
32
30
|
|
@@ -43,7 +41,6 @@ module RapiTapir
|
|
43
41
|
|
44
42
|
private
|
45
43
|
|
46
|
-
# rubocop:disable Metrics/AbcSize
|
47
44
|
def register_sinatra_route(endpoint_info)
|
48
45
|
method_name = endpoint_info[:endpoint].method.to_s.downcase.to_sym
|
49
46
|
path_pattern = convert_path_to_sinatra(endpoint_info[:endpoint].path)
|
@@ -61,7 +58,6 @@ module RapiTapir
|
|
61
58
|
end
|
62
59
|
end
|
63
60
|
end
|
64
|
-
# rubocop:enable Metrics/AbcSize
|
65
61
|
|
66
62
|
public
|
67
63
|
|
@@ -72,6 +68,18 @@ module RapiTapir
|
|
72
68
|
processed_inputs = extract_sinatra_inputs(request, params, endpoint_data[:endpoint])
|
73
69
|
result = @app.instance_exec(processed_inputs, &endpoint_data[:handler])
|
74
70
|
send_successful_response(endpoint_data, result)
|
71
|
+
rescue RapiTapir::Types::CoercionError => e
|
72
|
+
detailed_error_response(400, 'Validation Error', e.reason, {
|
73
|
+
field: extract_field_from_error(e),
|
74
|
+
value: e.value,
|
75
|
+
expected_type: e.type
|
76
|
+
})
|
77
|
+
rescue RapiTapir::Types::ValidationError => e
|
78
|
+
detailed_error_response(400, 'Validation Error', e.message, {
|
79
|
+
errors: e.errors,
|
80
|
+
value: e.value,
|
81
|
+
expected_type: e.type.to_s
|
82
|
+
})
|
75
83
|
rescue ArgumentError => e
|
76
84
|
error_response(400, e.message)
|
77
85
|
rescue StandardError => e
|
@@ -85,6 +93,22 @@ module RapiTapir
|
|
85
93
|
[status_code, { 'Content-Type' => 'application/json' }, [error_data.to_json]]
|
86
94
|
end
|
87
95
|
|
96
|
+
def detailed_error_response(status_code, error, message, details = {})
|
97
|
+
error_data = {
|
98
|
+
error: error,
|
99
|
+
message: message
|
100
|
+
}
|
101
|
+
error_data.merge!(details) if details.any?
|
102
|
+
[status_code, { 'Content-Type' => 'application/json' }, [error_data.to_json]]
|
103
|
+
end
|
104
|
+
|
105
|
+
def extract_field_from_error(error)
|
106
|
+
# Try to extract field name from error message
|
107
|
+
# Match both "Field 'field_name':" and "Required field 'field_name'"
|
108
|
+
match = error.reason.match(/(?:Field|Required field) '([^']+)'/)
|
109
|
+
match[1] if match
|
110
|
+
end
|
111
|
+
|
88
112
|
def execute_endpoint_handler(endpoint_data, processed_inputs)
|
89
113
|
case endpoint_data[:handler]
|
90
114
|
when Proc
|
@@ -23,8 +23,8 @@ module RapiTapir
|
|
23
23
|
end
|
24
24
|
|
25
25
|
# Mount an endpoint in Sinatra
|
26
|
-
def mount_endpoint(endpoint, &
|
27
|
-
rapitapir_adapter.mount(endpoint, &
|
26
|
+
def mount_endpoint(endpoint, &)
|
27
|
+
rapitapir_adapter.mount(endpoint, &)
|
28
28
|
|
29
29
|
# Register the route with Sinatra
|
30
30
|
method_name = endpoint.method.to_s.downcase
|
@@ -42,8 +42,8 @@ module RapiTapir
|
|
42
42
|
end
|
43
43
|
|
44
44
|
# Register error handlers
|
45
|
-
def rapitapir_error(error_class, &
|
46
|
-
rapitapir_adapter.on_error(error_class, &
|
45
|
+
def rapitapir_error(error_class, &)
|
46
|
+
rapitapir_adapter.on_error(error_class, &)
|
47
47
|
end
|
48
48
|
|
49
49
|
# Generate OpenAPI spec for all mounted endpoints
|
@@ -29,9 +29,9 @@ module RapiTapir
|
|
29
29
|
# Class methods added to the Sinatra application
|
30
30
|
module ClassMethods
|
31
31
|
# Configure RapiTapir integration
|
32
|
-
def rapitapir(&
|
32
|
+
def rapitapir(&)
|
33
33
|
config = settings.rapitapir_config
|
34
|
-
config.instance_eval(&
|
34
|
+
config.instance_eval(&) if block_given?
|
35
35
|
setup_rapitapir_integration
|
36
36
|
end
|
37
37
|
|