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.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +57 -0
  4. data/CHANGELOG.md +94 -0
  5. data/CLEANUP_SUMMARY.md +155 -0
  6. data/CONTRIBUTING.md +280 -0
  7. data/LICENSE +21 -0
  8. data/README.md +485 -0
  9. data/debug_hash.rb +20 -0
  10. data/docs/EXTENSION_COMPARISON.md +388 -0
  11. data/docs/SINATRA_EXTENSION.md +467 -0
  12. data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
  13. data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
  14. data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
  15. data/docs/archive/PHASE_2_SUMMARY.md +209 -0
  16. data/docs/archive/REFACTORING_SUMMARY.md +184 -0
  17. data/docs/archive/phase_1_3_plan.md +136 -0
  18. data/docs/archive/sinatra_extension_summary.md +188 -0
  19. data/docs/archive/sinatra_working_solution.md +113 -0
  20. data/docs/archive/typescript-client-generator-summary.md +259 -0
  21. data/docs/auto-derivation.md +146 -0
  22. data/docs/blueprint.md +1091 -0
  23. data/docs/endpoint-definition.md +211 -0
  24. data/docs/github_pages_fix.md +52 -0
  25. data/docs/github_pages_setup.md +49 -0
  26. data/docs/implementation-status.md +357 -0
  27. data/docs/observability.md +647 -0
  28. data/docs/phase3-plan.md +108 -0
  29. data/docs/sinatra_rapitapir.md +87 -0
  30. data/docs/type_shortcuts.md +146 -0
  31. data/examples/README_ENTERPRISE.md +202 -0
  32. data/examples/authentication_example.rb +192 -0
  33. data/examples/auto_derivation_ruby_friendly.rb +163 -0
  34. data/examples/cli/user_api_endpoints.rb +56 -0
  35. data/examples/client/typescript_client_example.rb +102 -0
  36. data/examples/client/user-api-client.ts +193 -0
  37. data/examples/demo_api.rb +41 -0
  38. data/examples/docs/documentation_example.rb +112 -0
  39. data/examples/docs/user-api-docs.html +789 -0
  40. data/examples/docs/user-api-docs.md +403 -0
  41. data/examples/enhanced_auto_derivation_test.rb +83 -0
  42. data/examples/enterprise_extension_demo.rb +417 -0
  43. data/examples/enterprise_rapitapir_api.rb +662 -0
  44. data/examples/getting_started_extension.rb +218 -0
  45. data/examples/hello_world.rb +74 -0
  46. data/examples/oauth2/.env.example +19 -0
  47. data/examples/oauth2/README.md +205 -0
  48. data/examples/oauth2/generic_oauth2_api.rb +226 -0
  49. data/examples/oauth2/get_token.rb +72 -0
  50. data/examples/oauth2/songs_api_with_auth0.rb +320 -0
  51. data/examples/oauth2/test_api.sh +16 -0
  52. data/examples/oauth2/test_songs_api.sh +110 -0
  53. data/examples/observability/.env.example +35 -0
  54. data/examples/observability/README.md +230 -0
  55. data/examples/observability/README_HONEYCOMB.md +332 -0
  56. data/examples/observability/advanced_setup.rb +384 -0
  57. data/examples/observability/basic_setup.rb +192 -0
  58. data/examples/observability/complete_test.rb +121 -0
  59. data/examples/observability/honeycomb_example.rb +523 -0
  60. data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
  61. data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
  62. data/examples/observability/honeycomb_working_example.rb +489 -0
  63. data/examples/observability/quick_test.rb +78 -0
  64. data/examples/observability/simple_test.rb +14 -0
  65. data/examples/observability/test_honeycomb_demo.rb +354 -0
  66. data/examples/observability/test_live_honeycomb.rb +111 -0
  67. data/examples/observability/test_validation.rb +78 -0
  68. data/examples/observability/test_working_validation.rb +66 -0
  69. data/examples/openapi/user_api_schema.rb +132 -0
  70. data/examples/production_ready_example.rb +105 -0
  71. data/examples/rails/users_controller.rb +146 -0
  72. data/examples/readme/basic_sinatra_example.rb +128 -0
  73. data/examples/server/user_api.rb +179 -0
  74. data/examples/simple_auto_derivation_demo.rb +44 -0
  75. data/examples/simple_demo_api.rb +18 -0
  76. data/examples/sinatra/user_app.rb +127 -0
  77. data/examples/t_shortcut_demo.rb +59 -0
  78. data/examples/user_api.rb +190 -0
  79. data/examples/working_getting_started.rb +184 -0
  80. data/examples/working_simple_example.rb +195 -0
  81. data/lib/rapitapir/auth/configuration.rb +129 -0
  82. data/lib/rapitapir/auth/context.rb +122 -0
  83. data/lib/rapitapir/auth/errors.rb +104 -0
  84. data/lib/rapitapir/auth/middleware.rb +324 -0
  85. data/lib/rapitapir/auth/oauth2.rb +350 -0
  86. data/lib/rapitapir/auth/schemes.rb +420 -0
  87. data/lib/rapitapir/auth.rb +113 -0
  88. data/lib/rapitapir/cli/command.rb +535 -0
  89. data/lib/rapitapir/cli/server.rb +243 -0
  90. data/lib/rapitapir/cli/validator.rb +373 -0
  91. data/lib/rapitapir/client/generator_base.rb +272 -0
  92. data/lib/rapitapir/client/typescript_generator.rb +350 -0
  93. data/lib/rapitapir/core/endpoint.rb +158 -0
  94. data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
  95. data/lib/rapitapir/core/input.rb +182 -0
  96. data/lib/rapitapir/core/output.rb +164 -0
  97. data/lib/rapitapir/core/request.rb +19 -0
  98. data/lib/rapitapir/core/response.rb +17 -0
  99. data/lib/rapitapir/docs/html_generator.rb +780 -0
  100. data/lib/rapitapir/docs/markdown_generator.rb +464 -0
  101. data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
  102. data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
  103. data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
  104. data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
  105. data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
  106. data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
  107. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
  108. data/lib/rapitapir/dsl/http_verbs.rb +77 -0
  109. data/lib/rapitapir/dsl/input_methods.rb +47 -0
  110. data/lib/rapitapir/dsl/observability_methods.rb +81 -0
  111. data/lib/rapitapir/dsl/output_methods.rb +43 -0
  112. data/lib/rapitapir/dsl/type_resolution.rb +43 -0
  113. data/lib/rapitapir/observability/configuration.rb +108 -0
  114. data/lib/rapitapir/observability/health_check.rb +236 -0
  115. data/lib/rapitapir/observability/logging.rb +270 -0
  116. data/lib/rapitapir/observability/metrics.rb +203 -0
  117. data/lib/rapitapir/observability/middleware.rb +243 -0
  118. data/lib/rapitapir/observability/tracing.rb +143 -0
  119. data/lib/rapitapir/observability.rb +28 -0
  120. data/lib/rapitapir/openapi/schema_generator.rb +403 -0
  121. data/lib/rapitapir/schema.rb +136 -0
  122. data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
  123. data/lib/rapitapir/server/middleware.rb +120 -0
  124. data/lib/rapitapir/server/path_matcher.rb +45 -0
  125. data/lib/rapitapir/server/rack_adapter.rb +215 -0
  126. data/lib/rapitapir/server/rails_adapter.rb +17 -0
  127. data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
  128. data/lib/rapitapir/server/rails_controller.rb +72 -0
  129. data/lib/rapitapir/server/rails_input_processor.rb +73 -0
  130. data/lib/rapitapir/server/rails_response_handler.rb +29 -0
  131. data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
  132. data/lib/rapitapir/server/sinatra_integration.rb +93 -0
  133. data/lib/rapitapir/sinatra/configuration.rb +91 -0
  134. data/lib/rapitapir/sinatra/extension.rb +214 -0
  135. data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
  136. data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
  137. data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
  138. data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
  139. data/lib/rapitapir/types/array.rb +163 -0
  140. data/lib/rapitapir/types/auto_derivation.rb +265 -0
  141. data/lib/rapitapir/types/base.rb +146 -0
  142. data/lib/rapitapir/types/boolean.rb +46 -0
  143. data/lib/rapitapir/types/date.rb +92 -0
  144. data/lib/rapitapir/types/datetime.rb +98 -0
  145. data/lib/rapitapir/types/email.rb +32 -0
  146. data/lib/rapitapir/types/float.rb +134 -0
  147. data/lib/rapitapir/types/hash.rb +161 -0
  148. data/lib/rapitapir/types/integer.rb +143 -0
  149. data/lib/rapitapir/types/object.rb +156 -0
  150. data/lib/rapitapir/types/optional.rb +65 -0
  151. data/lib/rapitapir/types/string.rb +185 -0
  152. data/lib/rapitapir/types/uuid.rb +32 -0
  153. data/lib/rapitapir/types.rb +155 -0
  154. data/lib/rapitapir/version.rb +5 -0
  155. data/lib/rapitapir.rb +173 -0
  156. data/rapitapir.gemspec +66 -0
  157. 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