grape-swagger 0.24.0 → 0.25.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +4 -0
  4. data/.rubocop_todo.yml +29 -32
  5. data/.travis.yml +2 -0
  6. data/CHANGELOG.md +15 -0
  7. data/Gemfile +1 -1
  8. data/LICENSE.txt +1 -1
  9. data/README.md +253 -223
  10. data/UPGRADING.md +4 -0
  11. data/grape-swagger.gemspec +2 -2
  12. data/lib/grape-swagger.rb +10 -4
  13. data/lib/grape-swagger/doc_methods.rb +10 -5
  14. data/lib/grape-swagger/doc_methods/build_model_definition.rb +38 -0
  15. data/lib/grape-swagger/doc_methods/data_type.rb +6 -5
  16. data/lib/grape-swagger/doc_methods/move_params.rb +9 -1
  17. data/lib/grape-swagger/doc_methods/optional_object.rb +1 -1
  18. data/lib/grape-swagger/doc_methods/parse_params.rb +20 -19
  19. data/lib/grape-swagger/doc_methods/tag_name_description.rb +9 -17
  20. data/lib/grape-swagger/endpoint.rb +26 -29
  21. data/lib/grape-swagger/model_parsers.rb +7 -0
  22. data/lib/grape-swagger/version.rb +1 -1
  23. data/spec/issues/427_entity_as_string_spec.rb +39 -0
  24. data/spec/issues/430_entity_definitions_spec.rb +48 -4
  25. data/spec/support/model_parsers/entity_parser.rb +10 -1
  26. data/spec/support/model_parsers/mock_parser.rb +4 -1
  27. data/spec/support/model_parsers/representable_parser.rb +13 -2
  28. data/spec/swagger_v2/api_swagger_v2_detail_spec.rb +0 -2
  29. data/spec/swagger_v2/api_swagger_v2_param_type_body_spec.rb +56 -0
  30. data/spec/swagger_v2/api_swagger_v2_spec.rb +87 -61
  31. data/spec/swagger_v2/endpoint_versioned_path_spec.rb +24 -3
  32. data/spec/swagger_v2/guarded_endpoint_spec.rb +8 -3
  33. data/spec/swagger_v2/namespace_tags_prefix_spec.rb +2 -8
  34. data/spec/swagger_v2/namespace_tags_spec.rb +2 -8
  35. data/spec/swagger_v2/params_array_spec.rb +36 -0
  36. data/spec/swagger_v2/simple_mounted_api_spec.rb +2 -14
  37. metadata +12 -9
@@ -1,6 +1,10 @@
1
1
  Upgrading Grape-swagger
2
2
  =======================
3
3
 
4
+ ### Upgrading to >= 0.25.0
5
+
6
+ The global tag set now only includes tags for documented routes. This behaviour has impact in particular for calling the documtation of a specific route.
7
+
4
8
  ### Upgrading to >= 0.21.0
5
9
 
6
10
  With grape >= 0.21.0, `grape-entity` support moved to separate gem `grape-swagger-entity`, if you use grape entity, update your Gemfile:
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
8
8
  s.authors = ['Tim Vandecasteele']
9
9
  s.email = ['tim.vandecasteele@gmail.com']
10
10
  s.homepage = 'https://github.com/ruby-grape/grape-swagger'
11
- s.summary = 'A simple way to add auto generated documentation to your Grape API that can be displayed with Swagger.'
11
+ s.summary = 'Add auto generated documentation to your Grape API that can be displayed with Swagger.'
12
12
  s.license = 'MIT'
13
13
 
14
14
  s.add_runtime_dependency 'grape', '>= 0.12.0'
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.add_development_dependency 'bundler'
21
21
  s.add_development_dependency 'rack-test'
22
22
  s.add_development_dependency 'rack-cors'
23
- s.add_development_dependency 'rubocop', '0.40.0'
23
+ s.add_development_dependency 'rubocop', '~> 0.40'
24
24
  s.add_development_dependency 'kramdown'
25
25
  s.add_development_dependency 'redcarpet' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx')
26
26
  s.add_development_dependency 'rouge' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx')
@@ -52,7 +52,9 @@ module Grape
52
52
  combine_namespace_routes(@target_class.combined_namespaces)
53
53
 
54
54
  exclusive_route_keys = @target_class.combined_routes.keys - @target_class.combined_namespaces.keys
