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.
@@ -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