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,379 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'json'
5
+ require_relative '../core/enhanced_endpoint'
6
+ require_relative '../dsl/enhanced_endpoint_dsl'
7
+ require_relative 'path_matcher'
8
+
9
+ module RapiTapir
10
+ module Server
11
+ # Enhanced Rack adapter that integrates with the new type system
12
+ class EnhancedRackAdapter
13
+ attr_reader :endpoints, :middleware_stack, :error_handlers
14
+
15
+ def initialize
16
+ @endpoints = []
17
+ @middleware_stack = []
18
+ @error_handlers = {}
19
+ end
20
+
21
+ # Register an endpoint with a handler
22
+ def mount(endpoint, &handler)
23
+ raise ArgumentError, 'Endpoint must be provided' unless endpoint
24
+ raise ArgumentError, 'Handler block must be provided' unless block_given?
25
+
26
+ # Ensure we're working with an enhanced endpoint
27
+ enhanced_endpoint = case endpoint
28
+ when Core::EnhancedEndpoint
29
+ endpoint
30
+ when Core::Endpoint
31
+ # Convert regular endpoint to enhanced endpoint
32
+ convert_to_enhanced(endpoint)
33
+ else
34
+ raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint'
35
+ end
36
+
37
+ enhanced_endpoint.validate!
38
+ path_matcher = PathMatcher.new(enhanced_endpoint.path)
39
+
40
+ @endpoints << {
41
+ endpoint: enhanced_endpoint,
42
+ handler: handler,
43
+ path_matcher: path_matcher
44
+ }
45
+ end
46
+
47
+ # Add middleware to the processing stack
48
+ def use(middleware_class, *args, &block)
49
+ @middleware_stack << [middleware_class, args, block]
50
+ end
51
+
52
+ # Register error handlers
53
+ def on_error(error_class, &handler)
54
+ @error_handlers[error_class] = handler
55
+ end
56
+
57
+ # Main Rack application call method
58
+ def call(env)
59
+ app = build_app
60
+ app.call(env)
61
+ end
62
+
63
+ # Build the complete middleware stack
64
+ def build_app
65
+ app = method(:handle_request)
66
+
67
+ @middleware_stack.reverse.each do |middleware_class, args, block|
68
+ app = middleware_class.new(app, *args, &block)
69
+ end
70
+
71
+ app
72
+ end
73
+
74
+ # Core request handler with enhanced type validation
75
+ def handle_request(env)
76
+ request = Rack::Request.new(env)
77
+
78
+ # Find matching endpoint
79
+ endpoint_match = find_matching_endpoint(request)
80
+ return not_found_response unless endpoint_match
81
+
82
+ begin
83
+ # Process the request through the endpoint with full validation
84
+ process_enhanced_request(request, endpoint_match)
85
+ rescue Core::EnhancedEndpoint::ValidationError => e
86
+ validation_error_response(e)
87
+ rescue Types::ValidationError => e
88
+ type_validation_error_response(e)
89
+ rescue Types::CoercionError => e
90
+ coercion_error_response(e)
91
+ rescue StandardError => e
92
+ handle_custom_error(e) || error_response(e)
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def find_matching_endpoint(request)
99
+ @endpoints.find do |endpoint_data|
100
+ endpoint = endpoint_data[:endpoint]
101
+ path_matcher = endpoint_data[:path_matcher]
102
+ matches_method?(endpoint, request) && path_matcher.matches?(request.path_info)
103
+ end
104
+ end
105
+
106
+ def matches_method?(endpoint, request)
107
+ endpoint.method.to_s.upcase == request.request_method
108
+ end
109
+
110
+ def process_enhanced_request(request, endpoint_match)
111
+ endpoint = endpoint_match[:endpoint]
112
+ handler = endpoint_match[:handler]
113
+ path_matcher = endpoint_match[:path_matcher]
114
+
115
+ # Extract path parameters
116
+ path_params = path_matcher.match(request.path_info) || {}
117
+
118
+ # Extract and validate inputs using the enhanced type system
119
+ processed_inputs = extract_and_validate_inputs(request, endpoint, path_params)
120
+
121
+ # Validate the complete request
122
+ validated_inputs = endpoint.process_request(processed_inputs)
123
+
124
+ # Call the handler with validated inputs
125
+ result = handler.call(validated_inputs)
126
+
127
+ # Validate and serialize response
128
+ serialize_validated_response(result, endpoint)
129
+ end
130
+
131
+ def extract_and_validate_inputs(request, endpoint, path_params = {})
132
+ inputs = {}
133
+
134
+ endpoint.inputs.each do |input|
135
+ processed_input = process_single_input(request, input, path_params)
136
+ inputs.merge!(processed_input) if processed_input
137
+ end
138
+
139
+ inputs
140
+ end
141
+
142
+ def process_single_input(request, input, path_params)
143
+ value = extract_input_value(request, input, path_params)
144
+
145
+ # Handle required inputs
146
+ validate_required_input(input, value)
147
+
148
+ # Skip optional nil values
149
+ return nil if value.nil? && input.optional?
150
+
151
+ # Coerce and validate the value
152
+ coerced_value = coerce_and_validate_input(input, value)
153
+ { input.name => coerced_value }
154
+ end
155
+
156
+ def validate_required_input(input, value)
157
+ return unless value.nil? && input.required?
158
+
159
+ raise Core::EnhancedEndpoint::ValidationError, "Required input '#{input.name}' is missing"
160
+ end
161
+
162
+ def coerce_and_validate_input(input, value)
163
+ coerced_value = input.coerce(value)
164
+ validation_result = input.validate(coerced_value)
165
+
166
+ unless validation_result[:valid]
167
+ errors = validation_result[:errors].join(', ')
168
+ raise Core::EnhancedEndpoint::ValidationError, "Input '#{input.name}' validation failed: #{errors}"
169
+ end
170
+
171
+ coerced_value
172
+ rescue Types::CoercionError => e
173
+ raise Core::EnhancedEndpoint::ValidationError, "Input '#{input.name}' coercion failed: #{e.message}"
174
+ end
175
+
176
+ def extract_input_value(request, input, path_params)
177
+ case input.kind
178
+ when :query
179
+ request.params[input.name.to_s]
180
+ when :header
181
+ extract_header_value(request, input)
182
+ when :path
183
+ path_params[input.name]
184
+ when :body
185
+ parse_request_body(request, input)
186
+ end
187
+ end
188
+
189
+ def extract_header_value(request, input)
190
+ header_name = case input.name
191
+ when :authorization
192
+ 'HTTP_AUTHORIZATION'
193
+ else
194
+ "HTTP_#{input.name.to_s.upcase.gsub('-', '_')}"
195
+ end
196
+ request.get_header(header_name)
197
+ end
198
+
199
+ def parse_request_body(request, input)
200
+ body = request.body.read
201
+ request.body.rewind
202
+
203
+ return nil if body.empty?
204
+
205
+ content_type = request.content_type&.downcase
206
+ format = input.options[:format] || detect_format_from_content_type(content_type)
207
+
208
+ case format
209
+ when :json
210
+ JSON.parse(body)
211
+ when :form
212
+ Rack::Utils.parse_nested_query(body)
213
+ else
214
+ body
215
+ end
216
+ rescue JSON::ParserError => e
217
+ raise Core::EnhancedEndpoint::ValidationError, "Invalid JSON in request body: #{e.message}"
218
+ end
219
+
220
+ def detect_format_from_content_type(content_type)
221
+ case content_type
222
+ when %r{application/json}
223
+ :json
224
+ when %r{application/x-www-form-urlencoded}, %r{multipart/form-data}
225
+ :form
226
+ else
227
+ :text
228
+ end
229
+ end
230
+
231
+ def serialize_validated_response(result, endpoint)
232
+ output, status_code = find_response_configuration(endpoint)
233
+
234
+ if output
235
+ build_validated_response(result, output, status_code)
236
+ else
237
+ build_default_response(result, status_code)
238
+ end
239
+ end
240
+
241
+ def find_response_configuration(endpoint)
242
+ output = find_response_output(endpoint)
243
+ status_code = find_status_code(endpoint)
244
+
245
+ [output, status_code]
246
+ end
247
+
248
+ def find_response_output(endpoint)
249
+ endpoint.outputs.find { |o| o.kind == :json } ||
250
+ endpoint.outputs.find { |o| o.kind == :text } ||
251
+ endpoint.outputs.first
252
+ end
253
+
254
+ def find_status_code(endpoint)
255
+ status_output = endpoint.outputs.find { |o| o.kind == :status }
256
+ status_output&.type || determine_default_status(endpoint.method)
257
+ end
258
+
259
+ def build_validated_response(result, output, status_code)
260
+ validate_response_result(result, output)
261
+
262
+ serialized = output.serialize(result)
263
+ content_type = determine_content_type(output)
264
+
265
+ [status_code, { 'Content-Type' => content_type }, [serialized]]
266
+ end
267
+
268
+ def validate_response_result(result, output)
269
+ validation_result = output.validate(result)
270
+ return if validation_result[:valid]
271
+
272
+ errors = validation_result[:errors].join(', ')
273
+ raise StandardError, "Response validation failed: #{errors}"
274
+ end
275
+
276
+ def build_default_response(result, status_code)
277
+ [status_code, { 'Content-Type' => 'application/json' }, [JSON.generate(result)]]
278
+ end
279
+
280
+ def determine_content_type(output)
281
+ case output.kind
282
+ when :text
283
+ 'text/plain'
284
+ else # Default to JSON for :json and unknown formats
285
+ 'application/json'
286
+ end
287
+ end
288
+
289
+ def determine_default_status(method)
290
+ case method
291
+ when :post
292
+ 201
293
+ when :delete
294
+ 204
295
+ else
296
+ 200
297
+ end
298
+ end
299
+
300
+ def convert_to_enhanced(endpoint)
301
+ # Convert a regular endpoint to an enhanced endpoint
302
+ Core::EnhancedEndpoint.new(
303
+ method: endpoint.method,
304
+ path: endpoint.path,
305
+ inputs: endpoint.inputs,
306
+ outputs: endpoint.outputs,
307
+ errors: endpoint.errors,
308
+ metadata: endpoint.metadata
309
+ )
310
+ end
311
+
312
+ def handle_custom_error(error)
313
+ handler = @error_handlers[error.class] || @error_handlers[StandardError]
314
+ return nil unless handler
315
+
316
+ begin
317
+ result = handler.call(error)
318
+ [500, { 'Content-Type' => 'application/json' }, [JSON.generate(result)]]
319
+ rescue StandardError
320
+ # If custom error handler fails, fall back to default
321
+ nil
322
+ end
323
+ end
324
+
325
+ # Error response helpers
326
+ def not_found_response
327
+ error_data = {
328
+ error: 'Not Found',
329
+ message: 'The requested endpoint was not found',
330
+ code: 404
331
+ }
332
+ [404, { 'Content-Type' => 'application/json' }, [JSON.generate(error_data)]]
333
+ end
334
+
335
+ def validation_error_response(error)
336
+ error_data = {
337
+ error: 'Validation Error',
338
+ message: error.message,
339
+ code: 400
340
+ }
341
+ [400, { 'Content-Type' => 'application/json' }, [JSON.generate(error_data)]]
342
+ end
343
+
344
+ def type_validation_error_response(error)
345
+ error_data = {
346
+ error: 'Type Validation Error',
347
+ message: error.message,
348
+ type: error.type.to_s,
349
+ value: error.value,
350
+ errors: error.errors,
351
+ code: 400
352
+ }
353
+ [400, { 'Content-Type' => 'application/json' }, [JSON.generate(error_data)]]
354
+ end
355
+
356
+ def coercion_error_response(error)
357
+ error_data = {
358
+ error: 'Type Coercion Error',
359
+ message: error.message,
360
+ type: error.type.to_s,
361
+ value: error.value,
362
+ reason: error.reason,
363
+ code: 400
364
+ }
365
+ [400, { 'Content-Type' => 'application/json' }, [JSON.generate(error_data)]]
366
+ end
367
+
368
+ def error_response(error)
369
+ error_data = {
370
+ error: error.class.name,
371
+ message: error.message,
372
+ code: 500
373
+ }
374
+
375
+ [500, { 'Content-Type' => 'application/json' }, [JSON.generate(error_data)]]
376
+ end
377
+ end
378
+ end
379
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Server
5
+ module Middleware
6
+ # Base middleware class for RapiTapir middleware
7
+ class Base
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ @app.call(env)
14
+ end
15
+ end
16
+
17
+ # Logging middleware for request/response logging
18
+ class Logger < Base
19
+ def initialize(app, logger = nil)
20
+ super(app)
21
+ @logger = logger || default_logger
22
+ end
23
+
24
+ def call(env)
25
+ start_time = Time.now
26
+ request = Rack::Request.new(env)
27
+
28
+ @logger.info("Started #{request.request_method} #{request.fullpath}")
29
+
30
+ status, headers, body = @app.call(env)
31
+
32
+ duration = ((Time.now - start_time) * 1000).round(2)
33
+ @logger.info("Completed #{status} in #{duration}ms")
34
+
35
+ [status, headers, body]
36
+ end
37
+
38
+ private
39
+
40
+ def default_logger
41
+ require 'logger'
42
+ Logger.new($stdout).tap do |logger|
43
+ logger.level = Logger::INFO
44
+ logger.formatter = proc do |severity, datetime, _progname, msg|
45
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # CORS middleware for cross-origin requests
52
+ class CORS < Base
53
+ def initialize(app, options = {})
54
+ super(app)
55
+ @options = {
56
+ allow_origin: '*',
57
+ allow_methods: %w[GET POST PUT DELETE OPTIONS PATCH],
58
+ allow_headers: %w[Content-Type Authorization],
59
+ max_age: 86_400
60
+ }.merge(options)
61
+ end
62
+
63
+ def call(env)
64
+ if env['REQUEST_METHOD'] == 'OPTIONS'
65
+ # Preflight request
66
+ [200, cors_headers, ['']]
67
+ else
68
+ status, headers, body = @app.call(env)
69
+ [status, headers.merge(cors_headers), body]
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def cors_headers
76
+ {
77
+ 'Access-Control-Allow-Origin' => @options[:allow_origin],
78
+ 'Access-Control-Allow-Methods' => @options[:allow_methods].join(', '),
79
+ 'Access-Control-Allow-Headers' => @options[:allow_headers].join(', '),
80
+ 'Access-Control-Max-Age' => @options[:max_age].to_s
81
+ }
82
+ end
83
+ end
84
+
85
+ # Exception handling middleware
86
+ class ExceptionHandler < Base
87
+ def initialize(app, options = {})
88
+ super(app)
89
+ @show_exceptions = options.fetch(:show_exceptions, false)
90
+ @logger = options[:logger]
91
+ end
92
+
93
+ def call(env)
94
+ @app.call(env)
95
+ rescue StandardError => e
96
+ @logger&.error("Unhandled exception: #{e.class}: #{e.message}")
97
+ @logger&.error(e.backtrace.join("\n")) if @show_exceptions
98
+
99
+ error_response(e)
100
+ end
101
+
102
+ private
103
+
104
+ def error_response(error)
105
+ error_data = { error: 'Internal Server Error' }
106
+
107
+ if @show_exceptions
108
+ error_data.merge!(
109
+ exception: error.class.name,
110
+ message: error.message,
111
+ backtrace: error.backtrace
112
+ )
113
+ end
114
+
115
+ [500, { 'Content-Type' => 'application/json' }, [JSON.generate(error_data)]]
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Server
5
+ # Path matching utility for HTTP routes
6
+ # Matches request paths against endpoint path patterns with parameter extraction
7
+ class PathMatcher
8
+ attr_reader :path_pattern, :param_names
9
+
10
+ def initialize(path_pattern)
11
+ @path_pattern = path_pattern
12
+ @param_names = extract_param_names(path_pattern)
13
+ @regex = build_regex(path_pattern)
14
+ end
15
+
16
+ def match(path)
17
+ match_data = @regex.match(path)
18
+ return nil unless match_data
19
+
20
+ params = {}
21
+ @param_names.each_with_index do |param_name, index|
22
+ params[param_name.to_sym] = match_data[index + 1]
23
+ end
24
+
25
+ params
26
+ end
27
+
28
+ def matches?(path)
29
+ @regex.match?(path)
30
+ end
31
+
32
+ private
33
+
34
+ def extract_param_names(pattern)
35
+ pattern.scan(/:(\w+)/).flatten
36
+ end
37
+
38
+ def build_regex(pattern)
39
+ # Convert "/users/:id/posts/:post_id" to /^\/users\/([^\/]+)\/posts\/([^\/]+)$/
40
+ regex_pattern = pattern.gsub(/:(\w+)/, '([^/]+)')
41
+ Regexp.new("^#{regex_pattern}$")
42
+ end
43
+ end
44
+ end
45
+ end