grape-swagger 0.33.0 → 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
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)