grape-swagger 1.1.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +14 -0
  3. data/.github/workflows/rubocop.yml +26 -0
  4. data/.github/workflows/ruby.yml +32 -0
  5. data/.rubocop.yml +65 -2
  6. data/.rubocop_todo.yml +1 -1
  7. data/CHANGELOG.md +50 -0
  8. data/Gemfile +8 -3
  9. data/README.md +182 -13
  10. data/UPGRADING.md +34 -0
  11. data/grape-swagger.gemspec +4 -4
  12. data/lib/grape-swagger.rb +7 -4
  13. data/lib/grape-swagger/doc_methods.rb +65 -62
  14. data/lib/grape-swagger/doc_methods/build_model_definition.rb +53 -2
  15. data/lib/grape-swagger/doc_methods/data_type.rb +4 -4
  16. data/lib/grape-swagger/doc_methods/format_data.rb +2 -2
  17. data/lib/grape-swagger/doc_methods/operation_id.rb +2 -2
  18. data/lib/grape-swagger/doc_methods/parse_params.rb +20 -4
  19. data/lib/grape-swagger/endpoint.rb +83 -32
  20. data/lib/grape-swagger/errors.rb +2 -0
  21. data/lib/grape-swagger/model_parsers.rb +2 -2
  22. data/lib/grape-swagger/rake/oapi_tasks.rb +2 -0
  23. data/lib/grape-swagger/version.rb +1 -1
  24. data/spec/issues/427_entity_as_string_spec.rb +1 -1
  25. data/spec/issues/430_entity_definitions_spec.rb +7 -5
  26. data/spec/issues/537_enum_values_spec.rb +1 -0
  27. data/spec/issues/776_multiple_presents_spec.rb +59 -0
  28. data/spec/issues/809_utf8_routes_spec.rb +55 -0
  29. data/spec/lib/data_type_spec.rb +12 -0
  30. data/spec/lib/format_data_spec.rb +24 -0
  31. data/spec/lib/move_params_spec.rb +2 -2
  32. data/spec/spec_helper.rb +1 -1
  33. data/spec/support/empty_model_parser.rb +3 -2
  34. data/spec/support/mock_parser.rb +1 -2
  35. data/spec/support/model_parsers/entity_parser.rb +8 -8
  36. data/spec/support/model_parsers/mock_parser.rb +24 -8
  37. data/spec/support/model_parsers/representable_parser.rb +8 -8
  38. data/spec/support/namespace_tags.rb +3 -0
  39. data/spec/swagger_v2/api_swagger_v2_hide_param_spec.rb +1 -1
  40. data/spec/swagger_v2/api_swagger_v2_mounted_spec.rb +1 -0
  41. data/spec/swagger_v2/api_swagger_v2_param_type_body_spec.rb +2 -2
  42. data/spec/swagger_v2/api_swagger_v2_response_with_headers_spec.rb +4 -2
  43. data/spec/swagger_v2/api_swagger_v2_response_with_models_spec.rb +53 -0
  44. data/spec/swagger_v2/api_swagger_v2_spec.rb +1 -0
  45. data/spec/swagger_v2/boolean_params_spec.rb +1 -0
  46. data/spec/swagger_v2/float_api_spec.rb +1 -0
  47. data/spec/swagger_v2/inheritance_and_discriminator_spec.rb +57 -0
  48. data/spec/swagger_v2/namespace_tags_prefix_spec.rb +1 -0
  49. data/spec/swagger_v2/param_multi_type_spec.rb +2 -0
  50. data/spec/swagger_v2/param_type_spec.rb +3 -0
  51. data/spec/swagger_v2/param_values_spec.rb +6 -0
  52. data/spec/swagger_v2/{params_array_collection_fromat_spec.rb → params_array_collection_format_spec.rb} +0 -0
  53. data/spec/swagger_v2/params_example_spec.rb +40 -0
  54. data/spec/swagger_v2/reference_entity_spec.rb +74 -29
  55. data/spec/swagger_v2/security_requirement_spec.rb +2 -2
  56. data/spec/swagger_v2/simple_mounted_api_spec.rb +3 -0
  57. metadata +27 -13
  58. data/.travis.yml +0 -35
