swagger_autogenerate 1.2.9 → 2.0.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.
@@ -1,99 +1,90 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'configuration'
4
+ require_relative 'helpers'
5
+ require_relative 'schema_builder'
6
+ require_relative 'parameter_builder'
7
+ require_relative 'response_builder'
8
+ require_relative 'path_normalizer'
9
+ require_relative 'document_writer'
10
+ require_relative 'yaml_merger'
2
11
 
3
12
  module SwaggerAutogenerate
13
+ # Observes one request/response pair and updates the Swagger YAML file.
4
14
  class SwaggerTrace
15
+ class << self
16
+ attr_accessor :rspec_description
17
+
18
+ def swagger_path_environment_variable
19
+ ::SwaggerAutogenerate.configuration.swagger_path_environment_variable
20
+ end
21
+
22
+ def generate_swagger_environment_variable
23
+ ::SwaggerAutogenerate.configuration.generate_swagger_environment_variable
24
+ end
25
+
26
+ def environment_name
27
+ ::SwaggerAutogenerate.configuration.environment_name
28
+ end
29
+ end
30
+
5
31
  def initialize(request, response)
6
- @with_config = ::SwaggerAutogenerate.configuration.with_config
7
- @with_multiple_examples = ::SwaggerAutogenerate.configuration.with_multiple_examples
8
- @with_rspec_examples = ::SwaggerAutogenerate.configuration.with_rspec_examples
9
- @with_response_description = ::SwaggerAutogenerate.configuration.with_response_description
10
- @with_payload_properties = ::SwaggerAutogenerate.configuration.with_payload_properties
11
- @security = ::SwaggerAutogenerate.configuration.security
12
- @swagger_config = ::SwaggerAutogenerate.configuration.swagger_config
13
- @response_status = ::SwaggerAutogenerate.configuration.response_status
14
- @default_path = ::SwaggerAutogenerate.configuration.default_path
32
+ @config = ::SwaggerAutogenerate.configuration
15
33
  @request = request
16
34
  @response = response
17
- @action_for_old_examples = ::SwaggerAutogenerate.configuration.action_for_old_examples
18
- @@paths = {}
35
+ @schema = SchemaBuilder.new
36
+ @parameter_builder = ParameterBuilder.new(request, schema_builder: @schema)
37
+ @paths = {}
38
+ @@removed_examples ||= []
19
39
  end
20
40
 
21
41
  def call
22
- read_swagger_trace
23
- write_swagger_trace
24
- end
25
-
26
- def self.swagger_path_environment_variable
27
- ::SwaggerAutogenerate.configuration.swagger_path_environment_variable
42
+ @current_path = PathNormalizer.call(request)
43
+ replace_old_examples_if_needed
44
+ build_path_entry
45
+ write_document
28
46
  end
29
47
 
30
48
  def swagger_path_environment_variable
31
- SwaggerTrace.swagger_path_environment_variable
32
- end
33
-
34
- def self.generate_swagger_environment_variable
35
- ::SwaggerAutogenerate.configuration.generate_swagger_environment_variable
49
+ self.class.swagger_path_environment_variable
36
50
  end
37
51
 
38
52
  def generate_swagger_environment_variable
39
- SwaggerTrace.generate_swagger_environment_variable
40
- end
41
-
42
- def self.environment_name
43
- ::SwaggerAutogenerate.configuration.environment_name
53
+ self.class.generate_swagger_environment_variable
44
54
  end
45
55
 
46
56
  def environment_name
47
- SwaggerTrace.environment_name
57
+ self.class.environment_name
48
58
  end
49
59
 
50
60
  private
51
61
 
52
- attr_reader :request, :response, :current_path, :yaml_file, :configuration,
53
- :with_config, :with_multiple_examples, :with_rspec_examples,
54
- :with_response_description, :with_payload_properties, :security, :response_status, :swagger_config,
55
- :default_path, :action_for_old_examples
56
-
57
- # main methods
58
-
59
- def read_swagger_trace
60
- path = request.path
62
+ attr_reader :request, :response, :config, :schema, :parameter_builder, :paths, :current_path
61
63
 
62
- request.path_parameters.except(:controller, :format, :action).each do |k, v|
63
- path_array = path.split('/')
64
- index = path_array.rindex(v)
65
- path_array[index] = "{#{k}}"
66
- path.replace(path_array.join('/'))
67
- end
68
-
69
- @current_path = path
64
+ def build_path_entry
70
65
  method = request.method.to_s.downcase