55
- exclusive_route_keys.each { |key| @target_class.combined_namespace_routes[key] = @target_class.combined_routes[key] }
55
+ exclusive_route_keys.each do |key|
56
+ @target_class.combined_namespace_routes[key] = @target_class.combined_routes[key]
57
+ end
56
58
  documentation_class
57
59
  end
58
60
 
@@ -128,10 +130,12 @@ module Grape
128
130
  end
129
131
 
130
132
  parent_standalone_namespaces = standalone_namespaces.reject { |ns_name, _| !name.start_with?(ns_name) }
131
- # add only to the main route if the namespace is not within any other namespace appearing as standalone resource
133
+ # add only to the main route
134
+ # if the namespace is not within any other namespace appearing as standalone resource
132
135
  if parent_standalone_namespaces.empty?
133
136
  # default option, append namespace methods to parent route
134
- @target_class.combined_namespace_routes[parent_route_name] = [] unless @target_class.combined_namespace_routes.key?(parent_route_name)
137
+ parent_route = @target_class.combined_namespace_routes.key?(parent_route_name)
138
+ @target_class.combined_namespace_routes[parent_route_name] = [] unless parent_route
135
139
  @target_class.combined_namespace_routes[parent_route_name].push(*namespace_routes)
136
140
  end
137
141
  end
@@ -175,7 +179,9 @@ module Grape
175
179
  # skip if sub_ns is standalone, too
176
180
  next unless sub_ns.options.key?(:swagger) && sub_ns.options[:swagger][:nested] == false
177
181
  # remove all namespaces that are nested below this standalone sub_ns
178
- sub_namespaces.each { |sub_sub_name, _| sub_namespaces.delete(sub_sub_name) if sub_sub_name.start_with?(sub_name) }
182
+ sub_namespaces.each do |sub_sub_name, _|
183
+ sub_namespaces.delete(sub_sub_name) if sub_sub_name.start_with?(sub_name)
184
+ end
179
185
  end
180
186
  sub_namespaces
181
187
  end
@@ -10,6 +10,7 @@ require 'grape-swagger/doc_methods/tag_name_description'
10
10
  require 'grape-swagger/doc_methods/parse_params'
11
11
  require 'grape-swagger/doc_methods/move_params'
12
12
  require 'grape-swagger/doc_methods/headers'
13
+ require 'grape-swagger/doc_methods/build_model_definition'
13
14
 
14
15
  module GrapeSwagger
15
16
  module DocMethods
@@ -36,9 +37,11 @@ module GrapeSwagger
36
37
 
37
38
  class_variables_from(options)
38
39
 
39
- [:format, :default_format, :default_error_formatter].each do |method|
40
- send(method, formatter)
41
- end if formatter
40
+ if formatter
41
+ [:format, :default_format, :default_error_formatter].each do |method|
42
+ send(method, formatter)
43
+ end
44
+ end
42
45
 
43
46
  send(guard.split.first.to_sym, *guard.split(/[\s,]+/).drop(1)) unless guard.nil?
44
47
 
@@ -49,7 +52,9 @@ module GrapeSwagger
49
52
  options
50
53
  )
51
54
 
52
- paths, definitions = endpoint.path_and_definition_objects(combi_routes, options)
55
+ paths, definitions = endpoint.path_and_definition_objects(combi_routes, options)
56
+ tags = GrapeSwagger::DocMethods::TagNameDescription.build(paths)
57
+ output[:tags] = tags unless tags.empty? || paths.blank?
53
58
  output[:paths] = paths unless paths.blank?
54
59
  output[:definitions] = definitions unless definitions.blank?
55
60
 
@@ -97,7 +102,7 @@ module GrapeSwagger
97
102
  specific_api_documentation: { desc: 'Swagger compatible API description for specific API' },
98
103
  endpoint_auth_wrapper: nil,
99
104
  swagger_endpoint_guard: nil,
100
- oauth_token: nil
105
+ token_owner: nil
101
106
  }
102
107
  end
103
108
 