data/UPGRADING.md CHANGED
@@ -1,5 +1,39 @@
1
1
  ## Upgrading Grape-swagger
2
2
 
3
+ ### Upgrading to >= 1.4.0
4
+
5
+ - Official support for ruby < 2.5 removed, ruby 2.5 only in testing mode, but no support.
6
+
7
+ ### Upgrading to >= 1.3.0
8
+
9
+ - The model (entity) description no longer comes from the route description. It will have a default value: `<<EntityName>> model`.
10
+
11
+ ### Upgrading to >= 1.2.0
12
+
13
+ - The entity_name class method is now called on parent classes for inherited entities. Now you can do this
14
+
15
+ ```ruby
16
+ module Some::Long::Module
17
+ class Base < Grape::Entity
18
+ # ... other shared logic
19
+ def self.entity_name
20
+ "V2::#{self.to_s.demodulize}"
21
+ end
22
+ end
23
+
24
+ def MyEntity < Base
25
+ # ....
26
+ end
27
+
28
+ def OtherEntity < Base
29
+ # revert back to the default behavior by hiding the method
30
+ private_class_method :entity_name
31
+ end
32
+ end
33
+ ```
34
+
35
+ - Full class name is modified to use `_` separator (e.g. `A_B_C` instead of `A::B::C`).
36
+
3
37
  ### Upgrading to >= 1.1.0
4
38
 
5
39
  Full class name is used for referencing entity by default (e.g. `A::B::C` instead of just `C`). `Entity` and `Entities` suffixes and prefixes are omitted (e.g. if entity name is `Entities::SomeScope::MyFavourite::Entity` only `SomeScope::MyFavourite` will be used).
@@ -7,14 +7,14 @@ Gem::Specification.new do |s|
7
7
  s.name = 'grape-swagger'
8
8
  s.version = GrapeSwagger::VERSION
9
9
  s.platform = Gem::Platform::RUBY
10
- s.authors = ['Tim Vandecasteele']
11
- s.email = ['tim.vandecasteele@gmail.com']
10
+ s.authors = ['LeFnord', 'Tim Vandecasteele']
11
+ s.email = ['pscholz.le@gmail.com', 'tim.vandecasteele@gmail.com']
12
12
  s.homepage = 'https://github.com/ruby-grape/grape-swagger'
13
13
  s.summary = 'Add auto generated documentation to your Grape API that can be displayed with Swagger.'
14
14
  s.license = 'MIT'
15
15
 
16
- s.required_ruby_version = '>= 2.4'
17
- s.add_runtime_dependency 'grape', '~> 1.3.0'
16
+ s.required_ruby_version = '>= 2.5'
17
+ s.add_runtime_dependency 'grape', '~> 1.3'
18
18
 
19
19
  s.files = `git ls-files`.split("\n")
20
20
  s.test_files = `git ls-files -- {test,spec}/*`.split("\n")
