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.
- checksums.yaml +4 -4
- data/.rubocop.yml +83 -0
- data/.travis.yml +15 -12
- data/CHANGELOG.md +44 -4
- data/Gemfile +9 -4
- data/README.md +123 -11
- data/UPGRADING.md +30 -0
- data/grape-swagger.gemspec +1 -1
- data/lib/grape-swagger.rb +2 -2
- data/lib/grape-swagger/doc_methods/build_model_definition.rb +53 -2
- data/lib/grape-swagger/doc_methods/data_type.rb +6 -6
- data/lib/grape-swagger/doc_methods/operation_id.rb +2 -2
- data/lib/grape-swagger/doc_methods/parse_params.rb +19 -4
- data/lib/grape-swagger/endpoint.rb +28 -18
- data/lib/grape-swagger/endpoint/params_parser.rb +12 -5
- data/lib/grape-swagger/rake/oapi_tasks.rb +10 -2
- data/lib/grape-swagger/version.rb +1 -1
- data/spec/issues/427_entity_as_string_spec.rb +1 -1
- data/spec/issues/430_entity_definitions_spec.rb +7 -5
- data/spec/issues/784_extensions_on_params_spec.rb +38 -0
- data/spec/lib/data_type_spec.rb +14 -2
- data/spec/lib/endpoint/params_parser_spec.rb +2 -1
- data/spec/lib/endpoint_spec.rb +1 -1
- data/spec/lib/oapi_tasks_spec.rb +15 -5
- data/spec/support/empty_model_parser.rb +1 -2
- data/spec/support/mock_parser.rb +1 -2
- data/spec/support/model_parsers/entity_parser.rb +1 -1
- data/spec/support/model_parsers/representable_parser.rb +1 -1
- data/spec/swagger_v2/api_swagger_v2_hide_param_spec.rb +14 -3
- data/spec/swagger_v2/api_swagger_v2_param_type_body_spec.rb +2 -2
- data/spec/swagger_v2/boolean_params_spec.rb +1 -1
- data/spec/swagger_v2/inheritance_and_discriminator_spec.rb +56 -0
- data/spec/swagger_v2/reference_entity_spec.rb +74 -29
- data/spec/swagger_v2/security_requirement_spec.rb +2 -2
- metadata +15 -17
data/UPGRADING.md
CHANGED
@@ -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,
|
data/grape-swagger.gemspec
CHANGED
@@ -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', '
|
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")
|
data/lib/grape-swagger.rb
CHANGED
@@ -107,8 +107,8 @@ module SwaggerRouting
|
|
107
107
|
end
|
108
108
|
|
109
109
|
module SwaggerDocumentationAdder
|
110
|
-
attr_accessor :combined_namespaces, :combined_namespace_identifiers
|
111
|
-
|
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 '
|
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.
|
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('::')[
|
55
|
-
elsif model.
|
56
|
-
model.
|
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('::').
|
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!(
|
20
|
-
operation.gsub!(
|
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
|
-
|
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 =
|
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
|
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 =
|
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
|
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 =
|
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
|
-
|
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] =
|
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
|
-
|
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
|
-
|
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
|
@@ -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', '
|
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 '
|
86
|
-
specify { expect(subject).to include '
|
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 '
|
89
|
-
specify { expect(subject).to include '
|
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 '
|
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
|