71
-
72
- process_replacing_examples
73
-
74
- hash =
75
- {
76
- method => {
77
- 'tags' => tags,
78
- 'summary' => summary,
79
- 'requestBody' => request_body,
80
- 'parameters' => parameters,
81
- 'responses' => {},
82
- 'security' => current_security
83
- }
66
+ hash = {
67
+ method => {
68
+ 'tags' => tags,
69
+ 'summary' => summary,
70
+ 'parameters' => parameter_builder.parameters,
71
+ 'responses' => {},
72
+ 'security' => current_security
84
73
  }
74
+ }
85
75
 
86
- hash[method].except!('requestBody') if hash[method]['requestBody'].blank?
87
- paths[path.to_s] ||= {}
88
- paths[path.to_s].merge!(hash)
76
+ body = parameter_builder.request_body
77
+ hash[method]['requestBody'] = body if body.present?
78
+
79
+ paths[current_path] ||= {}
80
+ paths[current_path].merge!(hash)
89
81
  end
90
82
 
91
- def write_swagger_trace
92
- if paths[current_path][request.method.downcase].present?
93
- paths[current_path][request.method.downcase]['responses'] = swagger_response
94
- end
83
+ def write_document
84
+ method = request.method.downcase
85
+ paths[current_path][method]['responses'] = swagger_response if paths.dig(current_path, method)
95
86
 
96
- if File.exist?(swagger_location)
87
+ if document_writer.exist?
97
88
  edit_file
98
89
  else
99
90
  create_file
@@ -101,734 +92,127 @@ module SwaggerAutogenerate
101
92
  end
102
93
 
103
94
  def create_file
104
- File.open(swagger_location, 'w') do |file|
105
- return file.write(YAML.dump({ 'paths' => {} })) if paths.blank?
106
- data = with_config ? swagger_config : {}
107
- data['paths'] = paths
108
- organize_result(data['paths'])
109
- data = data.to_hash
110
- # handel examples names
111
- example_title = full_rspec_description.present? ? full_rspec_description : 'example-0'
112
- old_examples = data['paths'][current_path][request.method.downcase]['responses'][response.status.to_s]['content']['application/json']['examples']
113
- current_example = old_examples[example_title]
114
- new_example(example_title, current_example, old_examples, data['paths'], true)
115
- # result
116
- result = reformat_dates_in_hash(data)
117
- result = convert_to_hash(result)
118
- result = YAML.dump(result)
119
- file.write(result)
120
- end
95
+ data = config.with_config ? deep_dup(config.resolved_swagger_config) : {}
96
+ data['paths'] = paths
97
+ merger = build_merger(data)
98
+ merger.organize_result!(data['paths'])
99
+ merger.merge_example!(data['paths'], with_schema_properties: true)
100
+ document_writer.write(data)
121
101
  end
122
102
 
123
103
  def edit_file
124
- @yaml_file = \
125
- YAML.safe_load(
126
- File.read(swagger_location),
127
- aliases: true,
128
- permitted_classes: [Symbol, DateTime, Date, Time, ActiveSupport::HashWithIndifferentAccess],
129
- permitted_symbols: []
130
- )
131
-
104
+ yaml_file = document_writer.read
132
105
  return create_file if yaml_file.nil? || yaml_file['paths'].nil?
133
106
 
134
- yaml_file.merge!(swagger_config) if with_config
135
-
136
- apply_yaml_file_changes
137
- organize_result(yaml_file['paths'])
138
- @yaml_file = convert_to_hash(yaml_file)
139
- File.open(swagger_location, 'w') do |file|
140
- result = reformat_dates_in_hash(yaml_file)
141
- result = convert_to_hash(result)
142
- result = YAML.dump(result)
143
- file.write(result)
144
- end
107
+ yaml_file.merge!(deep_dup(config.resolved_swagger_config)) if config.with_config
108
+ build_merger(yaml_file).apply!
109
+ document_writer.write(yaml_file)
145
110
  end
146
111
 
147
- # Helpers
148
-
149
- def process_replacing_examples
150
- create_file_if_not_exist
151
- $removed_examples ||= []
152
- if action_for_old_examples == :replace && !$removed_examples.include?({ @current_path => request.method.to_s.downcase })
153
- current_yaml_file = YAML.load(
154
- File.read(swagger_location),
155
- aliases: true,
156
- permitted_classes: [Symbol, Date, ActiveSupport::HashWithIndifferentAccess]
157
- )
158
-
159
- hash = { "paths" => { @current_path => { request.method.to_s.downcase => {} } } }
160
-
161
- current_yaml_file&.merge!(hash)
162
- File.open(swagger_location, 'w') do |file|
163
- result = reformat_dates_in_hash(current_yaml_file)
164
- result = convert_to_hash(result)
165
- result = YAML.dump(result)
166
- file.write(result)
167
- end
168
-
169
- $removed_examples << { @current_path => request.method.to_s.downcase }
170
- end
112
+ def build_merger(yaml_file)
113
+ YamlMerger.new(
114
+ yaml_file: yaml_file,
115
+ paths: paths,
116
+ current_path: current_path,
117
+ request: request,
118
+ response: response,
119
+ swagger_response: swagger_response,
120
+ example_title: example_title,
121
+ config: config,
122
+ schema_builder: schema,
123
+ parameter_builder: parameter_builder,
124
+ tags: tags,
125
+ summary: summary,
126
+ security: current_security
127
+ )
171
128
  end
172
129
 
173
- def reformat_dates_in_hash(data)
174
- case data
175
- when Hash
176
- data.each do |key, value|
177
- data[key] = reformat_dates_in_hash(value)
178
- end
179
- when Array
180
- data.map! { |element| reformat_dates_in_hash(element) }
181
- when String
182
- if is_valid_date?(data)
183
- convert_to_date(data).to_s
184
- else
185
- data
186
- end
187
- else
188
- data
189
- end
130
+ def document_writer
131
+ @document_writer ||= DocumentWriter.new(
132
+ config: config,
133
+ tags: [file_tag],
134
+ request: request
135
+ )
190
136
  end
191
137
 
192
- def convert_to_hash(obj)
193
- case obj
194
- when ActiveSupport::HashWithIndifferentAccess
195
- obj.to_hash
196
- when Hash
197
- obj.transform_values { |value| convert_to_hash(value) }
198
- when Array
199
- obj.map { |item| convert_to_hash(item) }
200
- else
201
- obj
202
- end
203
- end
138
+ def replace_old_examples_if_needed
139
+ return unless config.action_for_old_examples == :replace
204
140
 
205
- def properties_data(value)
206
- hash = {}
141
+ document_writer.ensure_exists!
142
+ marker = { current_path => request.method.to_s.downcase }
143
+ return if @@removed_examples.include?(marker)
207
144
 
208
- value.map do |k, v|
209
- type = schema_type(v)
210
- hash.merge!({ k => { 'type' => type, 'example' => convert_to_hash(v) } })
211
- end
212
-
213
- hash
145
+ current_yaml = document_writer.read || { 'paths' => {} }
146
+ current_yaml['paths'] ||= {}
147
+ current_yaml['paths'][current_path] = { request.method.to_s.downcase => {} }
148
+ document_writer.write(current_yaml)
149
+ @@removed_examples << marker
214
150
  end
215
151
 
216
- def schema_data(value)
217
- type = schema_type(value)
218
- hash = { 'type' => type }
219
-
220
- if type == 'object'
221
- # FIX: Removed additionalProperties: true as requested
222
- hash['properties'] = (!value.nil? && value.present?) ? properties_data(value) : {}
223
- end
224
-
225
- hash
226
- end
227
-
228
- def schema_type(value)
229
- return 'integer' if number?(value)
230
- return 'boolean' if (value.try(:downcase) == 'true') || (value.try(:downcase) == 'false')
231
- return 'string' if value.instance_of?(String) || value.instance_of?(Symbol)
232
- return 'array' if value.instance_of?(Array)
233
-
234
- 'object'
235
- end
236
-
237
- def set_complex_parameter(parameters, name, in_type, value, required: false)
238
- hash =
239
- {
240
- 'name' => name.to_s,
241
- 'in' => in_type.to_s,
242
- 'schema' => schema_data(value)
243
- }
244
-
245
- if in_type.to_s == 'query' && hash['schema']['type'] == 'object'
246
- hash['style'] = 'deepObject'
247
- hash['explode'] = true
248
- end
249
-
250
- hash['required'] = required if required
251
-
252
- parameters.push(hash)
253
- end
254
-
255
- def set_individual_parameters(parameters, parameter_set, required: false)
256
- return if parameter_set.blank?
257
-
258
- in_type = parameter_set.keys.first.to_s
259
- params_hash = parameter_set.values.first
260
-
261
- params_hash.each do |key, value|
262
- hash =
263
- {
264
- 'name' => key.to_s,
265
- 'in' => in_type,
266
- 'schema' => schema_data(value),
267
- 'example' => example(value)
268
- }
269
-
270
- hash['required'] = required if required
271
- hash.except!('example') if hash['example'].blank?
272
-
273
- parameters.push(hash)
274
- end
275
- end
276
-
277
- def request_body
278
- content_body(request.request_parameters) if request.request_parameters.present?
152
+ # Used for YAML filename — stable, does not depend on reading the file.
153
+ def file_tag
154
+ ENV['tag'].presence || controller_name&.capitalize || 'api'
279
155
  end
280
156
 
281
157
  def tags
282
- [yaml_file&.dig('paths', current_path, request.method.downcase, 'tags')&.last&.capitalize || ENV['tag'] || controller_name&.capitalize]
158
+ existing = existing_operation&.dig('tags')&.last
159
+ [existing&.capitalize || file_tag]
283
160
  end
284
161
 
285
162
  def summary
286
- yaml_file&.dig('paths', current_path, request.method.downcase, 'summary') || handel_summary(request.method.downcase, current_path)
287
- end
288
-
289
- def handel_summary(method, path)
290
- path_segments = path.split('/')
291
- resource = format_path_to_title(path)&.gsub(' ', ' ')
292
- id_segment = path.include?('{')
293
-
294
- case method.upcase
295
- when 'GET'
296
- id_segment ? "Get #{resource}" : "Get All #{resource&.pluralize}"
297
- when 'POST'
298
- "Create #{resource}"
299
- when 'PUT'
300
- "Update #{resource}"
301
- when 'DELETE'
302
- "Delete #{resource}"
303
- else
304
- ""
305
- end
306
- end
307
-
308
- def format_path_to_title(path)
309
- cleaned_path = path.sub(%r{^/v\d+/}, '')
310
- cleaned_path.gsub!(/\{[^}]+\}/, '')
311
- title = cleaned_path.split('/').map(&:capitalize).join(' ')
312
- title
163
+ existing_operation&.dig('summary') || build_summary(request.method.downcase, current_path)
313
164
  end