@@ -0,0 +1,38 @@
1
+ module GrapeSwagger
2
+ module DocMethods
3
+ class BuildModelDefinition
4
+ class << self
5
+ def build(model, properties)
6
+ definition = { type: 'object', properties: properties }
7
+
8
+ required = required_attributes(model)
9
+ definition[:required] = required unless required.blank?
10
+
11
+ definition
12
+ end
13
+
14
+ private
15
+
16
+ def required_attributes(model)
17
+ parse_entity(model) || parse_representable(model)
18
+ end
19
+
20
+ def parse_entity(model)
21
+ return unless model.respond_to?(:documentation)
22
+
23
+ model.documentation
24
+ .select { |_name, options| options[:required] }
25
+ .map { |name, options| options[:as] || name }
26
+ end
27
+
28
+ def parse_representable(model)
29
+ return unless model.respond_to?(:map)
30
+
31
+ model.map
32
+ .select { |p| p[:documentation] && p[:documentation][:required] }
33
+ .map(&:name)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -41,13 +41,14 @@ module GrapeSwagger
41
41
  end
42
42
 
43
43
  def parse_entity_name(model)
44
- if model.respond_to?(:entity_name)
44
+ if model.methods(false).include?(:entity_name)
45
45
  model.entity_name
46
+ elsif model.to_s.end_with?('::Entity', '::Entities')
47
+ model.to_s.split('::')[-2]
48
+ elsif model.respond_to?(:name)
49
+ model.name.demodulize.camelize
46
50
  else
47
- name = model.to_s
48
- entity_parts = name.split('::')
49
- entity_parts.reject! { |p| p == 'Entity' || p == 'Entities' }
50
- entity_parts.join('::')
51
+ model.to_s.split('::').last
51
52
  end
52
53
  end
53
54
 
@@ -74,7 +74,15 @@ module GrapeSwagger
74
74
  end
75
75
 
76
76
  def document_as_property(param)
77
- property_keys.each_with_object({}) { |x, memo| memo[x] = param[x] if param[x].present? }
77
+ property_keys.each_with_object({}) do |x, memo|
78
+ value = param[x]
79
+ next if value.blank?
80
+ if x == :type && @definitions[value].present?
81
+ memo['$ref'] = "#/definitions/#{value}"
82
+ else
83
+ memo[x] = value
84
+ end
85
+ end
78
86
  end
79
87
 
80
88
  def build_nested_properties(params, properties = {})
@@ -12,7 +12,7 @@ module GrapeSwagger
12
12
  end
13
13
 
14
14
  def evaluate(key, options, request)
15
- options[key].arity == 0 ? options[key].call : options[key].call(request)
15
+ options[key].arity.zero? ? options[key].call : options[key].call(request)
16
16
  end
17
17
 
18
18
  def default_values
@@ -2,7 +2,7 @@ module GrapeSwagger
2
2
  module DocMethods
3
3
  class ParseParams
4
4
  class << self
5
- def call(param, settings, route)
5
+ def call(param, settings, route, definitions)
6
6
  path = route.path
7
7
  method = route.request_method
8
8
 
@@ -21,7 +21,7 @@ module GrapeSwagger
21
21
  # optional properties
22
22
  document_description(settings)
23
23
  document_type_and_format(data_type)
24
- document_array_param(value_type)
24
+ document_array_param(value_type, definitions) if value_type[:is_array]
25
25
  document_default_value(settings)
26
26
  document_range_values(settings)
27
27
  document_required(settings)
@@ -60,25 +60,26 @@ module GrapeSwagger
60
60
  end
61
61
  end
62
62
 
63
- def document_array_param(value_type)
64
- if value_type[:is_array]
65
- if value_type[:documentation].present?
66
- param_type = value_type[:documentation][:param_type]
67
- doc_type = value_type[:documentation][:type]
68
- type = GrapeSwagger::DocMethods::DataType.mapping(doc_type) if doc_type && !DataType.request_primitive?(doc_type)
69
- collection_format = value_type[:documentation][:collectionFormat]
70
- end
71
-
72
- array_items = {
73
- type: type || @parsed_param[:type],
74
- format: @parsed_param.delete(:format)
75
- }.delete_if { |_, value| value.blank? }
63
+ def document_array_param(value_type, definitions)
64
+ if value_type[:documentation].present?
65
+ param_type = value_type[:documentation][:param_type]
66
+ doc_type = value_type[:documentation][:type]
67
+ type = GrapeSwagger::DocMethods::DataType.mapping(doc_type) if doc_type && !DataType.request_primitive?(doc_type)
68
+ collection_format = value_type[:documentation][:collectionFormat]
69
+ end
76
70
 
