grape-swagger 0.33.0 → 0.34.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -7
  3. data/.rubocop_todo.yml +0 -6
  4. data/.travis.yml +8 -9
  5. data/CHANGELOG.md +67 -5
  6. data/Gemfile +4 -4
  7. data/README.md +66 -3
  8. data/grape-swagger.gemspec +1 -1
  9. data/lib/grape-swagger.rb +1 -1
  10. data/lib/grape-swagger/doc_methods.rb +2 -0
  11. data/lib/grape-swagger/doc_methods/build_model_definition.rb +0 -17
  12. data/lib/grape-swagger/doc_methods/extensions.rb +6 -1
  13. data/lib/grape-swagger/doc_methods/format_data.rb +51 -0
  14. data/lib/grape-swagger/doc_methods/move_params.rb +22 -49
  15. data/lib/grape-swagger/doc_methods/parse_params.rb +6 -0
  16. data/lib/grape-swagger/endpoint.rb +32 -13
  17. data/lib/grape-swagger/endpoint/params_parser.rb +10 -17
  18. data/lib/grape-swagger/version.rb +1 -1
  19. data/spec/issues/751_deeply_nested_objects_spec.rb +190 -0
  20. data/spec/lib/endpoint/params_parser_spec.rb +44 -20
  21. data/spec/lib/endpoint_spec.rb +3 -3
  22. data/spec/lib/extensions_spec.rb +10 -0
  23. data/spec/lib/format_data_spec.rb +91 -0
  24. data/spec/lib/move_params_spec.rb +4 -266
  25. data/spec/lib/optional_object_spec.rb +0 -1
  26. data/spec/spec_helper.rb +1 -1
  27. data/spec/swagger_v2/api_swagger_v2_hash_and_array_spec.rb +3 -1
  28. data/spec/swagger_v2/api_swagger_v2_response_with_root_spec.rb +153 -0
  29. data/spec/swagger_v2/description_not_initialized_spec.rb +39 -0
  30. data/spec/swagger_v2/endpoint_versioned_path_spec.rb +33 -0
  31. data/spec/swagger_v2/mounted_target_class_spec.rb +1 -1
  32. data/spec/swagger_v2/namespace_tags_prefix_spec.rb +15 -1
  33. data/spec/swagger_v2/params_array_spec.rb +2 -2
  34. data/spec/swagger_v2/parent_less_namespace_spec.rb +32 -0
  35. data/spec/swagger_v2/{reference_entity.rb → reference_entity_spec.rb} +17 -10
  36. metadata +22 -9
  37. data/spec/swagger_v2/description_not_initialized.rb +0 -39
  38. data/spec/swagger_v2/parent_less_namespace.rb +0 -49
