swagger_autogenerate 1.2.8 → 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 -19
- 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 -691
- 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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwaggerAutogenerate
|
|
4
|
+
module Helpers
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def number?(value)
|
|
8
|
+
Float(value)
|
|
9
|
+
true
|
|
10
|
+
rescue StandardError
|
|
11
|
+
false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def integer?(value)
|
|
15
|
+
Integer(value)
|
|
16
|
+
true
|
|
17
|
+
rescue StandardError
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def valid_date?(string)
|
|
22
|
+
return false unless string.is_a?(String)
|
|
23
|
+
|
|
24
|
+
Date.strptime(string)
|
|
25
|
+
true
|
|
26
|
+
rescue ArgumentError
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def convert_to_date(string)
|
|
31
|
+
datetime = Date.strptime(string)
|
|
32
|
+
return Date.parse(string).strftime('%Y/%m/%d') if datetime.year == 1
|
|
33
|
+
|
|
34
|
+
datetime.strftime('%Y/%m/%d')
|
|
35
|
+
rescue ArgumentError
|
|
36
|
+
string
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def snake_case(text)
|
|
40
|
+
return text&.downcase if text&.match?(/\A[A-Z]+\z/)
|
|
41
|
+
|
|
42
|
+
text
|
|
43
|
+
.to_s
|
|
44
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
45
|
+
.gsub(/([a-z])([A-Z])/, '\1_\2')
|
|
46
|
+
.downcase
|
|
47
|
+
.tr(' ', '_')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def convert_to_hash(obj)
|
|
51
|
+
case obj
|
|
52
|
+
when ActiveSupport::HashWithIndifferentAccess
|
|
53
|
+
obj.to_hash
|
|
54
|
+
when Hash
|
|
55
|
+
obj.transform_values { |value| convert_to_hash(value) }
|
|
56
|
+
when Array
|
|
57
|
+
obj.map { |item| convert_to_hash(item) }
|
|
58
|
+
else
|
|
59
|
+
obj
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reformat_dates_in_hash(data)
|
|
64
|
+
case data
|
|
65
|
+
when Hash
|
|
66
|
+
data.each { |key, value| data[key] = reformat_dates_in_hash(value) }
|
|
67
|
+
when Array
|
|
68
|
+
data.map! { |element| reformat_dates_in_hash(element) }
|
|
69
|
+
when String
|
|
70
|
+
valid_date?(data) ? convert_to_date(data).to_s : data
|
|
71
|
+
else
|
|
72
|
+
data
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def merge_properties(old_data, new_data)
|
|
77
|
+
return old_data unless old_data.is_a?(Hash) && new_data.is_a?(Hash)
|
|
78
|
+
|
|
79
|
+
merged = old_data.dup
|
|
80
|
+
new_data.each do |key, value|
|
|
81
|
+
merged[key] =
|
|
82
|
+
if merged[key].is_a?(Hash) && value.is_a?(Hash)
|
|
83
|
+
merge_properties(merged[key], value)
|
|
84
|
+
else
|
|
85
|
+
value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
merged
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def json_example_plus_one(string)
|
|
92
|
+
if string =~ /(\d+)$/
|
|
93
|
+
string.sub(/(\d+)$/, (::Regexp.last_match(1).to_i + 1).to_s)
|
|
94
|
+
else
|
|
95
|
+
string
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def format_path_to_title(path)
|
|
100
|
+
cleaned_path = path.to_s.sub(%r{^/v\d+/}, '')
|
|
101
|
+
cleaned_path.gsub!(/\{[^}]+\}/, '')
|
|
102
|
+
cleaned_path.split('/').reject(&:empty?).map(&:capitalize).join(' ')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def dump_yaml(data)
|
|
106
|
+
YAML.dump(convert_to_hash(reformat_dates_in_hash(data)))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def load_yaml(path)
|
|
110
|
+
YAML.safe_load(
|
|
111
|
+
File.read(path),
|
|
112
|
+
aliases: true,
|
|
113
|
+
permitted_classes: [Symbol, DateTime, Date, Time, ActiveSupport::HashWithIndifferentAccess],
|
|
114
|
+
permitted_symbols: []
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwaggerAutogenerate
|
|
4
|
+
# Builds OpenAPI parameters and requestBody from a Rails request.
|
|
5
|
+
class ParameterBuilder
|
|
6
|
+
def initialize(request, schema_builder: SchemaBuilder.new)
|
|
7
|
+
@request = request
|
|
8
|
+
@schema = schema_builder
|
|
9
|
+
@payload_keys = []
|
|
10
|
+
@payload_hash = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parameters
|
|
14
|
+
result = []
|
|
15
|
+
push_individual(result, path_parameters, required: true)
|
|
16
|
+
query_parameters.each { |name, value| push_complex(result, name.to_s, 'query', value) }
|
|
17
|
+
push_individual(result, request_parameters) if request.request_parameters.blank?
|
|
18
|
+
result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def request_body
|
|
22
|
+
return if request.request_parameters.blank?
|
|
23
|
+
|
|
24
|
+
{ 'content' => json_to_content_form_data(request.request_parameters) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def path_parameters
|
|
28
|
+
{ path: request.path_parameters.except(:controller, :format, :action) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def query_parameters
|
|
32
|
+
request.query_parameters
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def request_parameters
|
|
36
|
+
{ body: request.request_parameters }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
attr_reader :request, :schema
|
|
42
|
+
|
|
43
|
+
def push_complex(parameters, name, in_type, value, required: false)
|
|
44
|
+
hash = {
|
|
45
|
+
'name' => name.to_s,
|
|
46
|
+
'in' => in_type.to_s,
|
|
47
|
+
'schema' => schema.schema_data(value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if in_type.to_s == 'query' && hash['schema']['type'] == 'object'
|
|
51
|
+
hash['style'] = 'deepObject'
|
|
52
|
+
hash['explode'] = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
hash['required'] = required if required
|
|
56
|
+
parameters.push(hash)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def push_individual(parameters, parameter_set, required: false)
|
|
60
|
+
return if parameter_set.blank?
|
|
61
|
+
|
|
62
|
+
in_type = parameter_set.keys.first.to_s
|
|
63
|
+
params_hash = parameter_set.values.first
|
|
64
|
+
|
|
65
|
+
params_hash.each do |key, value|
|
|
66
|
+
hash = {
|
|
67
|
+
'name' => key.to_s,
|
|
68
|
+
'in' => in_type,
|
|
69
|
+
'schema' => schema.schema_data(value),
|
|
70
|
+
'example' => schema.example(value)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
hash['required'] = required if required
|
|
74
|
+
hash.except!('example') if hash['example'].blank?
|
|
75
|
+
parameters.push(hash)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def json_to_content_form_data(json)
|
|
80
|
+
{
|
|
81
|
+
'multipart/form-data' => {
|
|
82
|
+
'schema' => schema.build_properties(json)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwaggerAutogenerate
|
|
4
|
+
# Turns concrete request paths into OpenAPI path templates.
|
|
5
|
+
# Example: /users/42 -> /users/{id}
|
|
6
|
+
class PathNormalizer
|
|
7
|
+
def self.call(request)
|
|
8
|
+
path = request.path.dup
|
|
9
|
+
|
|
10
|
+
request.path_parameters.except(:controller, :format, :action).each do |key, value|
|
|
11
|
+
path_array = path.split('/')
|
|
12
|
+
index = path_array.rindex(value.to_s)
|
|
13
|
+
next unless index
|
|
14
|
+
|
|
15
|
+
path_array[index] = "{#{key}}"
|
|
16
|
+
path = path_array.join('/')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
path
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwaggerAutogenerate
|
|
4
|
+
class Railtie < ::Rails::Railtie
|
|
5
|
+
initializer 'swagger_autogenerate.configure' do
|
|
6
|
+
config.after_initialize do
|
|
7
|
+
setup_rspec_hook!
|
|
8
|
+
auto_include_controller!
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def setup_rspec_hook!
|
|
15
|
+
return unless defined?(RSpec)
|
|
16
|
+
|
|
17
|
+
RSpec.configure do |rspec|
|
|
18
|
+
rspec.before(:each) do |example|
|
|
19
|
+
next unless SwaggerAutogenerate.generate?
|
|
20
|
+
|
|
21
|
+
SwaggerAutogenerate::SwaggerTrace.rspec_description =
|
|
22
|
+
example&.metadata&.dig(:example_group, :description)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def auto_include_controller!
|
|
28
|
+
return unless SwaggerAutogenerate.configuration.auto_include
|
|
29
|
+
return unless SwaggerAutogenerate.test_environment?
|
|
30
|
+
return unless defined?(ApplicationController)
|
|
31
|
+
return if ApplicationController.included_modules.include?(SwaggerAutogenerate)
|
|
32
|
+
|
|
33
|
+
ApplicationController.include(SwaggerAutogenerate)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwaggerAutogenerate
|
|
4
|
+
# Builds OpenAPI response objects from a Rails response.
|
|
5
|
+
class ResponseBuilder
|
|
6
|
+
def initialize(response, config:, example_title:)
|
|
7
|
+
@response = response
|
|
8
|
+
@config = config
|
|
9
|
+
@example_title = example_title
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build
|
|
13
|
+
body =
|
|
14
|
+
begin
|
|
15
|
+
JSON.parse(response.body)
|
|
16
|
+
rescue JSON::ParserError
|
|
17
|
+
{ 'file' => 'file/data' }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
hash = {
|
|
21
|
+
'headers' => {},
|
|
22
|
+
'content' => content_json_example(body)
|
|
23
|
+
}
|
|
24
|
+
hash['description'] = config.response_status[response.status] if config.with_response_description
|
|
25
|
+
|
|
26
|
+
{ response.status.to_s => hash }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :response, :config, :example_title
|
|
32
|
+
|
|
33
|
+
def content_json_example(data)
|
|
34
|
+
{
|
|
35
|
+
'application/json' => {
|
|
36
|
+
'schema' => { 'type' => 'object' },
|
|
37
|
+
'examples' => {
|
|
38
|
+
example_title => {
|
|
39
|
+
'value' => data
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwaggerAutogenerate
|
|
4
|
+
# Infers OpenAPI schema fragments from Ruby values.
|
|
5
|
+
class SchemaBuilder
|
|
6
|
+
def schema_type(value)
|
|
7
|
+
return 'integer' if Helpers.number?(value)
|
|
8
|
+
return 'boolean' if value.to_s.downcase == 'true' || value.to_s.downcase == 'false'
|
|
9
|
+
return 'string' if value.is_a?(String) || value.is_a?(Symbol)
|
|
10
|
+
return 'array' if value.is_a?(Array)
|
|
11
|
+
|
|
12
|
+
'object'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def example(value)
|
|
16
|
+
return value.to_i if Helpers.number?(value)
|
|
17
|
+
return Helpers.convert_to_date(value) if value.is_a?(String) && Helpers.valid_date?(value)
|
|
18
|
+
return value if value.is_a?(String) || value.is_a?(Symbol)
|
|
19
|
+
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def properties_data(value)
|
|
24
|
+
value.each_with_object({}) do |(key, val), hash|
|
|
25
|
+
hash[key] = { 'type' => schema_type(val), 'example' => Helpers.convert_to_hash(val) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def schema_data(value)
|
|
30
|
+
type = schema_type(value)
|
|
31
|
+
hash = { 'type' => type }
|
|
32
|
+
hash['properties'] = value.present? ? properties_data(value) : {} if type == 'object'
|
|
33
|
+
hash
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_properties(json)
|
|
37
|
+
case json
|
|
38
|
+
when Hash
|
|
39
|
+
hash_properties = json.transform_values { |value| build_properties(value) if value.present? }
|
|
40
|
+
hash_properties = hash_properties.delete_if { |_k, v| v.blank? }
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
'type' => 'object',
|
|
44
|
+
'properties' => hash_properties
|
|
45
|
+
}
|
|
46
|
+
when Array
|
|
47
|
+
item_schemas = json.map { |item| build_properties(item) }
|
|
48
|
+
merged_schema = merge_array_schemas(item_schemas)
|
|
49
|
+
|
|
50
|
+
if merged_schema[:type] == 'object' || merged_schema['type'] == 'object'
|
|
51
|
+
{ 'type' => 'array', 'items' => merged_schema }
|
|
52
|
+
else
|
|
53
|
+
{ 'type' => 'array', 'items' => { 'oneOf' => item_schemas.uniq } }
|
|
54
|
+
end
|
|
55
|
+
when String
|
|
56
|
+
if Helpers.integer?(json)
|
|
57
|
+
{ 'type' => 'integer', 'example' => json.to_i }
|
|
58
|
+
elsif Helpers.number?(json)
|
|
59
|
+
{ 'type' => 'number', 'example' => json.to_f }
|
|
60
|
+
elsif Helpers.valid_date?(json)
|
|
61
|
+
{ 'type' => 'string', 'example' => json.to_date.to_s }
|
|
62
|
+
else
|
|
63
|
+
{ 'type' => 'string', 'example' => json.to_s }
|
|
64
|
+
end
|
|
65
|
+
when Integer
|
|
66
|
+
{ 'type' => 'integer', 'example' => json }
|
|
67
|
+
when Float
|
|
68
|
+
{ 'type' => 'number', 'example' => json }
|
|
69
|
+
when TrueClass, FalseClass
|
|
70
|
+
{ 'type' => 'boolean', 'example' => json }
|
|
71
|
+
when Date, Time, DateTime
|
|
72
|
+
{ 'type' => 'string', 'example' => json.to_date.to_s }
|
|
73
|
+
else
|
|
74
|
+
{ 'type' => 'string', 'example' => json.to_s }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def merge_array_schemas(schemas)
|
|
81
|
+
return {} if schemas.empty?
|
|
82
|
+
|
|
83
|
+
schemas.reduce { |merged, schema| Helpers.merge_properties(merged, schema) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|