77
- @parsed_param[:in] = param_type || 'formData'
78
- @parsed_param[:items] = array_items
79
- @parsed_param[:type] = 'array'
80
- @parsed_param[:collectionFormat] = collection_format if %w(csv ssv tsv pipes multi).include?(collection_format)
71
+ array_items = {}
72
+ if definitions[value_type[:data_type]]
73
+ array_items['$ref'] = "#/definitions/#{@parsed_param[:type]}"
74
+ else
75
+ array_items[:type] = type || @parsed_param[:type]
81
76
  end
77
+ array_items[:format] = @parsed_param.delete(:format) if @parsed_param[:format]
78
+
79
+ @parsed_param[:in] = param_type || 'formData'
80
+ @parsed_param[:items] = array_items
81
+ @parsed_param[:type] = 'array'
82
+ @parsed_param[:collectionFormat] = collection_format if %w(csv ssv tsv pipes multi).include?(collection_format)
82
83
  end
83
84
 
84
85
  def param_type(value_type)
@@ -2,23 +2,15 @@ module GrapeSwagger
2
2
  module DocMethods
3
3
  class TagNameDescription
4
4
  class << self
5
- def build(options = {})
6
- target_class = options[:target_class]
7
- namespaces = target_class.combined_namespaces
8
- namespace_routes = target_class.combined_namespace_routes
9
-
10
- namespace_routes.keys.map do |local_route|
11
- next if namespace_routes[local_route].map { |route| route.options[:hidden] }.all? { |value| value.respond_to?(:call) ? value.call : value }
12
-
13
- original_namespace_name = target_class.combined_namespace_identifiers.key?(local_route) ? target_class.combined_namespace_identifiers[local_route] : local_route
14
- description = namespaces[original_namespace_name] && namespaces[original_namespace_name].options[:desc]
15
- description ||= "Operations about #{original_namespace_name.pluralize}"
16
-
17
- {
18
- name: local_route,
19
- description: description
20
- }
21
- end.compact
5
+ def build(paths)
6
+ paths.values.each_with_object([]) do |path, memo|
7
+ path.values.first[:tags].each do |tag|
8
+ memo << {
9
+ name: tag,
10
+ description: "Operations about #{tag.pluralize}"
11
+ }
12
+ end
13
+ end.uniq
22
14
  end
23
15
  end
24
16
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'active_support'
4
2
  require 'active_support/core_ext/string/inflections.rb'
5
3
 
@@ -29,7 +27,6 @@ module Grape
29
27
  securityDefinitions: options[:security_definitions],
30
28
  host: GrapeSwagger::DocMethods::OptionalObject.build(:host, options, request),
31
29
  basePath: GrapeSwagger::DocMethods::OptionalObject.build(:base_path, options, request),
32
- tags: GrapeSwagger::DocMethods::TagNameDescription.build(options),
33
30
  schemes: options[:schemes].is_a?(String) ? [options[:schemes]] : options[:schemes]
34
31
  }.delete_if { |_, value| value.blank? }
35
32
  end
@@ -112,7 +109,7 @@ module Grape
112
109
  method[:parameters] = params_object(route)
113
110
  method[:security] = security_object(route)
114
111
  method[:responses] = response_object(route, options[:markdown])
115
- method[:tags] = tag_object(route)
112
+ method[:tags] = route.options.fetch(:tags, tag_object(route))
116
113
  method[:operationId] = GrapeSwagger::DocMethods::OperationId.build(route, path)
117
114
  method.delete_if { |_, value| value.blank? }
118
115
 
@@ -164,7 +161,12 @@ module Grape
164
161
  parameters = partition_params(route).map do |param, value|
165
162
  value = { required: false }.merge(value) if value.is_a?(Hash)
166
163
  _, value = default_type([[param, value]]).first if value == ''
167
- GrapeSwagger::DocMethods::ParseParams.call(param, value, route)
164
+ if value[:type]
165
+ expose_params(value[:type])
166
+ elsif value[:documentation]
167
+ expose_params(value[:documentation][:type])
168
+ end
169
+ GrapeSwagger::DocMethods::ParseParams.call(param, value, route, @definitions)
168
170
  end
169
171
 
170
172
  if GrapeSwagger::DocMethods::MoveParams.can_be_moved?(parameters, route.request_method)
@@ -262,49 +264,44 @@ module Grape
262
264
  param_types.size == 1
263
265
  end
264
266
 
