grape-swagger 0.34.1 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +83 -0
  3. data/.travis.yml +15 -12
  4. data/CHANGELOG.md +44 -4
  5. data/Gemfile +9 -4
  6. data/README.md +123 -11
  7. data/UPGRADING.md +30 -0
  8. data/grape-swagger.gemspec +1 -1
  9. data/lib/grape-swagger.rb +2 -2
  10. data/lib/grape-swagger/doc_methods/build_model_definition.rb +53 -2
  11. data/lib/grape-swagger/doc_methods/data_type.rb +6 -6
  12. data/lib/grape-swagger/doc_methods/operation_id.rb +2 -2
  13. data/lib/grape-swagger/doc_methods/parse_params.rb +19 -4
  14. data/lib/grape-swagger/endpoint.rb +28 -18
  15. data/lib/grape-swagger/endpoint/params_parser.rb +12 -5
  16. data/lib/grape-swagger/rake/oapi_tasks.rb +10 -2
  17. data/lib/grape-swagger/version.rb +1 -1
  18. data/spec/issues/427_entity_as_string_spec.rb +1 -1
  19. data/spec/issues/430_entity_definitions_spec.rb +7 -5
  20. data/spec/issues/784_extensions_on_params_spec.rb +38 -0
  21. data/spec/lib/data_type_spec.rb +14 -2
  22. data/spec/lib/endpoint/params_parser_spec.rb +2 -1
  23. data/spec/lib/endpoint_spec.rb +1 -1
  24. data/spec/lib/oapi_tasks_spec.rb +15 -5
  25. data/spec/support/empty_model_parser.rb +1 -2
  26. data/spec/support/mock_parser.rb +1 -2
  27. data/spec/support/model_parsers/entity_parser.rb +1 -1
  28. data/spec/support/model_parsers/representable_parser.rb +1 -1
  29. data/spec/swagger_v2/api_swagger_v2_hide_param_spec.rb +14 -3
  30. data/spec/swagger_v2/api_swagger_v2_param_type_body_spec.rb +2 -2
  31. data/spec/swagger_v2/boolean_params_spec.rb +1 -1
  32. data/spec/swagger_v2/inheritance_and_discriminator_spec.rb +56 -0
  33. data/spec/swagger_v2/reference_entity_spec.rb +74 -29
  34. data/spec/swagger_v2/security_requirement_spec.rb +2 -2
  35. metadata +15 -17
@@ -1,5 +1,35 @@
1
1
  ## Upgrading Grape-swagger
2
2
 
3
+ ### Upgrading to >= 1.2.0
4
+
5
+ - The entity_name class method is now called on parent classes for inherited entities. Now you can do this
6
+
7
+ ```ruby
8
+ module Some::Long::Module
9
+ class Base < Grape::Entity
10
+ # ... other shared logic
11
+ def self.entity_name
12
+ "V2::#{self.to_s.demodulize}"
13
+ end
14
+ end
15
+
16
+ def MyEntity < Base
17
+ # ....
18
+ end
19
+
20
+ def OtherEntity < Base
21
+ # revert back to the default behavior by hiding the method
22
+ private_class_method :entity_name
23
+ end
24
+ end
25
+ ```
26
+
27
+ - Full class name is modified to use `_` separator (e.g. `A_B_C` instead of `A::B::C`).
28
+
29
+ ### Upgrading to >= 1.1.0
30
+
31
+ 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).
32
+
3
33
  ### Upgrading to >= 0.26.1
4
34
 
5
35
  The format can now be specified,
@@ -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', '< 1.3.0'
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")
@@ -107,8 +107,8 @@ module SwaggerRouting
107
107
  end
108
108
 
109
109
  module SwaggerDocumentationAdder
110
- attr_accessor :combined_namespaces, :combined_namespace_identifiers
111
- attr_accessor :combined_routes, :combined_namespace_routes
110
+ attr_accessor :combined_namespaces, :combined_namespace_identifiers, :combined_routes, :combined_namespace_routes
111
+
112
112
  include SwaggerRouting
113
113
 
114
114
  def add_swagger_documentation(options = {})
@@ -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)
@@ -16,7 +16,7 @@ module GrapeSwagger
16
16
  'object'
17
17
  when 'Rack::Multipart::UploadedFile', 'File'
18
18
  'file'
19
- when 'Virtus::Attribute::Boolean'
19
+ when 'Grape::API::Boolean'
20
20
  'boolean'
21
21
  when 'BigDecimal'
22
22
  'double'
@@ -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('::')[-2]
55
- elsif model.respond_to?(:name)
56
- model.name.demodulize.camelize
54
+ model.to_s.split('::')[0..-2].join('_')
55
+ elsif model.to_s.start_with?('Entity::', 'Entities::', 'Representable::')
56
+ model.to_s.split('::')[1..-1].join('_')
57
57
  else
58
- model.to_s.split('::').last
58
+ model.to_s.split('::').join('_')
59
59
  end
60
60
  end
61
61
 
@@ -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)
@@ -26,6 +26,7 @@ module GrapeSwagger
26
26
  document_range_values(settings) unless value_type[:is_array]
