grape-swagger 0.24.0 → 0.25.0

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