314
165
 
315
166
  def current_security
316
- yaml_file&.dig('paths', current_path, request.method.downcase, 'security') || security
167
+ existing_operation&.dig('security') || config.security
317
168
  end
318
169
 
319
- def response_description
320
- response_status[response.status]
321
- end
322
-
323
- def swagger_response
324
- hash = {}
325
- begin
326
- swagger_response = JSON.parse(response.body)
327
- rescue JSON::ParserError
328
- swagger_response = { 'file' => 'file/data' }
329
- end
330
-
331
- hash['description'] = response_description if with_response_description
332
- hash['headers'] = {} # response.headers
333
- hash['content'] = content_json_example(swagger_response)
334
-
335
- {
336
- response.status.to_s => hash
337
- }
338
- end
170
+ def existing_operation
171
+ return @existing_operation if defined?(@existing_operation)
339
172
 
340
- def convert_to_multipart(payload, main_key = nil, index = nil)
341
- payload_keys.push(main_key) if main_key.present?
342
- payload.each do |key, value|
343
- if value.is_a?(Hash)
344
- payload_keys.push(key)
345
- convert_to_multipart(value)
346
- elsif value.is_a?(Array)
347
- value.each_with_index { |v, index| convert_to_multipart(v, key, index) }
348
- else
349
- keys = payload_keys.clone
350
- first_key = keys.shift
351
- if index.present?
352
- keys.each { |inner_key| first_key = "#{first_key}[#{inner_key}][#{index}]" }
353
- else
354
- keys.each { |inner_key| first_key = "#{first_key}[#{inner_key}]" }
355
- end
356
-
357
- first_key = "#{first_key}[#{key}]"
358
- payload_hash.merge!({ first_key => { 'type' => schema_type(value), 'example' => example(value) } })
173
+ @existing_operation =
174
+ if document_writer.exist?
175
+ document_writer.read&.dig('paths', current_path, request.method.downcase)
359
176
  end