data/lib/grape-swagger.rb CHANGED
@@ -29,7 +29,10 @@ module SwaggerRouting
29
29
  route_match = route_path.split(/^.*?#{route.prefix}/).last
30
30
  next unless route_match
31
31
 
32
- route_match = route_match.match('\/([\w|-]*?)[\.\/\(]') || route_match.match('\/([\w|-]*)$')
32
+ # want to match emojis … ;)
33
+ # route_match = route_match
34
+ # .match('\/([\p{Alnum}|\p{Emoji}|\-|\_]*?)[\.\/\(]') || route_match.match('\/([\p{Alpha}|\p{Emoji}|\-|\_]*)$')
35
+ route_match = route_match.match('\/([\p{Alnum}|\-|\_]*?)[\.\/\(]') || route_match.match('\/([\p{Alpha}|\-|\_]*)$')
33
36
  next unless route_match
34
37
 
35
38
  resource = route_match.captures.first
@@ -85,7 +88,7 @@ module SwaggerRouting
85
88
  route_name = name.match(%r{^/?([^/]*).*$})[1]
86
89
  return route_name unless route_name.include? ':'
87
90
 
88
- matches = name.match(/\/[a-z]+/)
91
+ matches = name.match(/\/\p{Alpha}+/)
89
92
  matches.nil? ? route_name : matches[0].delete('/')
90
93
  end
91
94
 
@@ -107,8 +110,8 @@ module SwaggerRouting
107
110
  end
108
111
 
109
112
  module SwaggerDocumentationAdder
110
- attr_accessor :combined_namespaces, :combined_namespace_identifiers
111
- attr_accessor :combined_routes, :combined_namespace_routes
113
+ attr_accessor :combined_namespaces, :combined_namespace_identifiers, :combined_routes, :combined_namespace_routes
114
+
112
115
  include SwaggerRouting
113
116
 
114
117
  def add_swagger_documentation(options = {})
@@ -17,6 +17,61 @@ require 'grape-swagger/doc_methods/version'
17
17
 
18
18
  module GrapeSwagger
19
19
  module DocMethods
20
+ DEFAULTS =
21
+ {
22
+ info: {},
23
+ models: [],
24
+ doc_version: '0.0.1',
25
+ target_class: nil,
26
+ mount_path: '/swagger_doc',
27
+ host: nil,
28
+ base_path: nil,
29
+ add_base_path: false,
30
+ add_version: true,
31
+ add_root: false,
32
+ hide_documentation_path: true,
33
+ format: :json,
34
+ authorizations: nil,
35
+ security_definitions: nil,
36
+ security: nil,
37
+ api_documentation: { desc: 'Swagger compatible API description' },
38
+ specific_api_documentation: { desc: 'Swagger compatible API description for specific API' },
39
+ endpoint_auth_wrapper: nil,
40
+ swagger_endpoint_guard: nil,
41
+ token_owner: nil
42
+ }.freeze
43
+
44
+ FORMATTER_METHOD = %i[format default_format default_error_formatter].freeze
45
+
46
+ def self.output_path_definitions(combi_routes, endpoint, target_class, options)
47
+ output = endpoint.swagger_object(
48
+ target_class,
49
+ endpoint.request,
50
+ options
51
+ )
52
+
53
+ paths, definitions = endpoint.path_and_definition_objects(combi_routes, options)
54
+ tags = tags_from(paths, options)
55
+
56
+ output[:tags] = tags unless tags.empty? || paths.blank?
57
+ output[:paths] = paths unless paths.blank?
58
+ output[:definitions] = definitions unless definitions.blank?
59
+
60
+ output
61
+ end
62
+
63
+ def self.tags_from(paths, options)
64
+ tags = GrapeSwagger::DocMethods::TagNameDescription.build(paths)
65
+
66
+ if options[:tags]
67
+ names = options[:tags].map { |t| t[:name] }
68
+ tags.reject! { |t| names.include?(t[:name]) }
69
+ tags += options[:tags]
70
+ end
71
+
72
+ tags
73
+ end
74
+
20
75
  def hide_documentation_path
21
76
  @@hide_documentation_path
22
77
  end
@@ -26,54 +81,32 @@ module GrapeSwagger
26
81
  end
27
82
 
28
83
  def setup(options)
29
- options = defaults.merge(options)
84
+ options = DEFAULTS.merge(options)
30
85
 
31
86
  # options could be set on #add_swagger_documentation call,
32
87
  # for available options see #defaults
33
88
  target_class = options[:target_class]
34
89
  guard = options[:swagger_endpoint_guard]
35
- formatter = options[:format]
36
90
  api_doc = options[:api_documentation].dup
37
91
  specific_api_doc = options[:specific_api_documentation].dup
38
92
 
39
93
  class_variables_from(options)
40
94
 
41
- if formatter
42
- %i[format default_format default_error_formatter].each do |method|
43
- send(method, formatter)
44
- end
45
- end
95
+ setup_formatter(options[:format])
46
96
 
47
97
  desc api_doc.delete(:desc), api_doc
48
98
 
49
- output_path_definitions = proc do |combi_routes, endpoint|
50
- output = endpoint.swagger_object(
51
- target_class,
52
- endpoint.request,
53
- options
54
- )
55
-
56
- paths, definitions = endpoint.path_and_definition_objects(combi_routes, options)
57
- tags = tags_from(paths, options)
58
-
59
- output[:tags] = tags unless tags.empty? || paths.blank?
60
- output[:paths] = paths unless paths.blank?
61
- output[:definitions] = definitions unless definitions.blank?
62
-
63
- output
64
- end
65
-
66
99
  instance_eval(guard) unless guard.nil?
67
100
 
68
101
  get mount_path do
69
102
  header['Access-Control-Allow-Origin'] = '*'
70
103
  header['Access-Control-Request-Method'] = '*'
71
104
 
72
- output_path_definitions.call(target_class.combined_namespace_routes, self)
105
+ GrapeSwagger::DocMethods
106
+ .output_path_definitions(target_class.combined_namespace_routes, self, target_class, options)
73
107
  end
74
108
 
75
- desc specific_api_doc.delete(:desc), { params:
76
- specific_api_doc.delete(:params) || {} }.merge(specific_api_doc)
109
+ desc specific_api_doc.delete(:desc), { params: specific_api_doc.delete(:params) || {}, **specific_api_doc }
77
110
 
78
111
  params do
79
112
  requires :name, type: String, desc: 'Resource name of mounted API'
@@ -88,51 +121,21 @@ module GrapeSwagger
88
121
  combined_routes = target_class.combined_namespace_routes[params[:name]]
89
122
  error!({ error: 'named resource not exist' }, 400) if combined_routes.nil?
90
123
 
91
- output_path_definitions.call({ params[:name] => combined_routes }, self)
124
+ GrapeSwagger::DocMethods
125
+ .output_path_definitions({ params[:name] => combined_routes }, self, target_class, options)
92
126
  end
93
127
  end
94
128
 
95
- def defaults
96
- {
97
- info: {},
98
- models: [],
99
- doc_version: '0.0.1',
100
- target_class: nil,
101
- mount_path: '/swagger_doc',
102
- host: nil,
103
- base_path: nil,
104
- add_base_path: false,
105
- add_version: true,
106
- add_root: false,
107
- hide_documentation_path: true,
108
- format: :json,
109
- authorizations: nil,
110
- security_definitions: nil,
111
- security: nil,
112
- api_documentation: { desc: 'Swagger compatible API description' },
113
- specific_api_documentation: { desc: 'Swagger compatible API description for specific API' },
114
- endpoint_auth_wrapper: nil,
115
- swagger_endpoint_guard: nil,
116
- token_owner: nil
117
- }
118
- end
119
-
120
129
  def class_variables_from(options)
121
130
  @@mount_path = options[:mount_path]
122
131
  @@class_name = options[:class_name] || options[:mount_path].delete('/')
123
132
  @@hide_documentation_path = options[:hide_documentation_path]
124
133
  end
125
134
 
126
- def tags_from(paths, options)
127
- tags = GrapeSwagger::DocMethods::TagNameDescription.build(paths)
135
+ def setup_formatter(formatter)
136
+ return unless formatter
128
137
 
129
- if options[:tags]
130
- names = options[:tags].map { |t| t[:name] }
131
- tags.reject! { |t| names.include?(t[:name]) }
132
- tags += options[:tags]
133
- end
134
-
135
- tags
138
+ FORMATTER_METHOD.each { |method| send(method, formatter) }
136
139
  end
137
140
  end
138
141
  end
@@ -4,8 +4,8 @@ module GrapeSwagger
4
4
  module DocMethods
5
5
  class BuildModelDefinition
6
6
  class << self
7
- def build(model, properties, required)
8
- definition = { type: 'object', properties: properties }
7
+ def build(model, properties, required, other_def_properties = {})
8
+ definition = { type: 'object', properties: properties }.merge(other_def_properties)
9
9
 
10
10
  if required.nil?
11
11
  required_attrs = required_attributes(model)
@@ -17,6 +17,57 @@ module GrapeSwagger
17
17
  definition
18
18
  end
19
19
 
20
+ def parse_params_from_model(parsed_response, model, model_name)
21
+ if parsed_response.is_a?(Hash) && parsed_response.keys.first == :allOf
22
+ refs_or_models = parsed_response[:allOf]
23
+ parsed = parse_refs_and_models(refs_or_models, model)
24
+
25
+ {
26
+ allOf: parsed
27
+ }
28
+ else
29
+ properties, required = parsed_response
30
+ unless properties&.any?
31
+ raise GrapeSwagger::Errors::SwaggerSpec,
32
+ "Empty model #{model_name}, swagger 2.0 doesn't support empty definitions."
33
+ end
34
+ properties, other_def_properties = parse_properties(properties)
35
+
36
+ build(
37
+ model, properties, required, other_def_properties
38
+ )
39
+ end
40
+ end
41
+
42
+ def parse_properties(properties)
43
+ other_properties = {}
44
+
45
+ discriminator_key, discriminator_value =
46
+ properties.find do |_key, value|
47
+ value[:documentation].try(:[], :is_discriminator)
48
+ end
49
+
50
+ if discriminator_key
51
+ discriminator_value.delete(:documentation)
52
+ properties[discriminator_key] = discriminator_value
53
+
54
+ other_properties[:discriminator] = discriminator_key
55
+ end
56
+
57
+ [properties, other_properties]
58
+ end
59
+
60
+ def parse_refs_and_models(refs_or_models, model)
61
+ refs_or_models.map do |ref_or_models|
62
+ if ref_or_models.is_a?(Hash) && ref_or_models.keys.first == '$ref'
63
+ ref_or_models
64
+ else
65
+ properties, required = ref_or_models
66
+ GrapeSwagger::DocMethods::BuildModelDefinition.build(model, properties, required)
67
+ end
68
+ end
69
+ end
70
+
20
71
  private
21
72
 
22
73
  def required_attributes(model)
@@ -48,14 +48,14 @@ module GrapeSwagger
48
48
  end
49
49
 
50
50
  def parse_entity_name(model)
51
- if model.methods(false).include?(:entity_name)
51
+ if model.respond_to?(:entity_name)
52
52
  model.entity_name
53
53
  elsif model.to_s.end_with?('::Entity', '::Entities')
54
- model.to_s.split('::')[0..-2].join('::')
54
+ model.to_s.split('::')[0..-2].join('_')
55
55
  elsif model.to_s.start_with?('Entity::', 'Entities::', 'Representable::')
56
- model.to_s.split('::')[1..-1].join('::')
56
+ model.to_s.split('::')[1..-1].join('_')
57
57
  else
58
- model.to_s
58
+ model.to_s.split('::').join('_')
59
59
  end
60
60
  end
61
61
 
@@ -7,7 +7,7 @@ module GrapeSwagger
7
7
  def to_format(parameters)
8
8
  parameters.reject { |parameter| parameter[:in] == 'body' }.each do |b|
9
9
  related_parameters = parameters.select do |p|
10
- p[:name] != b[:name] && p[:name].to_s.include?(b[:name].to_s.gsub(/\[\]\z/, '') + '[')
10
+ p[:name] != b[:name] && p[:name].to_s.start_with?("#{b[:name].to_s.gsub(/\[\]\z/, '')}[")
11
11
  end
12
12
  parameters.reject! { |p| p[:name] == b[:name] } if move_down(b, related_parameters)
13
13
  end
@@ -30,7 +30,7 @@ module GrapeSwagger
30
30
 
31
31
  def add_braces(parameter, related_parameters)
32
32
  param_name = parameter[:name].gsub(/\A(.*)\[\]\z/, '\1')
33
- related_parameters.each { |p| p[:name] = p[:name].gsub(param_name, param_name + '[]') }
33
+ related_parameters.each { |p| p[:name] = p[:name].gsub(param_name, "#{param_name}[]") }
34
34
  end
35
35
 
36
36
  def add_array(parameter, related_parameters)
@@ -16,8 +16,8 @@ module GrapeSwagger
16
16
 
17
17
  def manipulate(path)
18
18
  operation = path.split('/').map(&:capitalize).join
19
- operation.gsub!(/\-(\w)/, &:upcase).delete!('-') if operation[/\-(\w)/]
20
- operation.gsub!(/\_(\w)/, &:upcase).delete!('_') if operation.include?('_')
19
+ operation.gsub!(/-(\w)/, &:upcase).delete!('-') if operation[/-(\w)/]
20
+ operation.gsub!(/_(\w)/, &:upcase).delete!('_') if operation.include?('_')
21
21
  operation.gsub!(/\.(\w)/, &:upcase).delete!('.') if operation[/\.(\w)/]
22
22
  if path.include?('{')
23
23
  operation.gsub!(/\{(\w)/, &:upcase)
@@ -27,6 +27,7 @@ module GrapeSwagger
27
27
  document_required(settings)
28
28
  document_additional_properties(settings)
29
29
  document_add_extensions(settings)
30
+ document_example(settings)
30
31
 
31
32
  @parsed_param
32
33
  end
@@ -77,6 +78,19 @@ module GrapeSwagger
77
78
 
78
79
  param_type ||= value_type[:param_type]
79
80
 
81
+ array_items = parse_array_item(
82
+ definitions,
83
+ type,
84
+ value_type
85
+ )
86
+
87
+ @parsed_param[:in] = param_type || 'formData'
88
+ @parsed_param[:items] = array_items
89
+ @parsed_param[:type] = 'array'
90
+ @parsed_param[:collectionFormat] = collection_format if DataType.collections.include?(collection_format)
91
+ end
92
+
93
+ def parse_array_item(definitions, type, value_type)
80
94
  array_items = {}
81
95
  if definitions[value_type[:data_type]]
82
96
  array_items['$ref'] = "#/definitions/#{@parsed_param[:type]}"
@@ -91,10 +105,7 @@ module GrapeSwagger
91
105
 
92
106
  array_items[:default] = value_type[:default] if value_type[:default].present?
93
107
 
94
- @parsed_param[:in] = param_type || 'formData'
95
- @parsed_param[:items] = array_items
96
- @parsed_param[:type] = 'array'
97
- @parsed_param[:collectionFormat] = collection_format if DataType.collections.include?(collection_format)
108
+ array_items
98
109
  end
99
110
 
100
111
  def document_additional_properties(settings)
@@ -102,6 +113,11 @@ module GrapeSwagger
102
113
  @parsed_param[:additionalProperties] = additional_properties if additional_properties
103
114
  end
104
115
 
116
+ def document_example(settings)
117
+ example = settings[:example]
118
+ @parsed_param[:example] = example if example
119
+ end
120
+
105
121
  def param_type(value_type)
106
122
  param_type = value_type[:param_type] || value_type[:in]
107
123
  if value_type[:path].include?("{#{value_type[:param_name]}}")
@@ -11,7 +11,7 @@ module Grape
11
11
 
12
12
  if content_types.empty?
13
13
  formats = [target_class.format, target_class.default_format].compact.uniq
14
- formats = Grape::Formatter.formatters({}).keys if formats.empty?
14
+ formats = Grape::Formatter.formatters(**{}).keys if formats.empty?
15
15
  content_types = Grape::ContentTypes::CONTENT_TYPES.select do |content_type, _mime_type|
16
16
  formats.include? content_type
17
17
  end.values
@@ -78,11 +78,11 @@ module Grape
78
78
  def path_and_definition_objects(namespace_routes, options)
79
79
  @paths = {}
80
80
  @definitions = {}
81
+ add_definitions_from options[:models]
81
82
  namespace_routes.each_value do |routes|
82
83
  path_item(routes, options)
83
84
  end
84
85
 
85
- add_definitions_from options[:models]
86
86
  [@paths, @definitions]
87
87
  end
88
88
 
@@ -175,15 +175,18 @@ module Grape
175
175
  end
176
176
 
177
177
  def params_object(route, options, path)
178
- parameters = partition_params(route, options).map do |param, value|
178
+ parameters = build_request_params(route, options).each_with_object([]) do |(param, value), memo|
179
+ next if hidden_parameter?(value)
180
+
179
181
  value = { required: false }.merge(value) if value.is_a?(Hash)
180
182
  _, value = default_type([[param, value]]).first if value == ''
183
+
181
184
  if value.dig(:documentation, :type)
182
185
  expose_params(value[:documentation][:type])
183
186
  elsif value[:type]
184
187
  expose_params(value[:type])
185
188
  end
186
- GrapeSwagger::DocMethods::ParseParams.call(param, value, path, route, @definitions)
189
+ memo << GrapeSwagger::DocMethods::ParseParams.call(param, value, path, route, @definitions)
187
190
  end
188
191
 
189
192
  if GrapeSwagger::DocMethods::MoveParams.can_be_moved?(route.request_method, parameters)
@@ -196,35 +199,38 @@ module Grape
196
199
  end
197
200
 
198
201
  def response_object(route, options)
199
- codes = http_codes_from_route(route)
200
- codes.map! { |x| x.is_a?(Array) ? { code: x[0], message: x[1], model: x[2], examples: x[3], headers: x[4] } : x }
201
-
202
- codes.each_with_object({}) do |value, memo|
202
+ codes(route).each_with_object({}) do |value, memo|
203
203
  value[:message] ||= ''
204
- memo[value[:code]] = { description: value[:message] }
205
-
204
+ memo[value[:code]] = { description: value[:message] ||= '' } unless memo[value[:code]].present?
206
205
  memo[value[:code]][:headers] = value[:headers] if value[:headers]
207
206
 
208
207
  next build_file_response(memo[value[:code]]) if file_response?(value[:model])
209
208
 
210
- response_model = @item
211
- response_model = expose_params_from_model(value[:model]) if value[:model]
212
-
213
209
  if memo.key?(200) && route.request_method == 'DELETE' && value[:model].nil?
214
210
  memo[204] = memo.delete(200)
215
211
  value[:code] = 204
212
+ next
216
213
  end
217
214
 
218
- next if value[:code] == 204
219
- next unless !response_model.start_with?('Swagger_doc') && (@definitions[response_model] || value[:model])
215
+ # Explicitly request no model with { model: '' }
216
+ next if value[:model] == ''
220
217
 
221
- @definitions[response_model][:description] = description_object(route)
218
+ response_model = value[:model] ? expose_params_from_model(value[:model]) : @item
219
+ next unless @definitions[response_model]
220
+ next if response_model.start_with?('Swagger_doc')
222
221
 
223
- memo[value[:code]][:schema] = build_reference(route, value, response_model, options)
222
+ @definitions[response_model][:description] ||= "#{response_model} model"
223
+ build_memo_schema(memo, route, value, response_model, options)
224
224
  memo[value[:code]][:examples] = value[:examples] if value[:examples]
225
225
  end
226
226
  end
227
227
 
228
+ def codes(route)
229
+ http_codes_from_route(route).map do |x|
230
+ x.is_a?(Array) ? { code: x[0], message: x[1], model: x[2], examples: x[3], headers: x[4] } : x
231
+ end
232
+ end
233
+
228
234
  def success_code?(code)
229
235
  status = code.is_a?(Array) ? code.first : code[:code]
230
236
  status.between?(200, 299)
@@ -250,7 +256,7 @@ module Grape
250
256
 
251
257
  def tag_object(route, path)
252
258
  version = GrapeSwagger::DocMethods::Version.get(route)
253
- version = [version] unless version.is_a?(Array)
259
+ version = Array(version)
254
260
  prefix = route.prefix.to_s.split('/').reject(&:empty?)
255
261
  Array(
256
262
  path.split('{')[0].split('/').reject(&:empty?).delete_if do |i|
@@ -261,15 +267,52 @@ module Grape
261
267
 
262
268
  private
263
269
 
270
+ def build_memo_schema(memo, route, value, response_model, options)
271
+ if memo[value[:code]][:schema] && value[:as]
272
+ memo[value[:code]][:schema][:properties].merge!(build_reference(route, value, response_model, options))
273
+
274
+ if value[:required]
275
+ memo[value[:code]][:schema][:required] ||= []
276
+ memo[value[:code]][:schema][:required] << value[:as].to_s
277
+ end
278
+
279
+ elsif value[:as]
280
+ memo[value[:code]][:schema] = {
281
+ type: :object,
282
+ properties: build_reference(route, value, response_model, options)
283
+ }
284
+ memo[value[:code]][:schema][:required] = [value[:as].to_s] if value[:required]
285
+ else
286
+ memo[value[:code]][:schema] = build_reference(route, value, response_model, options)
287
+ end
288
+ end
289
+
264
290
  def build_reference(route, value, response_model, settings)
265
291
  # TODO: proof that the definition exist, if model isn't specified
266
- reference = { '$ref' => "#/definitions/#{response_model}" }
292
+ reference = if value.key?(:as)
293
+ { value[:as] => build_reference_hash(response_model) }
294
+ else
295
+ build_reference_hash(response_model)
296
+ end
267
297
  return reference unless value[:code] < 300
268
298
 
269
- reference = { type: 'array', items: reference } if route.options[:is_array]
299
+ if value.key?(:as) && value.key?(:is_array)
300
+ reference[value[:as]] = build_reference_array(reference[value[:as]])
301
+ elsif route.options[:is_array]
302
+ reference = build_reference_array(reference)
303
+ end
304
+
270
305
  build_root(route, reference, response_model, settings)
271
306
  end
272
307
 
308
+ def build_reference_hash(response_model)
309
+ { '$ref' => "#/definitions/#{response_model}" }
310
+ end
311
+
312
+ def build_reference_array(reference)
313
+ { type: 'array', items: reference }
314
+ end
315
+
273
316
  def build_root(route, reference, response_model, settings)
274
317
  default_root = response_model.underscore
275
318
  default_root = default_root.pluralize if route.options[:is_array]
@@ -286,23 +329,20 @@ module Grape
286
329
  end
287
330
 
288
331
  def file_response?(value)
289
- value.to_s.casecmp('file').zero? ? true : false
332
+ value.to_s.casecmp('file').zero?
290
333
  end
291
334
 
292
335
  def build_file_response(memo)
293
336
  memo['schema'] = { type: 'file' }
294
337
  end
295
338
 
296
- def partition_params(route, settings)
297
- declared_params = route.settings[:declared_params] if route.settings[:declared_params].present?
339
+ def build_request_params(route, settings)
298
340
  required = merge_params(route)
299
341
  required = GrapeSwagger::DocMethods::Headers.parse(route) + required unless route.headers.nil?
300
342
 
301
343
  default_type(required)
302
344
 
303
- request_params = unless declared_params.nil? && route.headers.nil?
304
- GrapeSwagger::Endpoint::ParamsParser.parse_request_params(required, settings, self)
305
- end || {}
345
+ request_params = GrapeSwagger::Endpoint::ParamsParser.parse_request_params(required, settings, self)
306
346
 
307
347
  request_params.empty? ? required : request_params
308
348
  end
@@ -340,12 +380,10 @@ module Grape
340
380
  parser = GrapeSwagger.model_parsers.find(model)
341
381
  raise GrapeSwagger::Errors::UnregisteredParser, "No parser registered for #{model_name}." unless parser
342
382
 
343
- properties, required = parser.new(model, self).call
344
- unless properties&.any?
345
- raise GrapeSwagger::Errors::SwaggerSpec,
346
- "Empty model #{model_name}, swagger 2.0 doesn't support empty definitions."
347
- end
348
- @definitions[model_name] = GrapeSwagger::DocMethods::BuildModelDefinition.build(model, properties, required)
383
+ parsed_response = parser.new(model, self).call
384
+
385
+ @definitions[model_name] =
386
+ GrapeSwagger::DocMethods::BuildModelDefinition.parse_params_from_model(parsed_response, model, model_name)
349
387
 
350
388
  model_name
351
389
  end
@@ -362,6 +400,16 @@ module Grape
362
400
  options[:token_owner] ? route_hidden.call(send(options[:token_owner].to_sym)) : route_hidden.call
363
401
  end
364
402
 
403
+ def hidden_parameter?(value)
404
+ return false if value[:required]
405
+
406
+ if value.dig(:documentation, :hidden).is_a?(Proc)
407
+ value.dig(:documentation, :hidden).call
408
+ else
409
+ value.dig(:documentation, :hidden)
410
+ end
411
+ end
412
+
365
413
  def success_code_from_entity(route, entity)
366
414
  default_code = GrapeSwagger::DocMethods::StatusCodes.get[route.request_method.downcase.to_sym]
367
415
  if entity.is_a?(Hash)
@@ -370,6 +418,9 @@ module Grape
370
418
  default_code[:message] = entity[:message] || route.description || default_code[:message].sub('{item}', @item)
371
419
  default_code[:examples] = entity[:examples] if entity[:examples]
372
420
  default_code[:headers] = entity[:headers] if entity[:headers]
421
+ default_code[:as] = entity[:as] if entity[:as]
422
+ default_code[:is_array] = entity[:is_array] if entity[:is_array]
423
+ default_code[:required] = entity[:required] if entity[:required]
373
424
  else
374
425
  default_code = GrapeSwagger::DocMethods::StatusCodes.get[route.request_method.downcase.to_sym]
375
426
  default_code[:model] = entity if entity