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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -20
- data/README.md +165 -49
- data/examples/swagger_autogenerate.rb +28 -0
- data/lib/swagger_autogenerate/configuration.rb +110 -33
- data/lib/swagger_autogenerate/document_writer.rb +56 -0
- data/lib/swagger_autogenerate/helpers.rb +118 -0
- data/lib/swagger_autogenerate/parameter_builder.rb +87 -0
- data/lib/swagger_autogenerate/path_normalizer.rb +22 -0
- data/lib/swagger_autogenerate/railtie.rb +36 -0
- data/lib/swagger_autogenerate/response_builder.rb +46 -0
- data/lib/swagger_autogenerate/schema_builder.rb +86 -0
- data/lib/swagger_autogenerate/swagger_trace.rb +134 -750
- data/lib/swagger_autogenerate/version.rb +1 -1
- data/lib/swagger_autogenerate/yaml_merger.rb +198 -0
- data/lib/swagger_autogenerate.rb +33 -15
- data/sig/swagger_autogenerate.rbs +23 -0
- metadata +60 -16
- data/Gemfile +0 -8
- data/Gemfile.lock +0 -20
- data/Rakefile +0 -4
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/lib/config/initializers/swagger_autogenerate.rb +0 -5
- data/swagger_autogenerate.gemspec +0 -44
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
self.class.environment_name
|
|
48
58
|
end
|
|
49
59
|
|
|
50
60
|
private
|
|
51
61
|
|
|
52
|
-
attr_reader :request, :response, :
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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!(
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
193
|
-
|
|
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
|
-
|
|
206
|
-
|
|
141
|
+
document_writer.ensure_exists!
|
|
142
|
+
marker = { current_path => request.method.to_s.downcase }
|
|
143
|
+
return if @@removed_examples.include?(marker)
|
|
207
144
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
158
|
+
existing = existing_operation&.dig('tags')&.last
|
|
159
|
+
[existing&.capitalize || file_tag]
|
|
283
160
|
end
|
|
284
161
|
|
|
285
162
|
def summary
|
|
286
|
-
|
|
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
|
-
|
|
167
|
+
existing_operation&.dig('security') || config.security
|
|
317
168
|
end
|
|
318
169
|
|
|
319
|
-
def
|
|
320
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
|
806
|
-
|
|
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 ?
|
|
205
|
+
config.with_rspec_examples ? self.class.rspec_description : nil
|
|
814
206
|
end
|
|
815
207
|
|
|
816
|
-
def
|
|
817
|
-
|
|
208
|
+
def controller_name
|
|
209
|
+
request.params['controller'].to_s.split('/').last
|
|
818
210
|
end
|
|
819
211
|
|
|
820
|
-
def
|
|
821
|
-
return
|
|
212
|
+
def deep_dup(value)
|
|
213
|
+
return value.deep_dup if value.respond_to?(:deep_dup)
|
|
822
214
|
|
|
823
|
-
|
|
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
|