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,535 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'fileutils'
5
+
6
+ module RapiTapir
7
+ module CLI
8
+ # Command-line interface for RapiTapir operations
9
+ # Provides commands for generating OpenAPI specs, clients, and documentation
10
+ class Command
11
+ attr_reader :options
12
+
13
+ def initialize(args = ARGV)
14
+ @args = args
15
+ @options = {
16
+ input: nil,
17
+ output: nil,
18
+ format: 'json',
19
+ config: {}
20
+ }
21
+ end
22
+
23
+ def run
24
+ parser = create_option_parser
25
+
26
+ begin
27
+ parser.parse!(@args)
28
+ command = @args.shift
29
+ dispatch_command(command, parser)
30
+ rescue OptionParser::InvalidOption => e
31
+ handle_option_error(e, parser)
32
+ rescue StandardError => e
33
+ handle_general_error(e)
34
+ end
35
+ end
36
+
37
+ def dispatch_command(command, parser)
38
+ case command
39
+ when 'generate'
40
+ run_generate(@args)
41
+ when 'serve'
42
+ run_serve(@args)
43
+ when 'validate'
44
+ run_validate(@args)
45
+ when 'version'
46
+ puts "RapiTapir version #{RapiTapir::VERSION}"
47
+ when 'help', nil
48
+ puts parser.help
49
+ else
50
+ handle_unknown_command(command, parser)
51
+ end
52
+ end
53
+
54
+ def handle_unknown_command(command, parser)
55
+ puts "Unknown command: #{command}"
56
+ puts parser.help
57
+ exit 1
58
+ end
59
+
60
+ def handle_option_error(error, parser)
61
+ puts "Error: #{error.message}"
62
+ puts parser.help
63
+ exit 1
64
+ end
65
+
66
+ def handle_general_error(error)
67
+ puts "Error: #{error.message}"
68
+ raise error if ENV['RSPEC_RUNNING']
69
+
70
+ exit 1
71
+ end
72
+
73
+ private
74
+
75
+ def create_option_parser
76
+ OptionParser.new do |opts|
77
+ setup_banner_and_commands(opts)
78
+ setup_file_options(opts)
79
+ setup_config_options(opts)
80
+ setup_help_and_version_options(opts)
81
+ end
82
+ end
83
+
84
+ def setup_banner_and_commands(opts)
85
+ opts.banner = 'Usage: rapitapir [options] command [args]'
86
+ opts.separator ''
87
+ opts.separator 'Commands:'
88
+ opts.separator ' generate TYPE Generate clients or documentation'
89
+ opts.separator ' serve Start documentation server'
90
+ opts.separator ' validate Validate endpoint definitions'
91
+ opts.separator ' version Show version'
92
+ opts.separator ' help Show this help'
93
+ opts.separator ''
94
+ opts.separator 'Options:'
95
+ end
96
+
97
+ def setup_file_options(opts)
98
+ opts.on('-i', '--input FILE', '--endpoints FILE', 'Input Ruby file with endpoint definitions') do |file|
99
+ @options[:input] = file
100
+ end
101
+
102
+ opts.on('-o', '--output FILE', 'Output file path') do |file|
103
+ @options[:output] = file
104
+ end
105
+
106
+ opts.on('-f', '--format FORMAT', 'Output format (json, yaml, ts, py, md, html)') do |format|
107
+ @options[:format] = format
108
+ end
109
+ end
110
+
111
+ def setup_config_options(opts)
112
+ setup_generic_config_option(opts)
113
+ setup_specific_config_options(opts)
114
+ end
115
+
116
+ def setup_generic_config_option(opts)
117
+ opts.on('-c', '--config KEY=VALUE', 'Configuration option (can be used multiple times)') do |config|
118
+ key, value = config.split('=', 2)
119
+ @options[:config][key.to_sym] = value
120
+ end
121
+ end
122
+
123
+ def setup_specific_config_options(opts)
124
+ config_mappings = {
125
+ '--base-url URL' => [:base_url, 'Base URL for the API'],
126
+ '--client-name NAME' => [:client_name, 'Name for generated client class'],
127
+ '--package-name NAME' => [:package_name, 'Package name for generated client'],
128
+ '--client-version VERSION' => [:version, 'Version for generated client']
129
+ }
130
+
131
+ config_mappings.each do |option_spec, (config_key, description)|
132
+ opts.on(option_spec, description) do |value|
133
+ @options[:config][config_key] = value
134
+ end
135
+ end
136
+ end
137
+
138
+ public
139
+
140
+ def setup_help_and_version_options(opts)
141
+ opts.on('-v', '--version', 'Show RapiTapir version') do
142
+ puts "RapiTapir version #{RapiTapir::VERSION}"
143
+ exit
144
+ end
145
+
146
+ opts.on('-h', '--help', 'Show this help') do
147
+ puts opts
148
+ exit
149
+ end
150
+ end
151
+
152
+ def run_generate(args)
153
+ type = args.shift
154
+ unless type
155
+ puts 'Error: Generate command requires a type'
156
+ puts 'Available types: openapi, client, docs'
157
+ exit 1
158
+ end
159
+
160
+ case type
161
+ when 'openapi'
162
+ generate_openapi
163
+ when 'client'
164
+ client_type = args.shift || 'typescript'
165
+ generate_client(client_type)
166
+ when 'docs'
167
+ docs_type = args.shift || 'html'
168
+ generate_docs(docs_type)
169
+ else
170
+ puts "Error: Unknown generation type: #{type}"
171
+ puts 'Available types: openapi, client, docs'
172
+ exit 1
173
+ end
174
+ end
175
+
176
+ def run_serve(args)
177
+ port = args.shift || '3000'
178
+
179
+ unless @options[:input]
180
+ puts 'Error: --endpoints option is required for serve command'
181
+ exit 1
182
+ end
183
+
184
+ require_relative 'server'
185
+ server = CLI::Server.new(endpoints_file: @options[:input], port: port.to_i, config: @options[:config] || {})
186
+ puts "Starting documentation server on port #{port}..."
187
+ server.start
188
+ end
189
+
190
+ def run_validate(_args)
191
+ unless @options[:input]
192
+ puts 'Error: --endpoints is required'
193
+ exit 1
194
+ end
195
+
196
+ endpoints = load_endpoints(@options[:input])
197
+ require_relative 'validator'
198
+ validator = CLI::Validator.new(endpoints)
199
+
200
+ if validator.validate
201
+ puts '✓ All endpoints are valid'
202
+ else
203
+ puts '✗ Validation failed:'
204
+ validator.errors.each { |error| puts " - #{error}" }
205
+ exit 1
206
+ end
207
+ end
208
+
209
+ def generate_openapi
210
+ validate_openapi_options
211
+
212
+ begin
213
+ endpoints = load_endpoints(@options[:input])
214
+ generator = create_openapi_generator(endpoints)
215
+ content = generate_openapi_content(generator)
216
+ save_openapi_output(content)
217
+ rescue StandardError => e
218
+ handle_openapi_error(e)
219
+ end
220
+ end
221
+
222
+ def validate_openapi_options
223
+ unless @options[:input]
224
+ puts 'Error: --endpoints is required'
225
+ exit 1
226
+ end
227
+
228
+ return if @options[:output]
229
+
230
+ puts 'Error: --output is required'
231
+ exit 1
232
+ end
233
+
234
+ def create_openapi_generator(endpoints)
235
+ require_relative '../openapi/schema_generator'
236
+
237
+ openapi_info = build_openapi_info
238
+ openapi_servers = build_openapi_servers
239
+
240
+ RapiTapir::OpenAPI::SchemaGenerator.new(
241
+ endpoints: endpoints,
242
+ info: openapi_info,
243
+ servers: openapi_servers
244
+ )
245
+ end
246
+
247
+ def build_openapi_info
248
+ {
249
+ title: @options[:config][:title] || 'API Documentation',
250
+ version: @options[:config][:version] || '1.0.0',
251
+ description: @options[:config][:description] || 'Auto-generated API documentation'
252
+ }
253
+ end
254
+
255
+ def build_openapi_servers
256
+ servers = []
257
+ servers << { url: @options[:config][:base_url] } if @options[:config][:base_url]
258
+ servers
259
+ end
260
+
261
+ def generate_openapi_content(generator)
262
+ case @options[:format]
263
+ when 'yaml', 'yml'
264
+ generator.to_yaml
265
+ else
266
+ generator.to_json
267
+ end
268
+ end
269
+
270
+ def save_openapi_output(content)
271
+ if @options[:output]
272
+ File.write(@options[:output], content)
273
+ puts "OpenAPI schema saved to #{@options[:output]}"
274
+ else
275
+ puts content
276
+ end
277
+ end
278
+
279
+ def handle_openapi_error(error)
280
+ puts "Error generating OpenAPI schema: #{error.message}"
281
+ puts "Backtrace: #{error.backtrace.first(5).join("\n")}"
282
+ exit 1
283
+ end
284
+
285
+ def generate_client(client_type)
286
+ validate_client_options
287
+
288
+ endpoints = load_endpoints(@options[:input])
289
+ generator, extension = create_client_generator(client_type, endpoints)
290
+ content = generator.generate
291
+ save_client_output(content, client_type, extension)
292
+ end
293
+
294
+ def validate_client_options
295
+ unless @options[:input]
296
+ puts 'Error: --endpoints is required'
297
+ exit 1
298
+ end
299
+
300
+ return if @options[:output]
301
+
302
+ puts 'Error: --output is required'
303
+ exit 1
304
+ end
305
+
306
+ def create_client_generator(client_type, endpoints)
307
+ case client_type
308
+ when 'typescript', 'ts'
309
+ generator = create_typescript_generator(endpoints)
310
+ [generator, 'ts']
311
+ when 'python', 'py'
312
+ handle_python_generator_not_implemented
313
+ else
314
+ handle_unknown_client_type(client_type)
315
+ end
316
+ end
317
+
318
+ def create_typescript_generator(endpoints)
319
+ require_relative '../client/typescript_generator'
320
+ RapiTapir::Client::TypescriptGenerator.new(
321
+ endpoints: endpoints,
322
+ config: @options[:config]
323
+ )
324
+ end
325
+
326
+ def handle_python_generator_not_implemented
327
+ puts 'Error: Python client generator not implemented yet'
328
+ exit 1
329
+ end
330
+
331
+ def handle_unknown_client_type(client_type)
332
+ puts "Error: Unknown client type: #{client_type}"
333
+ puts 'Available types: typescript, python'
334
+ exit 1
335
+ end
336
+
337
+ def save_client_output(content, client_type, extension)
338
+ if @options[:output]
339
+ File.write(@options[:output], content)
340
+ puts "#{client_type.capitalize} client saved to #{@options[:output]}"
341
+ else
342
+ default_output = "api-client.#{extension}"
343
+ File.write(default_output, content)
344
+ puts "#{client_type.capitalize} client saved to #{default_output}"
345
+ end
346
+ end
347
+
348
+ def generate_docs(docs_type)
349
+ validate_docs_options
350
+
351
+ endpoints = load_endpoints(@options[:input])
352
+ generator, extension = create_docs_generator(docs_type, endpoints)
353
+ content = generator.generate
354
+ save_docs_output(content, docs_type, extension)
355
+ end
356
+
357
+ def validate_docs_options
358
+ unless @options[:input]
359
+ puts 'Error: --endpoints is required'
360
+ exit 1
361
+ end
362
+
363
+ return if @options[:output]
364
+
365
+ puts 'Error: --output is required'
366
+ exit 1
367
+ end
368
+
369
+ def create_docs_generator(docs_type, endpoints)
370
+ case docs_type
371
+ when 'markdown', 'md'
372
+ generator = create_markdown_generator(endpoints)
373
+ [generator, 'md']
374
+ when 'html'
375
+ generator = create_html_generator(endpoints)
376
+ [generator, 'html']
377
+ else
378
+ handle_unknown_docs_type(docs_type)
379
+ end
380
+ end
381
+
382
+ def create_markdown_generator(endpoints)
383
+ require_relative '../docs/markdown_generator'
384
+ RapiTapir::Docs::MarkdownGenerator.new(
385
+ endpoints: endpoints,
386
+ config: @options[:config]
387
+ )
388
+ end
389
+
390
+ def create_html_generator(endpoints)
391
+ require_relative '../docs/html_generator'
392
+ RapiTapir::Docs::HtmlGenerator.new(
393
+ endpoints: endpoints,
394
+ config: @options[:config]
395
+ )
396
+ end
397
+
398
+ def handle_unknown_docs_type(docs_type)
399
+ puts "Error: Unknown documentation type: #{docs_type}"
400
+ puts 'Available types: markdown, html'
401
+ exit 1
402
+ end
403
+
404
+ def save_docs_output(content, docs_type, extension)
405
+ if @options[:output]
406
+ File.write(@options[:output], content)
407
+ puts "#{docs_type.capitalize} documentation saved to #{@options[:output]}"
408
+ else
409
+ default_output = "api-docs.#{extension}"
410
+ File.write(default_output, content)
411
+ puts "#{docs_type.capitalize} documentation saved to #{default_output}"
412
+ end
413
+ end
414
+
415
+ def load_endpoints(file_path)
416
+ raise "Input file not found: #{file_path}" unless File.exist?(file_path)
417
+
418
+ prepare_loading_environment(file_path) do
419
+ content = File.read(file_path)
420
+ load_endpoints_from_content(content, file_path) || load_endpoints_fallback(file_path)
421
+ end
422
+ end
423
+
424
+ def prepare_loading_environment(file_path)
425
+ # Store original state
426
+ original_endpoints = RapiTapir.instance_variable_get(:@endpoints) || []
427
+ original_load_path = $LOAD_PATH.dup
428
+
429
+ # Add the file's directory to load path for relative requires
430
+ file_dir = File.dirname(File.expand_path(file_path))
431
+ $LOAD_PATH.unshift(file_dir) unless $LOAD_PATH.include?(file_dir)
432
+
433
+ RapiTapir.instance_variable_set(:@endpoints, [])
434
+
435
+ begin
436
+ yield
437
+ rescue SyntaxError => e
438
+ raise "Syntax error in #{file_path}: #{e.message}"
439
+ ensure
440
+ RapiTapir.instance_variable_set(:@endpoints, original_endpoints)
441
+ $LOAD_PATH.replace(original_load_path)
442
+ end
443
+ end
444
+
445
+ def load_endpoints_from_content(content, file_path)
446
+ return nil unless content.match?(/(\w+_api|\w+_endpoints|\wendpoints\w*)\s*=\s*\[/)
447
+
448
+ file_dir = File.dirname(File.expand_path(file_path))
449
+ Dir.chdir(file_dir) do
450
+ # Execute the file content using load instead of eval for safety
451
+ # rubocop:disable Security/Eval
452
+ eval(content, TOPLEVEL_BINDING, file_path)
453
+ # rubocop:enable Security/Eval
454
+ end
455
+
456
+ find_endpoints_in_variables(content)
457
+ end
458
+
459
+ def find_endpoints_in_variables(content)
460
+ # Try instance variables
461
+ endpoints = find_endpoints_in_instance_variables
462
+ return endpoints if endpoints
463
+
464
+ # Try global variables
465
+ endpoints = find_endpoints_in_global_variables
466
+ return endpoints if endpoints
467
+
468
+ # Try content-matched variables
469
+ find_endpoints_in_content_variables(content)
470
+ end
471
+
472
+ def find_endpoints_in_instance_variables
473
+ main_obj = TOPLEVEL_BINDING.eval('self')
474
+ endpoints_var = main_obj.instance_variables.find do |var|
475
+ var.to_s.include?('api') || var.to_s.include?('endpoint')
476
+ end
477
+
478
+ return nil unless endpoints_var
479
+
480
+ endpoints = main_obj.instance_variable_get(endpoints_var)
481
+ normalize_endpoints_array(endpoints)
482
+ end
483
+
484
+ def find_endpoints_in_global_variables
485
+ global_endpoints_var = global_variables.find do |var|
486
+ var.to_s.include?('api') || var.to_s.include?('endpoint')
487
+ end
488
+
489
+ return nil unless global_endpoints_var
490
+
491
+ # rubocop:disable Security/Eval
492
+ endpoints = eval(global_endpoints_var.to_s)
493
+ # rubocop:enable Security/Eval
494
+ normalize_endpoints_array(endpoints)
495
+ end
496
+
497
+ def find_endpoints_in_content_variables(content)
498
+ var_match = content.match(/(\w+_api|\w+_endpoints|\wendpoints\w*)\s*=/)
499
+ return nil unless var_match
500
+
501
+ var_name = var_match[1]
502
+ begin
503
+ # rubocop:disable Security/Eval
504
+ endpoints = eval(var_name, TOPLEVEL_BINDING)
505
+ # rubocop:enable Security/Eval
506
+ normalize_endpoints_array(endpoints)
507
+ rescue NameError
508
+ nil
509
+ end
510
+ end
511
+
512
+ def normalize_endpoints_array(endpoints)
513
+ return nil unless endpoints
514
+
515
+ endpoints = [endpoints] unless endpoints.is_a?(Array)
516
+ endpoints.flatten.compact
517
+ end
518
+
519
+ def load_endpoints_fallback(file_path)
520
+ # Fallback: try loading the file normally
521
+ load File.expand_path(file_path)
522
+ endpoints = RapiTapir.instance_variable_get(:@endpoints) || []
523
+
524
+ if endpoints.empty?
525
+ error_msg = "No endpoints found in #{file_path}. " \
526
+ 'Make sure the file defines endpoints in a variable ' \
527
+ "containing 'api' or 'endpoints' in its name."
528
+ raise error_msg
529
+ end
530
+
531
+ endpoints
532
+ end
533
+ end
534
+ end
535
+ end