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,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
|