360
- end
361
177
  end
362
178
 
363
- def content_form_data(data)
364
- hash_data = {}
365
- data.map do |key, value|
366
- if value.is_a?(Hash)
367
- hash_data.merge!({ key => value })
368
- elsif value.is_a?(Array)
369
- value.each_with_index { |v, index| convert_to_multipart(v, key) }
370
- else
371
- payload_hash.merge!({ key => { 'type' => schema_type(value), 'example' => example(value) } })
372
- end
373
- end
374
-
375
- convert_to_multipart(hash_data)
376
- converted_payload = @payload_hash.clone
377
- @payload_hash = nil
378
- @payload_keys = nil
379
-
380
- {
381
- 'multipart/form-data' => {
382
- 'schema' => {
383
- 'type' => 'object',
384
- 'properties' => converted_payload
385
- }
386
- }
387
- }
388
- end
389
-
390
- def json_to_content_form_data(json)
391
- {
392
- 'multipart/form-data' => {
393
- 'schema' => build_properties(json)
394
- }
395
- }
396
- end
397
-
398
- def build_properties(json)
399
- case json
400
- when Hash
401
- hash_properties = json.transform_values { |value| build_properties(value) if value.present? }
402
- hash_properties = hash_properties.delete_if { |_k, v| !v.present? }
403
-
404
- hashs = {
405
- 'type' => 'object',
406
- 'properties' => hash_properties
407
- }
408
- when Array
409
- item_schemas = json.map { |item| build_properties(item) }
410
- merged_schema = merge_array_schemas(item_schemas)
411
-
412
- if merged_schema[:type] == 'object'
413
- { 'type' => 'array', 'items' => merged_schema }
414
- else
415
- { 'type' => 'array', 'items' => { 'oneOf' => item_schemas.uniq } }
416
- end
417
- when String
418
- if integer?(json)
419
- { 'type' => 'integer', 'example' => json.to_i }
420
- elsif number?(json)
421
- { 'type' => 'number', 'example' => json.to_f }
422
- elsif is_valid_date?(json)
423
- { 'type' => 'string', 'example' => json.to_date.to_s }
424
- else
425
- { 'type' => 'string', 'example' => json.to_s }
426
- end
427
- when Integer
428
- { 'type' => 'integer', 'example' => json }
429
- when Float
430
- { 'type' => 'number', 'example' => json }
431
- when TrueClass, FalseClass
432
- { 'type' => 'boolean', 'example' => json }
433
- when Date, Time, DateTime
434
- { 'type' => 'string', 'example' => json.to_date.to_s }
435
- else
436
- { 'type' => 'string', 'example' => json.to_s }
437
- end
438
- end
439
-
440
- def merge_array_schemas(schemas)
441
- return {} if schemas.empty?
442
- schemas.reduce do |merged, schema|
443
- merge_properties(merged, schema)
444
- end
445
- end
446
-
447
- def merge_properties(old_data, new_data)
448
- return old_data unless old_data.is_a?(Hash) && new_data.is_a?(Hash)
449
-
450
- merged = old_data.dup
451
- new_data.each do |key, value|
452
- merged[key] = if merged[key].is_a?(Hash) && value.is_a?(Hash)
453
- merge_properties(merged[key], value)
454
- else
455
- value
456
- end
457
- end
458
- merged
459
- end
460
-
461
- def content_application_json_schema_properties(data)
462
- hash_data = {}
463
- data.map do |key, value|
464
- if value.is_a?(Hash)
465
- hash_data.merge!({ key => value })
466
- elsif value.is_a?(Array)
467
- value.each_with_index { |v, index| convert_to_multipart(v, key, index) }
468
- else
469
- payload_hash.merge!({ key => { 'type' => schema_type(value), 'example' => example(value) } })
470
- end
471
- end
472
-
473
- convert_to_multipart(hash_data)
474
- converted_payload = @payload_hash.clone
475
- @payload_hash = nil
476
- @payload_keys = nil
477
-
478
- converted_payload
479
- end
480
-
481
- def content_body(data)
482
- hash = {}
483
- hash.merge!(json_to_content_form_data(data))
484
- { 'content' => hash }
485
- end
486
-
487
- def number?(value)
488
- true if Float(value)
489
- rescue StandardError
490
- false
491
- end
492
-
493
- def integer?(value)
494
- true if Integer(value)
495
- rescue StandardError
496
- false
497
- end
498
-
499
- def example(value)
500
- return value.to_i if number?(value)
501
- return convert_to_date(value) if value.instance_of?(String) && is_valid_date?(value)
502
- return value if value.instance_of?(String) || value.instance_of?(Symbol)
503
-
504
- nil
505
- end
506
-
507
- def is_valid_date?(string)
508
- Date.strptime(string)
509
- true
510
- rescue ArgumentError
511
- false
512
- end
513
-
514
- def convert_to_date(string)
515
- datetime = Date.strptime(string)
516
- return Date.parse(string).strftime('%Y/%m/%d') if datetime.year == 1
517
- datetime.strftime('%Y/%m/%d')
518
- rescue ArgumentError
519
- string
520
- end
521
-
522
- # parameters
523
-
524
- def parameters
525
- parameters = []
526
-
527
- # 1. Path Parameters
528
- set_individual_parameters(parameters, path_parameters, required: true)
529
-
530
- # 2. Query Parameters
531
- query_parameters.each do |name, value|
532
- set_complex_parameter(parameters, name.to_s, 'query', value)
533
- end
534
-
535
- # 3. Request Parameters
536
- set_individual_parameters(parameters, request_parameters) if request.request_parameters.blank?
537
-
538
- parameters
539
- end
540
-
541
- def request_parameters
542
- { body: request.request_parameters }
543
- end
544
-
545
- def query_parameters
546
- request.query_parameters
547
- end
548
-
549
- def path_parameters
550
- { path: request.path_parameters.except(:controller, :format, :action) }
551
- end
552
-
553
- # Static
554
-
555
- def paths
556
- @@paths ||= {}
557
- end
558
-
559
- def controller_name
560
- request.params['controller'].split('/').last.to_s
561
- end
562
-
563
- def swagger_location
564
- return @swagger_location if instance_variable_defined?(:@swagger_location)
565
-
566
- if ENV[generate_swagger_environment_variable].present?
567
- directory_path = Rails.root.join(default_path).to_s
568
- FileUtils.mkdir_p(directory_path) unless File.directory?(directory_path)
569
- @swagger_location = "#{directory_path}/#{snake_case(tags&.first)}.yaml"
570
- elsif ENV[swagger_path_environment_variable].include?('.yaml') || ENV[swagger_path_environment_variable].include?('.yml')
571
- @swagger_location = Rails.root.join(ENV.fetch(swagger_path_environment_variable, nil).to_s).to_s
572
- else
573
- directory_path = Rails.root.join(ENV.fetch(swagger_path_environment_variable, nil).to_s).to_s
574
- FileUtils.mkdir_p(directory_path) unless File.directory?(directory_path)
575
- @swagger_location = "#{directory_path}/#{snake_case(tags&.first)}.yaml"
576
- end
577
- end
578
-
579
- def content_json(data)
580
- {
581
- 'application/json' => {
582
- 'schema' => { 'type' => 'object' },
583
- 'example' => data
584
- }
585
- }
586
- end
587
-
588
- def content_json_example(data)
589
- example_title = full_rspec_description.present? ? full_rspec_description : 'example-0'
590
-
591
- {
592
- 'application/json' => {
593
- 'schema' => { 'type' => 'object' },
594
- 'examples' => {
595
- example_title => {
596
- 'value' => data
597
- }
598
- }
599
- }
600
- }
601
- end
602
-
603
- def example_description
604
- body_ = request_parameters.values.first.present? ? { 'body_params' => request_parameters.values.first&.as_json }: nil
605
- query_ = query_parameters.present? ? { 'query_params' => query_parameters&.as_json } : nil
606
- path_ = path_parameters.values.first.present? ? { 'path_params' => path_parameters.values.first&.as_json }: nil
607
-
608
- [path_, query_, body_].
609
- compact.
610
- to_s.
611
- gsub('-', '/').
612
- gsub("'", '').
613
- gsub("=>", ': ')
614
- end
615
-
616
- def json_example_plus_one(string)
617
- if string =~ /(\d+)$/
618
- modified_numeric_part = $1.to_i + 1
619
- string.sub(/(\d+)$/, modified_numeric_part.to_s)
620
- else
621
- string
622
- end
623
- end
624
-
625
- def payload_keys
626
- @payload_keys ||= []
627
- end
628
-
629
- def payload_hash
630
- @payload_hash ||= {}
631
- end
632
-
633
- def new_example(example_title, current_example, old_examples, all_paths = yaml_file['paths'], with_schema_properties = false)
634
- if with_multiple_examples || old_examples&.keys&.count.to_i <= 1
635
- if !old_examples&.value?(current_example)
636
- last_example = handel_name_last_example(old_examples)
637
- last_example ||= example_title
638
- last_example = example_title
639
- hash = { 'examples' => { last_example => current_example } }
640
- all_paths[current_path][request.method.downcase]['responses'][response.status.to_s]['content']['application/json'].deep_merge!(hash)
641
- add_properties_to_schema(last_example, all_paths[current_path])
642
- elsif with_schema_properties
643
- add_properties_to_schema(full_rspec_description.present? ? full_rspec_description : 'example-0', all_paths[current_path])
644
- end
645
- end
646
-
647
- true
648
- end
649
-
650
- def handel_name_last_example(old_examples)
651
- last_example = full_rspec_description || old_examples.keys.last
652
- if old_examples&.keys&.include?(last_example)
653
- last_example += '-1'
654
- json_example_plus_one(last_example)
655
- end
656
- end
657
-
658
- def add_properties_to_schema(last_example, main_path = yaml_file['paths'][current_path])
659
- if with_payload_properties
660
- parameters = {}
661
- parameters.merge!(request_parameters.values.first, query_parameters, path_parameters.values.first)
662
- hash = {
663
- last_example => build_properties(parameters.as_json)
664
- }
665
-
666
- main_path[request.method.downcase]['responses'][response.status.to_s].deep_merge!(
667
- {
668
- 'content' => {
669
- 'application/json' => {
670
- 'schema' => {
671
- 'description' => 'These are the payloads for each example',
672
- 'type' => 'object',
673
- 'properties' => hash
674
- }
675
- }
676
- }
677
- }
678
- )
679
- end
680
- end
681
-
682
- # FIX: Run check_parameters and check_parameter sequentially so explicit logic runs
683
- def apply_yaml_file_changes
684
- check_path
685
- check_method
686
- check_status
687
- check_parameters
688
- check_parameter
689
- check_request_bodys
690
- check_request_body
691
- end
692
-
693
- def old_paths
694
- yaml_file['paths']
695
- end
696
-
697
- # checks
698
-
699
- def organize_result(current_paths)
700
- if current_paths.dig(current_path, request.method.downcase).present?
701
- new_hash = {
702
- 'tags' => tags,
703
- 'summary' => summary
704
- }
705
-
706
- new_hash['parameters'] = current_paths[current_path][request.method.downcase]['parameters'] if current_paths.dig(current_path, request.method.downcase, 'parameters')
707
- new_hash['requestBody'] = current_paths[current_path][request.method.downcase]['requestBody'] if current_paths.dig(current_path, request.method.downcase, 'requestBody')
708
- new_hash['responses'] = current_paths[current_path][request.method.downcase]['responses']
709
- new_hash['security'] = current_security
710
-
711
- current_paths[current_path][request.method.downcase] = new_hash
712
- end
713
- end
714
-
715
- def check_path
716
- unless old_paths.key?(current_path)
717
- yaml_file['paths'].merge!({ current_path => paths[current_path] })
718
- update_example_title(true)
719
- end
720
- end
721
-
722
- def check_method
723
- unless old_paths[current_path].key?(request.method.downcase)
724
- yaml_file['paths'][current_path][request.method.downcase] = { 'responses' => swagger_response }
725
- update_example_title(true)
726
- end
727
- end
728
-
729
- def check_status
730
- example_title = full_rspec_description.present? ? full_rspec_description : 'example-0'
731
- if old_paths[current_path][request.method.downcase]['responses'].present?
732
- if old_paths[current_path][request.method.downcase]['responses']&.key?(response.status.to_s)
733
- update_example_title
734
- else
735
- yaml_file['paths'][current_path][request.method.downcase]['responses'].merge!(swagger_response)
736
- update_example_title(true)
737
- end
738
- else
739
- yaml_file['paths'][current_path][request.method.downcase]['responses'] = swagger_response
740
- update_example_title
741
- end
742
- end
743
-
744
- # FIX: Stop overwriting parameters. Only initialize if missing.
745
- def check_parameters
746
- if old_paths[current_path][request.method.downcase]['parameters'].nil?
747
- yaml_file['paths'][current_path][request.method.downcase]['parameters'] = []
748
- end
749
- end
750
-
751
- # FIX: Merge logic for schemas
752
- def check_parameter
753
- current_params = paths[current_path][request.method.downcase]['parameters'] || []
754
-
755
- # Ensure existing parameters list exists
756
- yaml_file['paths'][current_path][request.method.downcase]['parameters'] ||= []
757
- existing_params_list = yaml_file['paths'][current_path][request.method.downcase]['parameters']
758
-
759
- current_params.each do |param|
760
- # Check if parameter with same name AND 'in' location exists
761
- existing_param = existing_params_list.find { |p| p['name'] == param['name'] && p['in'] == param['in'] }
762
-
763
- if existing_param
764
- # If it exists, merge the schemas (accumulate properties)
765
- merge_param_schemas(existing_param, param)
766
- else
767
- # If it is new, just add it
768
- existing_params_list.push(param)
769
- end
770
- end
771
- end
772
-
773
- # Helper method to merge schema properties of object parameters
774
- def merge_param_schemas(existing, new_param)
775
- # We only merge if both are objects and have schemas
776
- if existing.dig('schema', 'type') == 'object' && new_param.dig('schema', 'type') == 'object'
777
- existing_props = existing.dig('schema', 'properties') || {}
778
- new_props = new_param.dig('schema', 'properties') || {}
779
-
780
- # Merge new properties into existing ones (Accumulate keys)
781
- existing['schema']['properties'] = existing_props.merge(new_props)
782
- end
783
- end
179
+ def build_summary(method, path)
180
+ resource = Helpers.format_path_to_title(path)
181
+ id_segment = path.include?('{')
784
182
 