267
+ def expose_params(value)
268
+ if value.is_a?(Class) && GrapeSwagger.model_parsers.find(value)
269
+ expose_params_from_model(value)
270
+ elsif value.is_a?(String)
271
+ begin
272
+ expose_params(Object.const_get(value.gsub(/\[|\]/, ''))) # try to load class from its name
273
+ rescue NameError
274
+ nil
275
+ end
276
+ end
277
+ end
278
+
265
279
  def expose_params_from_model(model)
280
+ model = model.is_a?(String) ? model.constantize : model
266
281
  model_name = model_name(model)
267
282
 
268
283
  return model_name if @definitions.key?(model_name)
269
284
  @definitions[model_name] = nil
270
285
 
271
- properties = nil
272
- parser = nil
273
-
274
- GrapeSwagger.model_parsers.each do |klass, ancestor|
275
- next unless model.ancestors.map(&:to_s).include?(ancestor)
276
- parser = klass.new(model, self)
277
- break
278
- end
279
-
280
- properties = parser.call unless parser.nil?
281
-
286
+ parser = GrapeSwagger.model_parsers.find(model)
282
287
  raise GrapeSwagger::Errors::UnregisteredParser, "No parser registered for #{model_name}." unless parser
288
+
289
+ properties = parser.new(model, self).call
283
290
  raise GrapeSwagger::Errors::SwaggerSpec, "Empty model #{model_name}, swagger 2.0 doesn't support empty definitions." unless properties && properties.any?
284
291
 
285
- @definitions[model_name] = { type: 'object', properties: properties }
292
+ @definitions[model_name] = GrapeSwagger::DocMethods::BuildModelDefinition.build(model, properties)
286
293
 
287
294
  model_name
288
295
  end
289
296
 
290
297
  def model_name(name)
291
- if name.respond_to?(:entity_name)
292
- name.entity_name
293
- elsif name.to_s.end_with?('Entity', 'Entities')
294
- length = 0
295
- name.to_s.split('::')[0..-2].reverse.take_while do |x|
296
- length += x.length
297
- length < 42
298
- end.reverse.join
299
- else
300
- name.name.demodulize.camelize
301
- end
298
+ GrapeSwagger::DocMethods::DataType.parse_entity_name(name)
302
299
  end
303
300
 
304
301
  def hidden?(route, options)
305
302
  route_hidden = route.options[:hidden]
306
303
  return route_hidden unless route_hidden.is_a?(Proc)
307
- options[:oauth_token] ? route_hidden.call(send(options[:oauth_token].to_sym)) : route_hidden.call
304
+ options[:token_owner] ? route_hidden.call(send(options[:token_owner].to_sym)) : route_hidden.call
308
305
  end
309
306
 
310
307
  def public_parameter?(param)
@@ -29,5 +29,12 @@ module GrapeSwagger
29
29
  yield klass, ancestor
30
30
  end
31
31
  end
32
+
33
+ def find(model)
34
+ GrapeSwagger.model_parsers.each do |klass, ancestor|
35
+ return klass if model.ancestors.map(&:to_s).include?(ancestor)
36
+ end
37
+ nil
38
+ end
32
39
  end
33
40
  end
@@ -1,3 +1,3 @@
1
1
  module GrapeSwagger
2
- VERSION = '0.24.0'.freeze
2
+ VERSION = '0.25.0'.freeze
3
3
  end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+ require 'grape-entity'
3
+ require 'grape-swagger-entity'
4
+
5
+ describe '#427 nested entity given as string' do
6
+ let(:app) do
7
+ Class.new(Grape::API) do
8
+ namespace :issue_427 do
9
+ module Permission
10
+ class WithoutRole < Grape::Entity
11
+ expose :id
12
+ expose :description
13
+ end
14
+ end
15
+
16
+ class RoleEntity < Grape::Entity
17
+ expose :id
18
+ expose :description
19
+ expose :role
20
+ expose :permissions, using: 'Permission::WithoutRole'
21
+ end
22
+ desc 'Get a list of roles',
23
+ success: RoleEntity
24
+ get '/' do
25
+ present [], with: RoleEntity
26
+ end
27
+ end
28
+
29
+ add_swagger_documentation format: :json
30
+ end
31
+ end
32
+
33
+ subject do
34
+ get '/swagger_doc'
35
+ JSON.parse(last_response.body)['definitions']
36
+ end
37
+
38
+ specify { expect(subject.keys).to include 'RoleEntity', 'WithoutRole' }
39
+ end