27
27
  document_required(settings)
28
28
  document_additional_properties(settings)
29
+ document_add_extensions(settings)
29
30
 
30
31
  @parsed_param
31
32
  end
@@ -62,6 +63,10 @@ module GrapeSwagger
62
63
  @parsed_param[:format] = settings[:format] if settings[:format].present?
63
64
  end
64
65
 
66
+ def document_add_extensions(settings)
67
+ GrapeSwagger::DocMethods::Extensions.add_extensions_to_root(settings, @parsed_param)
68
+ end
69
+
65
70
  def document_array_param(value_type, definitions)
66
71
  if value_type[:documentation].present?
67
72
  param_type = value_type[:documentation][:param_type]
@@ -72,6 +77,19 @@ module GrapeSwagger
72
77
 
73
78
  param_type ||= value_type[:param_type]
74
79
 
80
+ array_items = parse_array_item(
81
+ definitions,
82
+ type,
83
+ value_type
84
+ )
85
+
86
+ @parsed_param[:in] = param_type || 'formData'
87
+ @parsed_param[:items] = array_items
88
+ @parsed_param[:type] = 'array'
89
+ @parsed_param[:collectionFormat] = collection_format if DataType.collections.include?(collection_format)
90
+ end
91
+
92
+ def parse_array_item(definitions, type, value_type)
75
93
  array_items = {}
76
94
  if definitions[value_type[:data_type]]
77
95
  array_items['$ref'] = "#/definitions/#{@parsed_param[:type]}"
@@ -86,10 +104,7 @@ module GrapeSwagger
86
104
 
87
105
  array_items[:default] = value_type[:default] if value_type[:default].present?
88
106
 
89
- @parsed_param[:in] = param_type || 'formData'
90
- @parsed_param[:items] = array_items
91
- @parsed_param[:type] = 'array'
92
- @parsed_param[:collectionFormat] = collection_format if DataType.collections.include?(collection_format)
107
+ array_items
93
108
  end
94
109
 
95
110
  def document_additional_properties(settings)
@@ -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,10 +199,7 @@ 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
204
  memo[value[:code]] = { description: value[:message] }
205
205
 
@@ -225,6 +225,12 @@ module Grape
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|
@@ -293,16 +299,13 @@ module Grape
293
299
  memo['schema'] = { type: 'file' }
294
300
  end
295
301
 
296
- def partition_params(route, settings)
297
- declared_params = route.settings[:declared_params] if route.settings[:declared_params].present?
302
+ def build_request_params(route, settings)
298
303
  required = merge_params(route)
299
304
  required = GrapeSwagger::DocMethods::Headers.parse(route) + required unless route.headers.nil?
300
305
 
301
306
  default_type(required)
302
307
 
303
- request_params = unless declared_params.nil? && route.headers.nil?
304
- GrapeSwagger::Endpoint::ParamsParser.parse_request_params(required, settings)
305
- end || {}
308
+ request_params = GrapeSwagger::Endpoint::ParamsParser.parse_request_params(required, settings, self)
306
309
 
307
310
  request_params.empty? ? required : request_params
308
311
  end
@@ -340,13 +343,10 @@ module Grape
340
343
  parser = GrapeSwagger.model_parsers.find(model)
341
344
  raise GrapeSwagger::Errors::UnregisteredParser, "No parser registered for #{model_name}." unless parser
342
345
 
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
346
+ parsed_response = parser.new(model, self).call
348
347
 
349
- @definitions[model_name] = GrapeSwagger::DocMethods::BuildModelDefinition.build(model, properties, required)
348
+ @definitions[model_name] =
349
+ GrapeSwagger::DocMethods::BuildModelDefinition.parse_params_from_model(parsed_response, model, model_name)
350
350
 
351
351
  model_name
352
352
  end
@@ -363,6 +363,16 @@ module Grape
363
363
  options[:token_owner] ? route_hidden.call(send(options[:token_owner].to_sym)) : route_hidden.call
364
364
  end
365
365
 
366
+ def hidden_parameter?(value)
367
+ return false if value.dig(:required)
368
+
369
+ if value.dig(:documentation, :hidden).is_a?(Proc)
370
+ value.dig(:documentation, :hidden).call
371
+ else
372
+ value.dig(:documentation, :hidden)
373
+ end
374
+ end
375
+
366
376
  def success_code_from_entity(route, entity)
367
377
  default_code = GrapeSwagger::DocMethods::StatusCodes.get[route.request_method.downcase.to_sym]
368
378
  if entity.is_a?(Hash)
@@ -3,15 +3,16 @@
3
3
  module GrapeSwagger
4
4
  module Endpoint
5
5
  class ParamsParser
6
- attr_reader :params, :settings
6
+ attr_reader :params, :settings, :endpoint
7
7
 
8
- def self.parse_request_params(params, settings)
9
- new(params, settings).parse_request_params
8
+ def self.parse_request_params(params, settings, endpoint)
9
+ new(params, settings, endpoint).parse_request_params
10
10
  end
11
11
 
