rapitapir 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +94 -0
- data/CLEANUP_SUMMARY.md +155 -0
- data/CONTRIBUTING.md +280 -0
- data/LICENSE +21 -0
- data/README.md +485 -0
- data/debug_hash.rb +20 -0
- data/docs/EXTENSION_COMPARISON.md +388 -0
- data/docs/SINATRA_EXTENSION.md +467 -0
- data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
- data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
- data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
- data/docs/archive/PHASE_2_SUMMARY.md +209 -0
- data/docs/archive/REFACTORING_SUMMARY.md +184 -0
- data/docs/archive/phase_1_3_plan.md +136 -0
- data/docs/archive/sinatra_extension_summary.md +188 -0
- data/docs/archive/sinatra_working_solution.md +113 -0
- data/docs/archive/typescript-client-generator-summary.md +259 -0
- data/docs/auto-derivation.md +146 -0
- data/docs/blueprint.md +1091 -0
- data/docs/endpoint-definition.md +211 -0
- data/docs/github_pages_fix.md +52 -0
- data/docs/github_pages_setup.md +49 -0
- data/docs/implementation-status.md +357 -0
- data/docs/observability.md +647 -0
- data/docs/phase3-plan.md +108 -0
- data/docs/sinatra_rapitapir.md +87 -0
- data/docs/type_shortcuts.md +146 -0
- data/examples/README_ENTERPRISE.md +202 -0
- data/examples/authentication_example.rb +192 -0
- data/examples/auto_derivation_ruby_friendly.rb +163 -0
- data/examples/cli/user_api_endpoints.rb +56 -0
- data/examples/client/typescript_client_example.rb +102 -0
- data/examples/client/user-api-client.ts +193 -0
- data/examples/demo_api.rb +41 -0
- data/examples/docs/documentation_example.rb +112 -0
- data/examples/docs/user-api-docs.html +789 -0
- data/examples/docs/user-api-docs.md +403 -0
- data/examples/enhanced_auto_derivation_test.rb +83 -0
- data/examples/enterprise_extension_demo.rb +417 -0
- data/examples/enterprise_rapitapir_api.rb +662 -0
- data/examples/getting_started_extension.rb +218 -0
- data/examples/hello_world.rb +74 -0
- data/examples/oauth2/.env.example +19 -0
- data/examples/oauth2/README.md +205 -0
- data/examples/oauth2/generic_oauth2_api.rb +226 -0
- data/examples/oauth2/get_token.rb +72 -0
- data/examples/oauth2/songs_api_with_auth0.rb +320 -0
- data/examples/oauth2/test_api.sh +16 -0
- data/examples/oauth2/test_songs_api.sh +110 -0
- data/examples/observability/.env.example +35 -0
- data/examples/observability/README.md +230 -0
- data/examples/observability/README_HONEYCOMB.md +332 -0
- data/examples/observability/advanced_setup.rb +384 -0
- data/examples/observability/basic_setup.rb +192 -0
- data/examples/observability/complete_test.rb +121 -0
- data/examples/observability/honeycomb_example.rb +523 -0
- data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
- data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
- data/examples/observability/honeycomb_working_example.rb +489 -0
- data/examples/observability/quick_test.rb +78 -0
- data/examples/observability/simple_test.rb +14 -0
- data/examples/observability/test_honeycomb_demo.rb +354 -0
- data/examples/observability/test_live_honeycomb.rb +111 -0
- data/examples/observability/test_validation.rb +78 -0
- data/examples/observability/test_working_validation.rb +66 -0
- data/examples/openapi/user_api_schema.rb +132 -0
- data/examples/production_ready_example.rb +105 -0
- data/examples/rails/users_controller.rb +146 -0
- data/examples/readme/basic_sinatra_example.rb +128 -0
- data/examples/server/user_api.rb +179 -0
- data/examples/simple_auto_derivation_demo.rb +44 -0
- data/examples/simple_demo_api.rb +18 -0
- data/examples/sinatra/user_app.rb +127 -0
- data/examples/t_shortcut_demo.rb +59 -0
- data/examples/user_api.rb +190 -0
- data/examples/working_getting_started.rb +184 -0
- data/examples/working_simple_example.rb +195 -0
- data/lib/rapitapir/auth/configuration.rb +129 -0
- data/lib/rapitapir/auth/context.rb +122 -0
- data/lib/rapitapir/auth/errors.rb +104 -0
- data/lib/rapitapir/auth/middleware.rb +324 -0
- data/lib/rapitapir/auth/oauth2.rb +350 -0
- data/lib/rapitapir/auth/schemes.rb +420 -0
- data/lib/rapitapir/auth.rb +113 -0
- data/lib/rapitapir/cli/command.rb +535 -0
- data/lib/rapitapir/cli/server.rb +243 -0
- data/lib/rapitapir/cli/validator.rb +373 -0
- data/lib/rapitapir/client/generator_base.rb +272 -0
- data/lib/rapitapir/client/typescript_generator.rb +350 -0
- data/lib/rapitapir/core/endpoint.rb +158 -0
- data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
- data/lib/rapitapir/core/input.rb +182 -0
- data/lib/rapitapir/core/output.rb +164 -0
- data/lib/rapitapir/core/request.rb +19 -0
- data/lib/rapitapir/core/response.rb +17 -0
- data/lib/rapitapir/docs/html_generator.rb +780 -0
- data/lib/rapitapir/docs/markdown_generator.rb +464 -0
- data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
- data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
- data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
- data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
- data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
- data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
- data/lib/rapitapir/dsl/http_verbs.rb +77 -0
- data/lib/rapitapir/dsl/input_methods.rb +47 -0
- data/lib/rapitapir/dsl/observability_methods.rb +81 -0
- data/lib/rapitapir/dsl/output_methods.rb +43 -0
- data/lib/rapitapir/dsl/type_resolution.rb +43 -0
- data/lib/rapitapir/observability/configuration.rb +108 -0
- data/lib/rapitapir/observability/health_check.rb +236 -0
- data/lib/rapitapir/observability/logging.rb +270 -0
- data/lib/rapitapir/observability/metrics.rb +203 -0
- data/lib/rapitapir/observability/middleware.rb +243 -0
- data/lib/rapitapir/observability/tracing.rb +143 -0
- data/lib/rapitapir/observability.rb +28 -0
- data/lib/rapitapir/openapi/schema_generator.rb +403 -0
- data/lib/rapitapir/schema.rb +136 -0
- data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
- data/lib/rapitapir/server/middleware.rb +120 -0
- data/lib/rapitapir/server/path_matcher.rb +45 -0
- data/lib/rapitapir/server/rack_adapter.rb +215 -0
- data/lib/rapitapir/server/rails_adapter.rb +17 -0
- data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
- data/lib/rapitapir/server/rails_controller.rb +72 -0
- data/lib/rapitapir/server/rails_input_processor.rb +73 -0
- data/lib/rapitapir/server/rails_response_handler.rb +29 -0
- data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
- data/lib/rapitapir/server/sinatra_integration.rb +93 -0
- data/lib/rapitapir/sinatra/configuration.rb +91 -0
- data/lib/rapitapir/sinatra/extension.rb +214 -0
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
- data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
- data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
- data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
- data/lib/rapitapir/types/array.rb +163 -0
- data/lib/rapitapir/types/auto_derivation.rb +265 -0
- data/lib/rapitapir/types/base.rb +146 -0
- data/lib/rapitapir/types/boolean.rb +46 -0
- data/lib/rapitapir/types/date.rb +92 -0
- data/lib/rapitapir/types/datetime.rb +98 -0
- data/lib/rapitapir/types/email.rb +32 -0
- data/lib/rapitapir/types/float.rb +134 -0
- data/lib/rapitapir/types/hash.rb +161 -0
- data/lib/rapitapir/types/integer.rb +143 -0
- data/lib/rapitapir/types/object.rb +156 -0
- data/lib/rapitapir/types/optional.rb +65 -0
- data/lib/rapitapir/types/string.rb +185 -0
- data/lib/rapitapir/types/uuid.rb +32 -0
- data/lib/rapitapir/types.rb +155 -0
- data/lib/rapitapir/version.rb +5 -0
- data/lib/rapitapir.rb +173 -0
- data/rapitapir.gemspec +66 -0
- metadata +387 -0
@@ -0,0 +1,215 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'json'
|
5
|
+
require_relative '../core/endpoint'
|
6
|
+
require_relative 'path_matcher'
|
7
|
+
|
8
|
+
module RapiTapir
|
9
|
+
module Server
|
10
|
+
# Rack adapter for RapiTapir endpoints
|
11
|
+
# Integrates RapiTapir with Rack-based applications and frameworks
|
12
|
+
class RackAdapter
|
13
|
+
attr_reader :endpoints, :middleware_stack
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@endpoints = []
|
17
|
+
@middleware_stack = []
|
18
|
+
end
|
19
|
+
|
20
|
+
# Register an endpoint with the adapter
|
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
|
25
|
+
raise ArgumentError, 'Handler must respond to call' unless handler.respond_to?(:call)
|
26
|
+
|
27
|
+
endpoint.validate!
|
28
|
+
path_matcher = PathMatcher.new(endpoint.path)
|
29
|
+
@endpoints << { endpoint: endpoint, handler: handler, path_matcher: path_matcher }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add middleware to the processing stack
|
33
|
+
def use(middleware_class, *args, &block)
|
34
|
+
@middleware_stack << [middleware_class, args, block]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Main Rack application call method
|
38
|
+
def call(env)
|
39
|
+
app = build_app
|
40
|
+
app.call(env)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Build the complete middleware stack
|
44
|
+
def build_app
|
45
|
+
app = method(:handle_request)
|
46
|
+
|
47
|
+
@middleware_stack.reverse.each do |middleware_class, args, block|
|
48
|
+
app = middleware_class.new(app, *args, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
app
|
52
|
+
end
|
53
|
+
|
54
|
+
# Core request handler (without middleware)
|
55
|
+
def handle_request(env)
|
56
|
+
request = Rack::Request.new(env)
|
57
|
+
|
58
|
+
# Find matching endpoint
|
59
|
+
endpoint_match = find_matching_endpoint(request)
|
60
|
+
return not_found_response unless endpoint_match
|
61
|
+
|
62
|
+
begin
|
63
|
+
# Process the request through the endpoint
|
64
|
+
process_request(request, endpoint_match)
|
65
|
+
rescue StandardError => e
|
66
|
+
error_response(e)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def find_matching_endpoint(request)
|
73
|
+
@endpoints.find do |endpoint_data|
|
74
|
+
endpoint = endpoint_data[:endpoint]
|
75
|
+
path_matcher = endpoint_data[:path_matcher]
|
76
|
+
matches_method?(endpoint, request) && path_matcher.matches?(request.path_info)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def matches_method?(endpoint, request)
|
81
|
+
endpoint.method.to_s.upcase == request.request_method
|
82
|
+
end
|
83
|
+
|
84
|
+
def process_request(request, endpoint_match)
|
85
|
+
endpoint = endpoint_match[:endpoint]
|
86
|
+
handler = endpoint_match[:handler]
|
87
|
+
path_matcher = endpoint_match[:path_matcher]
|
88
|
+
|
89
|
+
# Extract path parameters
|
90
|
+
path_params = path_matcher.match(request.path_info) || {}
|
91
|
+
|
92
|
+
# Extract and validate inputs
|
93
|
+
processed_inputs = extract_inputs(request, endpoint, path_params)
|
94
|
+
|
95
|
+
# Call the handler with processed inputs
|
96
|
+
result = handler.call(processed_inputs)
|
97
|
+
|
98
|
+
# Serialize and return response
|
99
|
+
serialize_response(result, endpoint)
|
100
|
+
end
|
101
|
+
|
102
|
+
def extract_inputs(request, endpoint, path_params = {})
|
103
|
+
inputs = {}
|
104
|
+
|
105
|
+
endpoint.inputs.each do |input|
|
106
|
+
value = extract_input_value(request, input, path_params)
|
107
|
+
process_input_value(inputs, input, value)
|
108
|
+
end
|
109
|
+
|
110
|
+
inputs
|
111
|
+
end
|
112
|
+
|
113
|
+
def extract_input_value(request, input, path_params)
|
114
|
+
case input.kind
|
115
|
+
when :query
|
116
|
+
request.params[input.name.to_s]
|
117
|
+
when :header
|
118
|
+
request.get_header("HTTP_#{input.name.to_s.upcase}")
|
119
|
+
when :path
|
120
|
+
path_params[input.name]
|
121
|
+
when :body
|
122
|
+
parse_body(request, input)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def process_input_value(inputs, input, value)
|
127
|
+
validate_required_input(input, value)
|
128
|
+
coerce_and_store_input(inputs, input, value)
|
129
|
+
end
|
130
|
+
|
131
|
+
def validate_required_input(input, value)
|
132
|
+
raise ArgumentError, "Required input '#{input.name}' is missing" if value.nil? && input.required?
|
133
|
+
end
|
134
|
+
|
135
|
+
def coerce_and_store_input(inputs, input, value)
|
136
|
+
if value.nil?
|
137
|
+
inputs[input.name] = nil unless input.required?
|
138
|
+
else
|
139
|
+
inputs[input.name] = input.coerce(value)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def parse_body(request, input)
|
144
|
+
body = request.body.read
|
145
|
+
request.body.rewind
|
146
|
+
|
147
|
+
return nil if body.empty?
|
148
|
+
|
149
|
+
if input.type == Hash || input.type.is_a?(Hash)
|
150
|
+
JSON.parse(body)
|
151
|
+
else
|
152
|
+
body
|
153
|
+
end
|
154
|
+
rescue JSON::ParserError
|
155
|
+
raise ArgumentError, 'Invalid JSON in request body'
|
156
|
+
end
|
157
|
+
|
158
|
+
def serialize_response(result, endpoint)
|
159
|
+
# Find the appropriate output definition
|
160
|
+
output = endpoint.outputs.find { |o| o.kind == :json } || endpoint.outputs.first
|
161
|
+
|
162
|
+
if output
|
163
|
+
serialized = output.serialize(result)
|
164
|
+
content_type = determine_content_type(output)
|
165
|
+
status_code = determine_status_code(endpoint)
|
166
|
+
|
167
|
+
[status_code, { 'Content-Type' => content_type }, [serialized]]
|
168
|
+
else
|
169
|
+
# Default response if no output defined
|
170
|
+
[200, { 'Content-Type' => 'text/plain' }, [result.to_s]]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def determine_content_type(output)
|
175
|
+
# For enhanced outputs, use the content_type attribute directly
|
176
|
+
return output.content_type if output.respond_to?(:content_type) && output.content_type
|
177
|
+
|
178
|
+
# Fallback to kind-based detection for legacy outputs
|
179
|
+
case output.kind
|
180
|
+
when :json
|
181
|
+
'application/json'
|
182
|
+
when :xml
|
183
|
+
'application/xml'
|
184
|
+
else
|
185
|
+
'text/plain'
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def determine_status_code(endpoint)
|
190
|
+
status_output = endpoint.outputs.find { |o| o.kind == :status }
|
191
|
+
status_output ? status_output.type : 200
|
192
|
+
end
|
193
|
+
|
194
|
+
def not_found_response
|
195
|
+
[404, { 'Content-Type' => 'application/json' }, ['{"error":"Not Found"}']]
|
196
|
+
end
|
197
|
+
|
198
|
+
def error_response(error)
|
199
|
+
error_data = {
|
200
|
+
error: error.class.name,
|
201
|
+
message: error.message
|
202
|
+
}
|
203
|
+
|
204
|
+
status_code = case error
|
205
|
+
when ArgumentError
|
206
|
+
400
|
207
|
+
else
|
208
|
+
500
|
209
|
+
end
|
210
|
+
|
211
|
+
[status_code, { 'Content-Type' => 'application/json' }, [JSON.generate(error_data)]]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'rails_controller'
|
4
|
+
require_relative 'rails_adapter_class'
|
5
|
+
|
6
|
+
module RapiTapir
|
7
|
+
module Server
|
8
|
+
# Rails integration module for RapiTapir
|
9
|
+
#
|
10
|
+
# Provides controller concerns and adapters for seamless integration with Rails applications.
|
11
|
+
# Split into separate files for better organization while maintaining compatibility.
|
12
|
+
module Rails
|
13
|
+
# Main module that includes both Controller concern and RailsAdapter class
|
14
|
+
# This maintains backward compatibility while organizing code better
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Server
|
5
|
+
module Rails
|
6
|
+
# Rails adapter for automatic route generation
|
7
|
+
class RailsAdapter
|
8
|
+
attr_reader :endpoints
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@endpoints = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Register an endpoint and generate Rails routes
|
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
|
19
|
+
|
20
|
+
endpoint.validate!
|
21
|
+
|
22
|
+
@endpoints << {
|
23
|
+
endpoint: endpoint,
|
24
|
+
controller: controller_class,
|
25
|
+
action: action_name
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Generate Rails routes for registered endpoints
|
30
|
+
def generate_routes(rails_routes)
|
31
|
+
@endpoints.each do |endpoint_config|
|
32
|
+
endpoint = endpoint_config[:endpoint]
|
33
|
+
controller = endpoint_config[:controller]
|
34
|
+
action = endpoint_config[:action]
|
35
|
+
|
36
|
+
method_name = endpoint.method.to_s.downcase
|
37
|
+
path_pattern = convert_path_to_rails(endpoint.path)
|
38
|
+
controller_name = controller.name.underscore.sub(/_controller$/, '')
|
39
|
+
|
40
|
+
rails_routes.send(method_name, path_pattern, to: "#{controller_name}##{action}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def convert_path_to_rails(path)
|
47
|
+
# Convert "/users/:id" to "/users/:id" (Rails format is the same)
|
48
|
+
path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'rails_input_processor'
|
4
|
+
require_relative 'rails_response_handler'
|
5
|
+
|
6
|
+
module RapiTapir
|
7
|
+
module Server
|
8
|
+
module Rails
|
9
|
+
# Rails controller concern for RapiTapir integration
|
10
|
+
module Controller
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
include InputProcessor
|
13
|
+
include ResponseHandler
|
14
|
+
|
15
|
+
included do
|
16
|
+
class_attribute :rapitapir_endpoints, default: {}
|
17
|
+
end
|
18
|
+
|
19
|
+
class_methods do
|
20
|
+
# Register an endpoint for this controller
|
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
|
25
|
+
|
26
|
+
endpoint.validate!
|
27
|
+
|
28
|
+
self.rapitapir_endpoints = rapitapir_endpoints.merge(
|
29
|
+
action_name.to_sym => {
|
30
|
+
endpoint: endpoint,
|
31
|
+
handler: handler
|
32
|
+
}
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Process a RapiTapir endpoint within a Rails action
|
40
|
+
def process_rapitapir_endpoint(action_name = nil)
|
41
|
+
action_name ||= params[:action]&.to_sym || :index
|
42
|
+
endpoint_config = find_endpoint_config(action_name)
|
43
|
+
|
44
|
+
return render_endpoint_not_configured_error unless endpoint_config
|
45
|
+
|
46
|
+
process_configured_endpoint(endpoint_config)
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_endpoint_config(action_name)
|
50
|
+
self.class.rapitapir_endpoints[action_name.to_sym]
|
51
|
+
end
|
52
|
+
|
53
|
+
def render_endpoint_not_configured_error
|
54
|
+
render json: { error: 'Endpoint not configured' }, status: 500
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_configured_endpoint(endpoint_config)
|
58
|
+
endpoint = endpoint_config[:endpoint]
|
59
|
+
handler = endpoint_config[:handler]
|
60
|
+
|
61
|
+
processed_inputs = extract_rails_inputs(endpoint, request)
|
62
|
+
result = instance_exec(processed_inputs, &handler)
|
63
|
+
render_rapitapir_response(result, endpoint)
|
64
|
+
rescue ArgumentError => e
|
65
|
+
render json: { error: e.message }, status: 400
|
66
|
+
rescue StandardError => e
|
67
|
+
render json: { error: 'Internal Server Error', message: e.message }, status: 500
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Server
|
5
|
+
module Rails
|
6
|
+
# Input processing methods for Rails controller integration
|
7
|
+
module InputProcessor
|
8
|
+
private
|
9
|
+
|
10
|
+
def extract_rails_inputs(endpoint, request)
|
11
|
+
inputs = {}
|
12
|
+
|
13
|
+
endpoint.inputs.each do |input|
|
14
|
+
value = extract_input_value(input, request)
|
15
|
+
validate_required_input(input, value)
|
16
|
+
add_input_to_collection(inputs, input, value)
|
17
|
+
end
|
18
|
+
|
19
|
+
inputs
|
20
|
+
end
|
21
|
+
|
22
|
+
def extract_input_value(input, request)
|
23
|
+
case input.kind
|
24
|
+
when :query
|
25
|
+
extract_query_value(request, input)
|
26
|
+
when :header
|
27
|
+
extract_header_value(request, input)
|
28
|
+
when :path
|
29
|
+
params[input.name.to_s]
|
30
|
+
when :body
|
31
|
+
parse_rails_body(request, input)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def extract_query_value(request, input)
|
36
|
+
request.query_parameters[input.name.to_s] || params[input.name.to_s]
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_header_value(request, input)
|
40
|
+
request.headers[input.name.to_s] || request.headers["HTTP_#{input.name.to_s.upcase}"]
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_required_input(input, value)
|
44
|
+
return unless value.nil? && input.required?
|
45
|
+
|
46
|
+
raise ArgumentError, "Required input '#{input.name}' is missing"
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_input_to_collection(inputs, input, value)
|
50
|
+
return unless value || !input.required?
|
51
|
+
|
52
|
+
inputs[input.name] = input.coerce(value)
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_rails_body(request, input)
|
56
|
+
# Rails typically parses JSON automatically into params
|
57
|
+
if input.type == Hash || input.type.is_a?(Hash)
|
58
|
+
# Try to get from parsed params first
|
59
|
+
request.request_parameters.presence ||
|
60
|
+
# Fall back to parsing raw body
|
61
|
+
begin
|
62
|
+
JSON.parse(request.raw_post)
|
63
|
+
rescue JSON::ParserError
|
64
|
+
raise ArgumentError, 'Invalid JSON in request body'
|
65
|
+
end
|
66
|
+
else
|
67
|
+
request.raw_post
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Server
|
5
|
+
module Rails
|
6
|
+
# Response handling methods for Rails controller integration
|
7
|
+
module ResponseHandler
|
8
|
+
private
|
9
|
+
|
10
|
+
def render_rapitapir_response(result, endpoint)
|
11
|
+
output = endpoint.outputs.find { |o| o.kind == :json } || endpoint.outputs.first
|
12
|
+
status_code = determine_rails_status_code(endpoint)
|
13
|
+
|
14
|
+
if output&.kind == :xml
|
15
|
+
render xml: result, status: status_code
|
16
|
+
else
|
17
|
+
# Default to JSON for nil output, :json kind, or unknown kinds
|
18
|
+
render json: result, status: status_code
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def determine_rails_status_code(endpoint)
|
23
|
+
status_output = endpoint.outputs.find { |o| o.kind == :status }
|
24
|
+
status_output ? status_output.type : 200
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module RapiTapir
|
6
|
+
module Server
|
7
|
+
# Sinatra integration adapter for RapiTapir
|
8
|
+
# Provides seamless integration with Sinatra applications
|
9
|
+
class SinatraAdapter
|
10
|
+
attr_reader :app, :endpoints
|
11
|
+
|
12
|
+
def initialize(sinatra_app)
|
13
|
+
@app = sinatra_app
|
14
|
+
@endpoints = []
|
15
|
+
|
16
|
+
# Store adapter reference in the app for access in route handlers
|
17
|
+
@app.instance_variable_set(:@rapitapir_adapter, self)
|
18
|
+
|
19
|
+
# Define a helper method on the app to access the adapter
|
20
|
+
@app.define_singleton_method(:rapitapir_adapter) do
|
21
|
+
@rapitapir_adapter
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Register an endpoint with automatic Sinatra route creation
|
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
|
30
|
+
|
31
|
+
endpoint.validate!
|
32
|
+
|
33
|
+
# Use provided handler or block
|
34
|
+
endpoint_handler = handler || block
|
35
|
+
raise ArgumentError, 'Handler must be provided' unless endpoint_handler
|
36
|
+
|
37
|
+
endpoint_info = { endpoint: endpoint, handler: endpoint_handler, id: @endpoints.length }
|
38
|
+
@endpoints << endpoint_info
|
39
|
+
|
40
|
+
# Register route with Sinatra
|
41
|
+
register_sinatra_route(endpoint_info)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# rubocop:disable Metrics/AbcSize
|
47
|
+
def register_sinatra_route(endpoint_info)
|
48
|
+
method_name = endpoint_info[:endpoint].method.to_s.downcase.to_sym
|
49
|
+
path_pattern = convert_path_to_sinatra(endpoint_info[:endpoint].path)
|
50
|
+
endpoint_id = endpoint_info[:id]
|
51
|
+
adapter = self
|
52
|
+
|
53
|
+
@app.send(method_name, path_pattern) do
|
54
|
+
result = adapter.handle_sinatra_request(endpoint_id, request, params)
|
55
|
+
if result.is_a?(Array) && result.length == 3
|
56
|
+
status result[0]
|
57
|
+
headers result[1]
|
58
|
+
body result[2]
|
59
|
+
else
|
60
|
+
result
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
# rubocop:enable Metrics/AbcSize
|
65
|
+
|
66
|
+
public
|
67
|
+
|
68
|
+
def handle_sinatra_request(endpoint_id, request, params)
|
69
|
+
endpoint_data = @endpoints[endpoint_id]
|
70
|
+
|
71
|
+
begin
|
72
|
+
processed_inputs = extract_sinatra_inputs(request, params, endpoint_data[:endpoint])
|
73
|
+
result = @app.instance_exec(processed_inputs, &endpoint_data[:handler])
|
74
|
+
send_successful_response(endpoint_data, result)
|
75
|
+
rescue ArgumentError => e
|
76
|
+
error_response(400, e.message)
|
77
|
+
rescue StandardError => e
|
78
|
+
error_response(500, 'Internal Server Error', e.message)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def error_response(status_code, error, message = nil)
|
83
|
+
error_data = { error: error }
|
84
|
+
error_data[:message] = message if message
|
85
|
+
[status_code, { 'Content-Type' => 'application/json' }, [error_data.to_json]]
|
86
|
+
end
|
87
|
+
|
88
|
+
def execute_endpoint_handler(endpoint_data, processed_inputs)
|
89
|
+
case endpoint_data[:handler]
|
90
|
+
when Proc
|
91
|
+
instance_exec(processed_inputs, &endpoint_data[:handler])
|
92
|
+
else
|
93
|
+
call_handler_method(endpoint_data[:handler], processed_inputs)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def call_handler_method(handler, processed_inputs)
|
98
|
+
if handler.respond_to?(:call)
|
99
|
+
handler.call(processed_inputs)
|
100
|
+
else
|
101
|
+
instance_exec(processed_inputs, &handler)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def send_successful_response(endpoint_data, result)
|
106
|
+
endpoint = endpoint_data[:endpoint]
|
107
|
+
|
108
|
+
# Find status code from outputs
|
109
|
+
status_output = endpoint.outputs.find { |o| o.kind == :status }
|
110
|
+
status_code = status_output ? status_output.type : 200
|
111
|
+
|
112
|
+
# Default headers
|
113
|
+
headers = { 'Content-Type' => 'application/json' }
|
114
|
+
|
115
|
+
# Add any additional headers from outputs if they exist
|
116
|
+
# Note: This is for future compatibility when header outputs are supported
|
117
|
+
|
118
|
+
[status_code, headers, [result.to_json]]
|
119
|
+
end
|
120
|
+
|
121
|
+
def convert_path_to_sinatra(path)
|
122
|
+
# Convert "/users/:id" to "/users/:id" (Sinatra format is the same)
|
123
|
+
path
|
124
|
+
end
|
125
|
+
|
126
|
+
def extract_sinatra_inputs(request, params, endpoint)
|
127
|
+
inputs = {}
|
128
|
+
|
129
|
+
endpoint.inputs.each do |input|
|
130
|
+
value = extract_input_value(request, params, input)
|
131
|
+
validate_and_add_input(inputs, input, value)
|
132
|
+
end
|
133
|
+
|
134
|
+
inputs
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def extract_input_value(request, params, input)
|
140
|
+
case input.kind
|
141
|
+
when :query, :path
|
142
|
+
# Both query and path parameters are available in params
|
143
|
+
params[input.name.to_s] || params[input.name.to_sym]
|
144
|
+
when :header
|
145
|
+
request.env["HTTP_#{input.name.to_s.upcase}"]
|
146
|
+
when :body
|
147
|
+
parse_sinatra_body(request, input)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def validate_and_add_input(inputs, input, value)
|
152
|
+
# Validate and coerce the value
|
153
|
+
raise ArgumentError, "Required input '#{input.name}' is missing" if value.nil? && input.required?
|
154
|
+
|
155
|
+
inputs[input.name] = input.coerce(value) if value || !input.required?
|
156
|
+
end
|
157
|
+
|
158
|
+
def parse_sinatra_body(request, input)
|
159
|
+
body = request.body.read
|
160
|
+
request.body.rewind
|
161
|
+
|
162
|
+
return nil if body.empty?
|
163
|
+
|
164
|
+
if input.type == Hash || input.type.is_a?(Hash)
|
165
|
+
JSON.parse(body)
|
166
|
+
else
|
167
|
+
body
|
168
|
+
end
|
169
|
+
rescue JSON::ParserError
|
170
|
+
raise ArgumentError, 'Invalid JSON in request body'
|
171
|
+
end
|
172
|
+
|
173
|
+
def serialize_response(result, endpoint)
|
174
|
+
output = endpoint.outputs.find { |o| o.kind == :json } || endpoint.outputs.first
|
175
|
+
|
176
|
+
if output
|
177
|
+
output.serialize(result)
|
178
|
+
else
|
179
|
+
result.to_json
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def determine_content_type(endpoint)
|
184
|
+
output = endpoint.outputs.find { |o| o.kind == :json } || endpoint.outputs.first
|
185
|
+
|
186
|
+
case output&.kind
|
187
|
+
when :xml
|
188
|
+
'application/xml'
|
189
|
+
else # Default to JSON for :json and unknown formats
|
190
|
+
'application/json'
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def determine_status_code(endpoint)
|
195
|
+
status_output = endpoint.outputs.find { |o| o.kind == :status }
|
196
|
+
status_output ? status_output.type : 200
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|