785
- def check_request_bodys
786
- if paths[current_path][request.method.downcase]['requestBody'].present? && old_paths[current_path][request.method.downcase]['requestBody'].nil?
787
- yaml_file['paths'][current_path][request.method.downcase]['requestBody'] = paths[current_path][request.method.downcase]['requestBody']
183
+ case method.upcase
184
+ when 'GET' then id_segment ? "Get #{resource}" : "Get All #{resource&.pluralize}"
185
+ when 'POST' then "Create #{resource}"
186
+ when 'PUT', 'PATCH' then "Update #{resource}"
187
+ when 'DELETE' then "Delete #{resource}"
188
+ else ''
788
189
  end
789
190
  end
790
191
 
791
- def check_request_body
792
- if paths[current_path][request.method.downcase]['requestBody'].present?
793
- param_current_hash = paths[current_path][request.method.downcase]['requestBody']['content']['multipart/form-data']['schema']['properties']
794
- param_current_file = yaml_file['paths'][current_path][request.method.downcase]['requestBody']['content']['multipart/form-data']['schema']['properties']
795
- if param_current_hash.present? && param_current_file.present?
796
- param_names = param_current_hash.keys - param_current_file.keys
797
- param_names.each do |param_name|
798
- param = paths[current_path][request.method.downcase]['requestBody']['content']['multipart/form-data']['schema']['properties'].select { |parameter| parameter == param_name }
799
- yaml_file['paths'][current_path][request.method.downcase]['requestBody']['content']['multipart/form-data']['schema']['properties'].merge!(param)
800
- end
801
- end
802
- end
192
+ def swagger_response
193
+ @swagger_response ||= ResponseBuilder.new(
194
+ response,
195
+ config: config,
196
+ example_title: example_title
197
+ ).build
803
198
  end