12
- def initialize(params, settings)
12
+ def initialize(params, settings, endpoint)
13
13
  @params = params
14
14
  @settings = settings
15
+ @endpoint = endpoint
15
16
  end
16
17
 
17
18
  def parse_request_params
@@ -55,7 +56,13 @@ module GrapeSwagger
55
56
  return true unless param_options.key?(:documentation) && !param_options[:required]
56
57
 
57
58
  param_hidden = param_options[:documentation].fetch(:hidden, false)
58
- param_hidden = param_hidden.call if param_hidden.is_a?(Proc)
59
+ if param_hidden.is_a?(Proc)
60
+ param_hidden = if settings[:token_owner]
61
+ param_hidden.call(endpoint.send(settings[:token_owner].to_sym))
62
+ else
63
+ param_hidden.call
64
+ end
65
+ end
59
66
  !param_hidden
60
67
  end
61
68
 
@@ -10,17 +10,25 @@ module GrapeSwagger
10
10
  include Rack::Test::Methods
11
11
 
12
12
  attr_reader :oapi
13
- attr_reader :api_class
14
13
 
15
14
  def initialize(api_class)
16
15
  super()
17
16
 
18
- @api_class = api_class
17
+ if api_class.is_a? String
18
+ @api_class_name = api_class
19
+ else
20
+ @api_class = api_class
21
+ end
22
+
19
23
  define_tasks
20
24
  end
21
25
 
22
26
  private
23
27
 
28
+ def api_class
29
+ @api_class ||= @api_class_name.constantize
30
+ end
31
+
24
32
  def define_tasks
25
33
  namespace :oapi do
26
34
  fetch
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GrapeSwagger
4
- VERSION = '0.34.1'
4
+ VERSION = '1.2.1'
5
5
  end
@@ -35,5 +35,5 @@ describe '#427 nested entity given as string' do
35
35
  JSON.parse(last_response.body)['definitions']
36
36
  end
37
37
 
38
- specify { expect(subject.keys).to include 'RoleEntity', 'WithoutRole' }
38
+ specify { expect(subject.keys).to include 'RoleEntity', 'Permission_WithoutRole' }
39
39
  end
@@ -55,6 +55,8 @@ describe 'definition names' do
55
55
  class Class7
56
56
  class SeventhEntity < Class6::SixthEntity
57
57
  expose :seventh_thing
58
+
59
+ private_class_method :entity_name
58
60
  end
59
61
  end
60
62
  end
@@ -82,11 +84,11 @@ describe 'definition names' do
82
84
  JSON.parse(last_response.body)['definitions']
83
85
  end
84
86
 
85
- specify { expect(subject).to include 'Class1' }
86
- specify { expect(subject).to include 'Class2' }
87
+ specify { expect(subject).to include 'TestDefinition_DummyEntities_WithVeryLongName_AnotherGroupingModule_Class1' }
88
+ specify { expect(subject).to include 'TestDefinition_DummyEntities_WithVeryLongName_AnotherGroupingModule_Class2' }
87
89
  specify { expect(subject).to include 'FooKlass' }
88
- specify { expect(subject).to include 'FourthEntity' }
89
- specify { expect(subject).to include 'FithEntity' }
90
+ specify { expect(subject).to include 'TestDefinition_DummyEntities_WithVeryLongName_AnotherGroupingModule_Class4_FourthEntity' }
91
+ specify { expect(subject).to include 'TestDefinition_DummyEntities_WithVeryLongName_AnotherGroupingModule_Class5_FithEntity' }
90
92
  specify { expect(subject).to include 'BarKlass' }
91
- specify { expect(subject).to include 'SeventhEntity' }
93
+ specify { expect(subject).to include 'TestDefinition_DummyEntities_WithVeryLongName_AnotherGroupingModule_Class7_SeventhEntity' }
92
94
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe '#532 allow custom format' do
6
+ let(:app) do
7
+ Class.new(Grape::API) do
8
+ namespace :issue_784 do
9
+ params do
10
+ requires :logs, type: String, documentation: { format: 'log', x: { name: 'Log' } }
11
+ optional :phone_number, type: Integer, documentation: { format: 'phone_number', x: { name: 'PhoneNumber' } }
12
+ end
13
+
14
+ post do
15
+ present params
16
+ end
17
+ end
18
+
19
+ add_swagger_documentation format: :json
20
+ end
21
+ end
22
+
23
+ subject do
24
+ get '/swagger_doc'
25
+ JSON.parse(last_response.body)
26
+ end
27
+
28
+ let(:parameters) { subject['paths']['/issue_784']['post']['parameters'] }
29
+
30
+ specify do
31
+ expect(parameters).to eql(
32
+ [
33
+ { 'in' => 'formData', 'name' => 'logs', 'type' => 'string', 'format' => 'log', 'required' => true, 'x-name' => 'Log' },
34
+ { 'in' => 'formData', 'name' => 'phone_number', 'type' => 'integer', 'format' => 'phone_number', 'required' => false, 'x-name' => 'PhoneNumber' }
35
+ ]
36
+ )
37
+ end
38
+ end