data/README.md CHANGED
@@ -104,9 +104,9 @@ Also added support for [representable](https://github.com/apotonick/representabl
104
104
 
105
105
  ```ruby
106
106
  # For Grape::Entity ( https://github.com/ruby-grape/grape-entity )
107
- gem 'grape-swagger-entity'
107
+ gem 'grape-swagger-entity', '~> 0.3'
108
108
  # For representable ( https://github.com/apotonick/representable )
109
- gem 'grape-swagger-representable'
109
+ gem 'grape-swagger-representable', '~> 0.2'
110
110
  ```
111
111
 
112
112
  If you are not using Rails, make sure to load the parser inside your application initialization logic, e.g., via `require 'grape-swagger/entity'` or `require 'grape-swagger/representable'`.
@@ -189,6 +189,7 @@ end
189
189
  * [base_path](#base_path)
190
190
  * [mount_path](#mount_path)
191
191
  * [add_base_path](#add_base_path)
192
+ * [add_root](#add_root)
192
193
  * [add_version](#add_version)
193
194
  * [doc_version](#doc_version)
194
195
  * [endpoint_auth_wrapper](#endpoint_auth_wrapper)
@@ -248,6 +249,13 @@ add_swagger_documentation \
248
249
  add_base_path: true # only if base_path given
249
250
  ```
250
251
 
252
+ #### add_root: <a name="add_root"></a>
253
+ Add root element to all the responses, default is: `false`.
254
+ ```ruby
255
+ add_swagger_documentation \
256
+ add_root: true
257
+ ```
258
+
251
259
  #### add_version: <a name="add_version"></a>
252
260
 
253
261
  Add `version` key to the documented path keys, default is: `true`,
@@ -447,6 +455,7 @@ add_swagger_documentation \
447
455
  * [Extensions](#extensions)
448
456
  * [Response examples documentation](#response-examples)
449
457
  * [Response headers documentation](#response-headers)
458
+ * [Adding root element to responses](#response-root)
450
459
 
451
460
  #### Swagger Header Parameters <a name="headers"></a>
452
461
 
@@ -913,7 +922,7 @@ desc 'Attach a field to an entity through a PUT',
913
922
  failure: [
914
923
  { code: 400, message: 'Bad request' },
915
924
  { code: 404, message: 'Not found' }
916
- ]
925
+ ]
917
926
  put do
918
927
  # your code comes here
919
928
  end
@@ -1180,6 +1189,60 @@ The result will look like following:
1180
1189
 
1181
1190
  Failure information can be passed as an array of arrays or an array of hashes.
1182
1191
 
1192
+ #### Adding root element to responses <a name="response-root"></a>
1193
+
1194
+ You can specify a custom root element for a successful response:
1195
+
1196
+ ```ruby
1197
+ route_setting :swagger, root: 'cute_kitten'
1198
+ desc 'Get a kitten' do
1199
+ http_codes [{ code: 200, model: Entities::Kitten }]
1200
+ end
1201
+ get '/kittens/:id' do
1202
+ end
1203
+ ```
1204
+
1205
+ The result will look like following:
1206
+
1207
+ ```
1208
+ "responses": {
1209
+ "200": {
1210
+ "description": "Get a kitten",
1211
+ "schema": {
1212
+ "type": "object",
1213
+ "properties": { "cute_kitten": { "$ref": "#/definitions/Kitten" } }
1214
+ }
1215
+ }
1216
+ }
1217
+ ```
1218
+
1219
+ If you specify `true`, the value of the root element will be deduced based on the model name.
1220
+ E.g. in the following example the root element will be "kittens":
1221
+
1222
+ ```ruby
1223
+ route_setting :swagger, root: true
1224
+ desc 'Get kittens' do
1225
+ is_array true
1226
+ http_codes [{ code: 200, model: Entities::Kitten }]
1227
+ end
1228
+ get '/kittens' do
1229
+ end
1230
+ ```
1231
+
1232
+ The result will look like following:
1233
+
1234
+ ```
1235
+ "responses": {
1236
+ "200": {
1237
+ "description": "Get kittens",
1238
+ "schema": {
1239
+ "type": "object",
1240
+ "properties": { "type": "array", "items": { "kittens": { "$ref": "#/definitions/Kitten" } } }
1241
+ }
1242
+ }
1243
+ }
1244
+ ```
1245
+
1183
1246
  ## Using Grape Entities <a name="grape-entity"></a>
1184
1247
 
1185
1248
  Add the [grape-entity](https://github.com/ruby-grape/grape-entity) and [grape-swagger-entity](https://github.com/ruby-grape/grape-swagger-entity) gem to your Gemfile.
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
14
14
  s.license = 'MIT'
15
15
 
16
16
  s.required_ruby_version = '>= 2.4'
17
- s.add_runtime_dependency 'grape', '>= 0.16.2'
17
+ s.add_runtime_dependency 'grape', '>= 0.16.2', '<= 1.2.5'
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
@@ -26,7 +26,7 @@ module SwaggerRouting
26
26
  def combine_routes(app, doc_klass)
27
27
  app.routes.each do |route|
28
28
  route_path = route.path
29
- route_match = route_path.split(/^.*?#{route.prefix.to_s}/).last
29
+ route_match = route_path.split(/^.*?#{route.prefix}/).last
30
30
  next unless route_match
31
31
 
32
32
  route_match = route_match.match('\/([\w|-]*?)[\.\/\(]') || route_match.match('\/([\w|-]*)$')
@@ -4,6 +4,7 @@ require 'grape-swagger/doc_methods/status_codes'
4
4
  require 'grape-swagger/doc_methods/produces_consumes'
5
5
  require 'grape-swagger/doc_methods/data_type'
6
6
  require 'grape-swagger/doc_methods/extensions'
7
+ require 'grape-swagger/doc_methods/format_data'
7
8
  require 'grape-swagger/doc_methods/operation_id'
8
9
  require 'grape-swagger/doc_methods/optional_object'
9
10
  require 'grape-swagger/doc_methods/path_string'
@@ -102,6 +103,7 @@ module GrapeSwagger
102
103
  base_path: nil,
103
104
  add_base_path: false,
104
105
  add_version: true,
106
+ add_root: false,
105
107
  hide_documentation_path: true,
106
108
  format: :json,
107
109
  authorizations: nil,
@@ -25,27 +25,10 @@ module GrapeSwagger
25
25
 
26
26
  def parse_entity(model)
27
27
  return unless model.respond_to?(:documentation)
28
-
29
- deprecated_workflow_for('grape-swagger-entity')
30
-
31
- model.documentation
32
- .select { |_name, options| options[:required] }
33
- .map { |name, options| options[:as] || name }
34
28
  end
35
29
 
36
30
  def parse_representable(model)
37
31
  return unless model.respond_to?(:map)
38
-
39
- deprecated_workflow_for('grape-swagger-representable')
40
-
41
- model.map
42
- .select { |p| p[:documentation] && p[:documentation][:required] }
43
- .map(&:name)
44
- end
45
-
46
- def deprecated_workflow_for(gem_name)
47
- warn "DEPRECATED: You are using old #{gem_name} version, which doesn't provide " \
48
- "required attributes. To solve this problem, please update #{gem_name}"
49
32
  end
50
33
  end
51
34
  end
@@ -87,7 +87,12 @@ module GrapeSwagger
87
87
  part.select { |x| x == identifier }
88
88
  end
89
89
 
90
- def method
90
+ def method(*args)
91
+ # We're shadowing Object.method(:symbol) here so we provide
92
+ # a compatibility layer for code that introspects the methods
93
+ # of this class
94
+ return super if args.size.positive?
95
+
91
96
  @route.request_method.downcase.to_sym
92
97
  end
93
98
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrapeSwagger
4
+ module DocMethods
5
+ class FormatData
6
+ class << self
7
+ def to_format(parameters)
8
+ parameters.reject { |parameter| parameter[:in] == 'body' }.each do |b|
9
+ related_parameters = parameters.select do |p|
10
+ p[:name] != b[:name] && p[:name].to_s.include?(b[:name].to_s.gsub(/\[\]\z/, '') + '[')
11
+ end
12
+ parameters.reject! { |p| p[:name] == b[:name] } if move_down(b, related_parameters)
13
+ end
14
+ parameters
15
+ end
16
+
17
+ def move_down(parameter, related_parameters)
18
+ case parameter[:type]
19
+ when 'array'
20
+ add_array(parameter, related_parameters)
21
+ unless related_parameters.blank?
22
+ add_braces(parameter, related_parameters) if parameter[:name].match?(/\A.*\[\]\z/)
23
+ return true
24
+ end
25
+ when 'object'
26
+ return true
27
+ end
28
+ false
29
+ end
30
+
31
+ def add_braces(parameter, related_parameters)
32
+ param_name = parameter[:name].gsub(/\A(.*)\[\]\z/, '\1')
33
+ related_parameters.each { |p| p[:name] = p[:name].gsub(param_name, param_name + '[]') }
34
+ end
35
+
36
+ def add_array(parameter, related_parameters)
37
+ related_parameters.each do |p|
38
+ p_type = p[:type] == 'array' ? 'string' : p[:type]
39
+ p[:items] = { type: p_type, format: p[:format], enum: p[:enum], is_array: p[:is_array] }
40
+ p[:items].delete_if { |_k, v| v.nil? }
41
+ p[:type] = 'array'
42
+ p[:is_array] = parameter[:is_array]
43
+ p.delete(:format)
44
+ p.delete(:enum)
45
+ p.delete_if { |_k, v| v.nil? }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -8,7 +8,7 @@ module GrapeSwagger
8
8
  class << self
9
9
  attr_accessor :definitions
10
10
 
11
- def can_be_moved?(params, http_verb)
11
+ def can_be_moved?(http_verb, params)
12
12
  move_methods.include?(http_verb) && includes_body_param?(params)
13
13
  end
14
14
 
@@ -51,22 +51,33 @@ module GrapeSwagger
51
51
 
52
52
  def move_params_to_new(definition, params)
53
53
  params, nested_params = params.partition { |x| !x[:name].to_s.include?('[') }
54
-
55
- unless params.blank?
56
- properties, required = build_properties(params)
57
- add_properties_to_definition(definition, properties, required)
54
+ params.each do |param|
55
+ property = param[:name]
56
+ param_properties, param_required = build_properties([param])
57
+ add_properties_to_definition(definition, param_properties, param_required)
58
+ related_nested_params, nested_params = nested_params.partition { |x| x[:name].start_with?("#{property}[") }
59
+ prepare_nested_names(property, related_nested_params)
60
+
61
+ next if related_nested_params.blank?
62
+
63
+ nested_definition = if should_expose_as_array?([param])
64
+ move_params_to_new(array_type, related_nested_params)
65
+ else
66
+ move_params_to_new(object_type, related_nested_params)
67
+ end
68
+ if definition.key?(:items)
69
+ definition[:items][:properties][property.to_sym].deep_merge!(nested_definition)
70
+ else
71
+ definition[:properties][property.to_sym].deep_merge!(nested_definition)
72
+ end
58
73
  end
59
-
60
- nested_properties = build_nested_properties(nested_params) unless nested_params.blank?
61
- add_properties_to_definition(definition, nested_properties, []) unless nested_params.blank?
74
+ definition
62
75
  end
63
76
 
64
77
  def build_properties(params)
65
78
  properties = {}
66
79
  required = []
67
80
 
68
- prepare_nested_types(params) if should_expose_as_array?(params)
69
-
70
81
  params.each do |param|
71
82
  name = param[:name].to_sym
72
83
 
@@ -103,28 +114,6 @@ module GrapeSwagger
103
114
  end
104
115
  end
105
116
 
106
- def build_nested_properties(params, properties = {})
107
- property = params.bsearch { |x| x[:name].include?('[') }[:name].split('[').first
108
-
109
- nested_params, params = params.partition { |x| x[:name].start_with?("#{property}[") }
110
- prepare_nested_names(property, nested_params)
111
-
112
- recursive_call(properties, property, nested_params) unless nested_params.empty?
113
- build_nested_properties(params, properties) unless params.empty?
114
-
115
- properties
116
- end
117
-
118
- def recursive_call(properties, property, nested_params)
119
- if should_expose_as_array?(nested_params)
120
- properties[property.to_sym] = array_type
121
- move_params_to_new(properties[property.to_sym][:items], nested_params)
122
- else
123
- properties[property.to_sym] = object_type
124
- move_params_to_new(properties[property.to_sym], nested_params)
125
- end
126
- end
127
-
128
117
  def movable_params(params)
129
118
  to_delete = params.each_with_object([]) { |x, memo| memo << x if deletable?(x) }
130
119
  delete_from(params, to_delete)
@@ -177,22 +166,6 @@ module GrapeSwagger
177
166
  { type: 'object', properties: {} }
178
167
  end
179
168
 
180
- def prepare_nested_types(params)
181
- params.each do |param|
182
- next unless param[:items]
183
-
184
- param[:type] = if param[:items][:type] == 'array'
185
- 'string'
186
- elsif param[:items].key?('$ref')
187
- param[:type] = 'object'
188
- else
189
- param[:items][:type]
190
- end
191
- param[:format] = param[:items][:format] if param[:items][:format]
192
- param.delete(:items) if param[:type] != 'object'
193
- end
194
- end
195
-
196
169
  def prepare_nested_names(property, params)
197
170
  params.each { |x| x[:name] = x[:name].sub(property, '').sub('[', '').sub(']', '') }
198
171
  end
@@ -208,7 +181,7 @@ module GrapeSwagger
208
181
  end
209
182
 
210
183
  def property_keys
211
- %i[type format description minimum maximum items enum default]
184
+ %i[type format description minimum maximum items enum default additionalProperties]
212
185
  end
213
186
 
214
187
  def deletable?(param)
@@ -25,6 +25,7 @@ module GrapeSwagger
25
25
  document_default_value(settings) unless value_type[:is_array]
26
26
  document_range_values(settings) unless value_type[:is_array]
27
27
  document_required(settings)
28
+ document_additional_properties(settings)
28
29
 
29
30
  @parsed_param
30
31
  end
@@ -91,6 +92,11 @@ module GrapeSwagger
91
92
  @parsed_param[:collectionFormat] = collection_format if DataType.collections.include?(collection_format)
92
93
  end
93
94
 
95
+ def document_additional_properties(settings)
96
+ additional_properties = settings[:additionalProperties]
97
+ @parsed_param[:additionalProperties] = additional_properties if additional_properties
98
+ end
99
+
94
100
  def param_type(value_type)
95
101
  param_type = value_type[:param_type] || value_type[:in]
96
102
  if value_type[:path].include?("{#{value_type[:param_name]}}")
@@ -78,8 +78,7 @@ module Grape
78
78
  def path_and_definition_objects(namespace_routes, options)
79
79
  @paths = {}
80
80
  @definitions = {}
81
- namespace_routes.each_key do |key|
82
- routes = namespace_routes[key]
81
+ namespace_routes.each_value do |routes|
83
82
  path_item(routes, options)
84
83
  end
85
84
 
@@ -121,7 +120,7 @@ module Grape
121
120
  method[:consumes] = consumes_object(route, options[:format])
122
121
  method[:parameters] = params_object(route, options, path)
123
122
  method[:security] = security_object(route)
124
- method[:responses] = response_object(route)
123
+ method[:responses] = response_object(route, options)
125
124
  method[:tags] = route.options.fetch(:tags, tag_object(route, path))
126
125
  method[:operationId] = GrapeSwagger::DocMethods::OperationId.build(route, path)
127
126
  method[:deprecated] = deprecated_object(route)
@@ -179,22 +178,24 @@ module Grape
179
178
  parameters = partition_params(route, options).map do |param, value|
180
179
  value = { required: false }.merge(value) if value.is_a?(Hash)
181
180
  _, value = default_type([[param, value]]).first if value == ''
182
- if value[:type]
183
- expose_params(value[:type])
184
- elsif value[:documentation]
181
+ if value.dig(:documentation, :type)
185
182
  expose_params(value[:documentation][:type])
183
+ elsif value[:type]
184
+ expose_params(value[:type])
186
185
  end
187
186
  GrapeSwagger::DocMethods::ParseParams.call(param, value, path, route, @definitions)
188
187
  end
189
188
 
190
- if GrapeSwagger::DocMethods::MoveParams.can_be_moved?(parameters, route.request_method)
189
+ if GrapeSwagger::DocMethods::MoveParams.can_be_moved?(route.request_method, parameters)
191
190
  parameters = GrapeSwagger::DocMethods::MoveParams.to_definition(path, parameters, route, @definitions)
192
191
  end
193
192
 
193
+ GrapeSwagger::DocMethods::FormatData.to_format(parameters)
194
+
194
195
  parameters.presence
195
196
  end
196
197
 
197
- def response_object(route)
198
+ def response_object(route, options)
198
199
  codes = http_codes_from_route(route)
199
200
  codes.map! { |x| x.is_a?(Array) ? { code: x[0], message: x[1], model: x[2], examples: x[3], headers: x[4] } : x }
200
201
 
@@ -219,7 +220,7 @@ module Grape
219
220
 
220
221
  @definitions[response_model][:description] = description_object(route)
221
222
 
222
- memo[value[:code]][:schema] = build_reference(route, value, response_model)
223
+ memo[value[:code]][:schema] = build_reference(route, value, response_model, options)
223
224
  memo[value[:code]][:examples] = value[:examples] if value[:examples]
224
225
  end
225
226
  end
@@ -250,20 +251,38 @@ module Grape
250
251
  def tag_object(route, path)
251
252
  version = GrapeSwagger::DocMethods::Version.get(route)
252
253
  version = [version] unless version.is_a?(Array)
253
-
254
+ prefix = route.prefix.to_s.split('/').reject(&:empty?)
254
255
  Array(
255
256
  path.split('{')[0].split('/').reject(&:empty?).delete_if do |i|
256
- i == route.prefix.to_s || version.map(&:to_s).include?(i)
257
+ prefix.include?(i) || version.map(&:to_s).include?(i)
257
258
  end.first
258
259
  ).presence
259
260
  end
260
261
 
261
262
  private
262
263
 
263
- def build_reference(route, value, response_model)
264
+ def build_reference(route, value, response_model, settings)
264
265
  # TODO: proof that the definition exist, if model isn't specified
265
266
  reference = { '$ref' => "#/definitions/#{response_model}" }
266
- route.options[:is_array] && value[:code] < 300 ? { type: 'array', items: reference } : reference
267
+ return reference unless value[:code] < 300
268
+
269
+ reference = { type: 'array', items: reference } if route.options[:is_array]
270
+ build_root(route, reference, response_model, settings)
271
+ end
272
+
273
+ def build_root(route, reference, response_model, settings)
274
+ default_root = response_model.underscore
275
+ default_root = default_root.pluralize if route.options[:is_array]
276
+ case route.settings.dig(:swagger, :root)
277
+ when true
278
+ { type: 'object', properties: { default_root => reference } }
279
+ when false
280
+ reference
281
+ when nil
282
+ settings[:add_root] ? { type: 'object', properties: { default_root => reference } } : reference
283
+ else
284
+ { type: 'object', properties: { route.settings.dig(:swagger, :root) => reference } }
285
+ end
267
286
  end
268
287
 
269
288
  def file_response?(value)