804
199
 
805
- def update_example_title(with_schema_properties = false)
806
- example_title = full_rspec_description.present? ? full_rspec_description : 'example-0'
807
- current_example = swagger_response[response.status.to_s]['content']['application/json']['examples'][example_title]
808
- old_examples = old_paths[current_path][request.method.downcase]['responses'][response.status.to_s]['content']['application/json']['examples']
809
- new_example(example_title, current_example, old_examples, yaml_file['paths'], with_schema_properties)
200
+ def example_title
201
+ full_rspec_description.presence || 'example-0'
810
202
  end
811
203
 
812
204
  def full_rspec_description
813
- with_rspec_examples ? SwaggerAutogenerate::SwaggerTrace.rspec_description : nil
205
+ config.with_rspec_examples ? self.class.rspec_description : nil
814
206
  end
815
207
 
816
- def create_file_if_not_exist
817
- create_file unless File.exist?(swagger_location)
208
+ def controller_name
209
+ request.params['controller'].to_s.split('/').last
818
210
  end
819
211
 
820
- def snake_case(text)
821
- return text&.downcase if text&.match?(/\A[A-Z]+\z/)
212
+ def deep_dup(value)
213
+ return value.deep_dup if value.respond_to?(:deep_dup)
822
214
 
823
- text
824
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
825
- .gsub(/([a-z])([A-Z])/, '\1_\2')
826
- .downcase
827
- .tr(' ', '_')
828
- end
829
-
830
- class << self
831
- attr_accessor :rspec_description
215
+ Marshal.load(Marshal.dump(value))
832
216
  end
833
217
  end
834
218
  end