grape-swagger 0.10.0 → 0.10.2
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +10 -10
- data/.travis.yml +4 -1
- data/CHANGELOG.md +92 -63
- data/CONTRIBUTING.md +8 -8
- data/Gemfile +1 -1
- data/README.md +90 -16
- data/RELEASING.md +1 -1
- data/UPGRADING.md +26 -5
- data/grape-swagger.gemspec +1 -1
- data/lib/grape-swagger/doc_methods.rb +495 -0
- data/lib/grape-swagger/version.rb +1 -1
- data/lib/grape-swagger.rb +50 -476
- data/spec/api_description_spec.rb +2 -2
- data/spec/api_global_models_spec.rb +21 -21
- data/spec/api_models_spec.rb +62 -42
- data/spec/api_paths_spec.rb +64 -0
- data/spec/api_with_nil_types.rb +50 -0
- data/spec/api_with_prefix_and_namespace_spec.rb +32 -0
- data/spec/api_with_standalone_namespace_spec.rb +16 -16
- data/spec/array_entity_spec.rb +34 -0
- data/spec/array_params_spec.rb +35 -6
- data/spec/default_api_spec.rb +42 -0
- data/spec/float_api_spec.rb +1 -1
- data/spec/grape-swagger_helper_spec.rb +14 -1
- data/spec/mounted_target_class_spec.rb +63 -0
- data/spec/non_default_api_spec.rb +47 -16
- data/spec/param_type_spec.rb +52 -0
- data/spec/param_values_spec.rb +130 -0
- data/spec/reference_entity.rb +80 -0
- data/spec/simple_mounted_api_spec.rb +2 -2
- data/spec/support/grape_version.rb +11 -0
- metadata +54 -40
- data/.ruby-version +0 -1
- data/spec/range_values_spec.rb +0 -49
data/lib/grape-swagger.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'grape'
|
2
2
|
require 'grape-swagger/version'
|
3
3
|
require 'grape-swagger/errors'
|
4
|
+
require 'grape-swagger/doc_methods'
|
4
5
|
require 'grape-swagger/markdown'
|
5
6
|
require 'grape-swagger/markdown/kramdown_adapter'
|
6
7
|
require 'grape-swagger/markdown/redcarpet_adapter'
|
@@ -8,16 +9,19 @@ require 'grape-swagger/markdown/redcarpet_adapter'
|
|
8
9
|
module Grape
|
9
10
|
class API
|
10
11
|
class << self
|
11
|
-
|
12
|
+
attr_accessor :combined_routes, :combined_namespaces, :combined_namespace_routes, :combined_namespace_identifiers
|
12
13
|
|
13
14
|
def add_swagger_documentation(options = {})
|
14
15
|
documentation_class = create_documentation_class
|
15
16
|
|
16
|
-
|
17
|
+
options = { target_class: self }.merge(options)
|
18
|
+
@target_class = options[:target_class]
|
19
|
+
|
20
|
+
documentation_class.setup(options)
|
17
21
|
mount(documentation_class)
|
18
22
|
|
19
|
-
@combined_routes = {}
|
20
|
-
routes.each do |route|
|
23
|
+
@target_class.combined_routes = {}
|
24
|
+
@target_class.routes.each do |route|
|
21
25
|
route_path = route.route_path
|
22
26
|
route_match = route_path.split(/^.*?#{route.route_prefix.to_s}/).last
|
23
27
|
next unless route_match
|
@@ -26,20 +30,20 @@ module Grape
|
|
26
30
|
resource = route_match.captures.first
|
27
31
|
next if resource.empty?
|
28
32
|
resource.downcase!
|
29
|
-
@combined_routes[resource] ||= []
|
30
|
-
next if documentation_class.hide_documentation_path && route.route_path.
|
31
|
-
@combined_routes[resource] << route
|
33
|
+
@target_class.combined_routes[resource] ||= []
|
34
|
+
next if documentation_class.hide_documentation_path && route.route_path.match(/#{documentation_class.mount_path}($|\/|\(\.)/)
|
35
|
+
@target_class.combined_routes[resource] << route
|
32
36
|
end
|
33
37
|
|
34
|
-
@combined_namespaces = {}
|
35
|
-
combine_namespaces(
|
38
|
+
@target_class.combined_namespaces = {}
|
39
|
+
combine_namespaces(@target_class)
|
36
40
|
|
37
|
-
@combined_namespace_routes = {}
|
38
|
-
@combined_namespace_identifiers = {}
|
39
|
-
combine_namespace_routes(@combined_namespaces)
|
41
|
+
@target_class.combined_namespace_routes = {}
|
42
|
+
@target_class.combined_namespace_identifiers = {}
|
43
|
+
combine_namespace_routes(@target_class.combined_namespaces)
|
40
44
|
|
41
|
-
exclusive_route_keys = @combined_routes.keys - @combined_namespaces.keys
|
42
|
-
exclusive_route_keys.each { |key| @combined_namespace_routes[key] = @combined_routes[key] }
|
45
|
+
exclusive_route_keys = @target_class.combined_routes.keys - @target_class.combined_namespaces.keys
|
46
|
+
exclusive_route_keys.each { |key| @target_class.combined_namespace_routes[key] = @target_class.combined_routes[key] }
|
43
47
|
documentation_class
|
44
48
|
end
|
45
49
|
|
@@ -54,7 +58,7 @@ module Grape
|
|
54
58
|
end
|
55
59
|
# use the full namespace here (not the latest level only)
|
56
60
|
# and strip leading slash
|
57
|
-
@combined_namespaces[endpoint.namespace.sub(/^\//, '')] = ns if ns
|
61
|
+
@target_class.combined_namespaces[endpoint.namespace.sub(/^\//, '')] = ns if ns
|
58
62
|
|
59
63
|
combine_namespaces(endpoint.options[:app]) if endpoint.options[:app]
|
60
64
|
end
|
@@ -65,10 +69,10 @@ module Grape
|
|
65
69
|
namespaces.each do |name, namespace|
|
66
70
|
# get the parent route for the namespace
|
67
71
|
parent_route_name = name.match(%r{^/?([^/]*).*$})[1]
|
68
|
-
parent_route = @combined_routes[parent_route_name]
|
72
|
+
parent_route = @target_class.combined_routes[parent_route_name]
|
69
73
|
# fetch all routes that are within the current namespace
|
70
74
|
namespace_routes = parent_route.collect do |route|
|
71
|
-
route if (route.route_path.start_with?("/#{name}") || route.route_path.start_with?("/:version/#{name}")) &&
|
75
|
+
route if (route.route_path.start_with?(route.route_prefix ? "/#{route.route_prefix}/#{name}" : "/#{name}") || route.route_path.start_with?((route.route_prefix ? "/#{route.route_prefix}/:version/#{name}" : "/:version/#{name}"))) &&
|
72
76
|
(route.instance_variable_get(:@options)[:namespace] == "/#{name}" || route.instance_variable_get(:@options)[:namespace] == "/:version/#{name}")
|
73
77
|
end.compact
|
74
78
|
|
@@ -79,8 +83,8 @@ module Grape
|
|
79
83
|
else
|
80
84
|
identifier = name.gsub(/_/, '-').gsub(/\//, '_')
|
81
85
|
end
|
82
|
-
@combined_namespace_identifiers[identifier] = name
|
83
|
-
@combined_namespace_routes[identifier] = namespace_routes
|
86
|
+
@target_class.combined_namespace_identifiers[identifier] = name
|
87
|
+
@target_class.combined_namespace_routes[identifier] = namespace_routes
|
84
88
|
|
85
89
|
# get all nested namespaces below the current namespace
|
86
90
|
sub_namespaces = standalone_sub_namespaces(name, namespaces)
|
@@ -92,7 +96,7 @@ module Grape
|
|
92
96
|
route if sub_ns_paths.include?(route.instance_variable_get(:@options)[:namespace]) || sub_ns_paths_versioned.include?(route.instance_variable_get(:@options)[:namespace])
|
93
97
|
end.compact
|
94
98
|
# add all determined routes of the sub namespaces to standalone resource
|
95
|
-
@combined_namespace_routes[identifier].push(*sub_routes)
|
99
|
+
@target_class.combined_namespace_routes[identifier].push(*sub_routes)
|
96
100
|
else
|
97
101
|
# default case when not explicitly specified or nested == true
|
98
102
|
standalone_namespaces = namespaces.reject { |_, ns| !ns.options.key?(:swagger) || !ns.options[:swagger].key?(:nested) || ns.options[:swagger][:nested] != false }
|
@@ -100,8 +104,8 @@ module Grape
|
|
100
104
|
# add only to the main route if the namespace is not within any other namespace appearing as standalone resource
|
101
105
|
if parent_standalone_namespaces.empty?
|
102
106
|
# default option, append namespace methods to parent route
|
103
|
-
@combined_namespace_routes[parent_route_name] = [] unless @combined_namespace_routes.key?(parent_route_name)
|
104
|
-
@combined_namespace_routes[parent_route_name].push(*namespace_routes)
|
107
|
+
@target_class.combined_namespace_routes[parent_route_name] = [] unless @target_class.combined_namespace_routes.key?(parent_route_name)
|
108
|
+
@target_class.combined_namespace_routes[parent_route_name].push(*namespace_routes)
|
105
109
|
end
|
106
110
|
end
|
107
111
|
end
|
@@ -125,7 +129,7 @@ module Grape
|
|
125
129
|
|
126
130
|
def get_non_nested_params(params)
|
127
131
|
# Duplicate the params as we are going to modify them
|
128
|
-
dup_params = params.each_with_object(
|
132
|
+
dup_params = params.each_with_object({}) do |(param, value), dparams|
|
129
133
|
dparams[param] = value.dup
|
130
134
|
end
|
131
135
|
|
@@ -145,11 +149,13 @@ module Grape
|
|
145
149
|
params.each_key do |k|
|
146
150
|
if params[k].is_a?(Hash) && params[k][:type] == 'Array'
|
147
151
|
array_param = k
|
152
|
+
modified_params[k] = params[k]
|
148
153
|
else
|
149
154
|
new_key = k
|
150
155
|
unless array_param.nil?
|
151
156
|
if k.to_s.start_with?(array_param.to_s + '[')
|
152
157
|
new_key = array_param.to_s + '[]' + k.to_s.split(array_param)[1]
|
158
|
+
modified_params.delete array_param
|
153
159
|
end
|
154
160
|
end
|
155
161
|
modified_params[new_key] = params[k]
|
@@ -158,461 +164,29 @@ module Grape
|
|
158
164
|
modified_params
|
159
165
|
end
|
160
166
|
|
161
|
-
def
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
non_nested_parent_params = get_non_nested_params(parsed_array_params)
|
178
|
-
|
179
|
-
non_nested_parent_params.map do |param, value|
|
180
|
-
items = {}
|
181
|
-
|
182
|
-
raw_data_type = value.is_a?(Hash) ? (value[:type] || 'string').to_s : 'string'
|
183
|
-
data_type = case raw_data_type
|
184
|
-
when 'Hash'
|
185
|
-
'object'
|
186
|
-
when 'Rack::Multipart::UploadedFile'
|
187
|
-
'File'
|
188
|
-
when 'Virtus::Attribute::Boolean'
|
189
|
-
'boolean'
|
190
|
-
when 'Boolean', 'Date', 'Integer', 'String', 'Float'
|
191
|
-
raw_data_type.downcase
|
192
|
-
when 'BigDecimal'
|
193
|
-
'long'
|
194
|
-
when 'DateTime'
|
195
|
-
'dateTime'
|
196
|
-
when 'Numeric'
|
197
|
-
'double'
|
198
|
-
when 'Symbol'
|
199
|
-
'string'
|
200
|
-
else
|
201
|
-
@@documentation_class.parse_entity_name(raw_data_type)
|
202
|
-
end
|
203
|
-
description = value.is_a?(Hash) ? value[:desc] || value[:description] : ''
|
204
|
-
required = value.is_a?(Hash) ? !!value[:required] : false
|
205
|
-
default_value = value.is_a?(Hash) ? value[:default] : nil
|
206
|
-
is_array = value.is_a?(Hash) ? (value[:is_array] || false) : false
|
207
|
-
enum_values = value.is_a?(Hash) ? value[:values] : nil
|
208
|
-
enum_values = enum_values.to_a if enum_values && enum_values.is_a?(Range)
|
209
|
-
enum_values = enum_values.call if enum_values && enum_values.is_a?(Proc)
|
210
|
-
|
211
|
-
if value.is_a?(Hash) && value.key?(:param_type)
|
212
|
-
param_type = value[:param_type]
|
213
|
-
if is_array
|
214
|
-
items = { '$ref' => data_type }
|
215
|
-
data_type = 'array'
|
216
|
-
end
|
217
|
-
else
|
218
|
-
param_type = case
|
219
|
-
when path.include?(":#{param}")
|
220
|
-
'path'
|
221
|
-
when %w(POST PUT PATCH).include?(method)
|
222
|
-
if is_primitive?(data_type)
|
223
|
-
'form'
|
224
|
-
else
|
225
|
-
'body'
|
226
|
-
end
|
227
|
-
else
|
228
|
-
'query'
|
229
|
-
end
|
230
|
-
end
|
231
|
-
name = (value.is_a?(Hash) && value[:full_name]) || param
|
232
|
-
|
233
|
-
parsed_params = {
|
234
|
-
paramType: param_type,
|
235
|
-
name: name,
|
236
|
-
description: as_markdown(description),
|
237
|
-
type: data_type,
|
238
|
-
required: required,
|
239
|
-
allowMultiple: is_array
|
240
|
-
}
|
241
|
-
parsed_params.merge!(format: 'int32') if data_type == 'integer'
|
242
|
-
parsed_params.merge!(format: 'int64') if data_type == 'long'
|
243
|
-
parsed_params.merge!(items: items) if items.present?
|
244
|
-
parsed_params.merge!(defaultValue: default_value) if default_value
|
245
|
-
parsed_params.merge!(enum: enum_values) if enum_values
|
246
|
-
parsed_params
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
def content_types_for(target_class)
|
251
|
-
content_types = (target_class.content_types || {}).values
|
252
|
-
|
253
|
-
if content_types.empty?
|
254
|
-
formats = [target_class.format, target_class.default_format].compact.uniq
|
255
|
-
formats = Grape::Formatter::Base.formatters({}).keys if formats.empty?
|
256
|
-
content_types = Grape::ContentTypes::CONTENT_TYPES.select { |content_type, _mime_type| formats.include? content_type }.values
|
257
|
-
end
|
258
|
-
|
259
|
-
content_types.uniq
|
260
|
-
end
|
261
|
-
|
262
|
-
def parse_info(info)
|
263
|
-
{
|
264
|
-
contact: info[:contact],
|
265
|
-
description: as_markdown(info[:description]),
|
266
|
-
license: info[:license],
|
267
|
-
licenseUrl: info[:license_url],
|
268
|
-
termsOfServiceUrl: info[:terms_of_service_url],
|
269
|
-
title: info[:title]
|
270
|
-
}.delete_if { |_, value| value.blank? }
|
271
|
-
end
|
272
|
-
|
273
|
-
def parse_header_params(params)
|
274
|
-
params ||= []
|
275
|
-
|
276
|
-
params.map do |param, value|
|
277
|
-
data_type = 'String'
|
278
|
-
description = value.is_a?(Hash) ? value[:description] : ''
|
279
|
-
required = value.is_a?(Hash) ? !!value[:required] : false
|
280
|
-
default_value = value.is_a?(Hash) ? value[:default] : nil
|
281
|
-
param_type = 'header'
|
282
|
-
|
283
|
-
parsed_params = {
|
284
|
-
paramType: param_type,
|
285
|
-
name: param,
|
286
|
-
description: as_markdown(description),
|
287
|
-
type: data_type,
|
288
|
-
required: required
|
289
|
-
}
|
290
|
-
|
291
|
-
parsed_params.merge!(defaultValue: default_value) if default_value
|
292
|
-
|
293
|
-
parsed_params
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
|
-
def parse_path(path, version)
|
298
|
-
# adapt format to swagger format
|
299
|
-
parsed_path = path.gsub('(.:format)', @@hide_format ? '' : '.{format}')
|
300
|
-
# This is attempting to emulate the behavior of
|
301
|
-
# Rack::Mount::Strexp. We cannot use Strexp directly because
|
302
|
-
# all it does is generate regular expressions for parsing URLs.
|
303
|
-
# TODO: Implement a Racc tokenizer to properly generate the
|
304
|
-
# parsed path.
|
305
|
-
parsed_path = parsed_path.gsub(/:([a-zA-Z_]\w*)/, '{\1}')
|
306
|
-
# add the version
|
307
|
-
version ? parsed_path.gsub('{version}', version) : parsed_path
|
308
|
-
end
|
309
|
-
|
310
|
-
def parse_entity_name(model)
|
311
|
-
if model.respond_to?(:entity_name)
|
312
|
-
model.entity_name
|
313
|
-
else
|
314
|
-
name = model.to_s
|
315
|
-
entity_parts = name.split('::')
|
316
|
-
entity_parts.reject! { |p| p == 'Entity' || p == 'Entities' }
|
317
|
-
entity_parts.join('::')
|
318
|
-
end
|
319
|
-
end
|
320
|
-
|
321
|
-
def parse_entity_models(models)
|
322
|
-
result = {}
|
323
|
-
models.each do |model|
|
324
|
-
name = parse_entity_name(model)
|
325
|
-
properties = {}
|
326
|
-
required = []
|
327
|
-
|
328
|
-
model.documentation.each do |property_name, property_info|
|
329
|
-
p = property_info.dup
|
330
|
-
|
331
|
-
required << property_name.to_s if p.delete(:required)
|
332
|
-
|
333
|
-
type = if p[:type]
|
334
|
-
p.delete(:type)
|
335
|
-
elsif (entity = model.exposures[property_name][:using])
|
336
|
-
parse_entity_name(entity)
|
337
|
-
end
|
338
|
-
|
339
|
-
if p.delete(:is_array)
|
340
|
-
p[:items] = generate_typeref(type)
|
341
|
-
p[:type] = 'array'
|
342
|
-
else
|
343
|
-
p.merge! generate_typeref(type)
|
344
|
-
end
|
345
|
-
|
346
|
-
# rename Grape Entity's "desc" to "description"
|
347
|
-
property_description = p.delete(:desc)
|
348
|
-
p[:description] = property_description if property_description
|
349
|
-
|
350
|
-
# rename Grape's 'values' to 'enum'
|
351
|
-
select_values = p.delete(:values)
|
352
|
-
if select_values
|
353
|
-
select_values = select_values.call if select_values.is_a?(Proc)
|
354
|
-
p[:enum] = select_values
|
355
|
-
end
|
356
|
-
|
357
|
-
properties[property_name] = p
|
358
|
-
end
|
359
|
-
|
360
|
-
result[name] = {
|
361
|
-
id: model.instance_variable_get(:@root) || name,
|
362
|
-
properties: properties
|
363
|
-
}
|
364
|
-
result[name].merge!(required: required) unless required.empty?
|
365
|
-
end
|
366
|
-
|
367
|
-
result
|
368
|
-
end
|
369
|
-
|
370
|
-
def models_with_included_presenters(models)
|
371
|
-
all_models = models
|
372
|
-
|
373
|
-
models.each do |model|
|
374
|
-
# get model references from exposures with a documentation
|
375
|
-
nested_models = model.exposures.map do |_, config|
|
376
|
-
if config.key?(:documentation)
|
377
|
-
model = config[:using]
|
378
|
-
model.respond_to?(:constantize) ? model.constantize : model
|
379
|
-
end
|
380
|
-
end.compact
|
381
|
-
|
382
|
-
# get all nested models recursively
|
383
|
-
additional_models = nested_models.map do |nested_model|
|
384
|
-
models_with_included_presenters([nested_model])
|
385
|
-
end.flatten
|
386
|
-
|
387
|
-
all_models += additional_models
|
388
|
-
end
|
389
|
-
|
390
|
-
all_models
|
391
|
-
end
|
392
|
-
|
393
|
-
def is_primitive?(type)
|
394
|
-
%w(object integer long float double string byte boolean date dateTime).include? type
|
395
|
-
end
|
396
|
-
|
397
|
-
def generate_typeref(type)
|
398
|
-
type = type.to_s.sub(/^[A-Z]/) { |f| f.downcase } if type.is_a?(Class)
|
399
|
-
if is_primitive? type
|
400
|
-
{ 'type' => type }
|
401
|
-
else
|
402
|
-
{ '$ref' => type }
|
403
|
-
end
|
404
|
-
end
|
405
|
-
|
406
|
-
def parse_http_codes(codes, models)
|
407
|
-
codes ||= {}
|
408
|
-
codes.map do |k, v, m|
|
409
|
-
models << m if m
|
410
|
-
http_code_hash = {
|
411
|
-
code: k,
|
412
|
-
message: v
|
413
|
-
}
|
414
|
-
http_code_hash[:responseModel] = parse_entity_name(m) if m
|
415
|
-
http_code_hash
|
416
|
-
end
|
417
|
-
end
|
418
|
-
|
419
|
-
def strip_heredoc(string)
|
420
|
-
indent = string.scan(/^[ \t]*(?=\S)/).min.try(:size) || 0
|
421
|
-
string.gsub(/^[ \t]{#{indent}}/, '')
|
422
|
-
end
|
423
|
-
|
424
|
-
def parse_base_path(base_path, request)
|
425
|
-
if base_path.is_a?(Proc)
|
426
|
-
base_path.call(request)
|
427
|
-
elsif base_path.is_a?(String)
|
428
|
-
URI(base_path).relative? ? URI.join(request.base_url, base_path).to_s : base_path
|
429
|
-
else
|
430
|
-
request.base_url
|
431
|
-
end
|
432
|
-
end
|
433
|
-
|
434
|
-
def hide_documentation_path
|
435
|
-
@@hide_documentation_path
|
436
|
-
end
|
437
|
-
|
438
|
-
def mount_path
|
439
|
-
@@mount_path
|
440
|
-
end
|
441
|
-
|
442
|
-
def setup(options)
|
443
|
-
defaults = {
|
444
|
-
target_class: nil,
|
445
|
-
mount_path: '/swagger_doc',
|
446
|
-
base_path: nil,
|
447
|
-
api_version: '0.1',
|
448
|
-
markdown: nil,
|
449
|
-
hide_documentation_path: false,
|
450
|
-
hide_format: false,
|
451
|
-
format: nil,
|
452
|
-
models: [],
|
453
|
-
info: {},
|
454
|
-
authorizations: nil,
|
455
|
-
root_base_path: true,
|
456
|
-
api_documentation: { desc: 'Swagger compatible API description' },
|
457
|
-
specific_api_documentation: { desc: 'Swagger compatible API description for specific API' }
|
458
|
-
}
|
459
|
-
|
460
|
-
options = defaults.merge(options)
|
461
|
-
|
462
|
-
target_class = options[:target_class]
|
463
|
-
@@mount_path = options[:mount_path]
|
464
|
-
@@class_name = options[:class_name] || options[:mount_path].gsub('/', '')
|
465
|
-
@@markdown = options[:markdown] ? GrapeSwagger::Markdown.new(options[:markdown]) : nil
|
466
|
-
@@hide_format = options[:hide_format]
|
467
|
-
api_version = options[:api_version]
|
468
|
-
authorizations = options[:authorizations]
|
469
|
-
root_base_path = options[:root_base_path]
|
470
|
-
extra_info = options[:info]
|
471
|
-
api_doc = options[:api_documentation].dup
|
472
|
-
specific_api_doc = options[:specific_api_documentation].dup
|
473
|
-
@@models = options[:models] || []
|
474
|
-
|
475
|
-
@@hide_documentation_path = options[:hide_documentation_path]
|
476
|
-
|
477
|
-
if options[:format]
|
478
|
-
[:format, :default_format, :default_error_formatter].each do |method|
|
479
|
-
send(method, options[:format])
|
480
|
-
end
|
481
|
-
end
|
482
|
-
|
483
|
-
@@documentation_class = self
|
484
|
-
|
485
|
-
desc api_doc.delete(:desc), api_doc
|
486
|
-
get @@mount_path do
|
487
|
-
header['Access-Control-Allow-Origin'] = '*'
|
488
|
-
header['Access-Control-Request-Method'] = '*'
|
489
|
-
|
490
|
-
namespaces = target_class.combined_namespaces
|
491
|
-
namespace_routes = target_class.combined_namespace_routes
|
492
|
-
|
493
|
-
if @@hide_documentation_path
|
494
|
-
namespace_routes.reject! { |route, _value| "/#{route}/".index(@@documentation_class.parse_path(@@mount_path, nil) << '/') == 0 }
|
495
|
-
end
|
496
|
-
|
497
|
-
namespace_routes_array = namespace_routes.keys.map do |local_route|
|
498
|
-
next if namespace_routes[local_route].map(&:route_hidden).all? { |value| value.respond_to?(:call) ? value.call : value }
|
499
|
-
|
500
|
-
url_format = '.{format}' unless @@hide_format
|
501
|
-
|
502
|
-
original_namespace_name = target_class.combined_namespace_identifiers.key?(local_route) ? target_class.combined_namespace_identifiers[local_route] : local_route
|
503
|
-
description = namespaces[original_namespace_name] && namespaces[original_namespace_name].options[:desc]
|
504
|
-
description ||= "Operations about #{original_namespace_name.pluralize}"
|
505
|
-
|
506
|
-
{
|
507
|
-
path: "/#{local_route}#{url_format}",
|
508
|
-
description: description
|
509
|
-
}
|
510
|
-
end.compact
|
511
|
-
|
512
|
-
output = {
|
513
|
-
apiVersion: api_version,
|
514
|
-
swaggerVersion: '1.2',
|
515
|
-
produces: @@documentation_class.content_types_for(target_class),
|
516
|
-
apis: namespace_routes_array,
|
517
|
-
info: @@documentation_class.parse_info(extra_info)
|
518
|
-
}
|
519
|
-
|
520
|
-
output[:authorizations] = authorizations unless authorizations.nil? || authorizations.empty?
|
521
|
-
|
522
|
-
output
|
523
|
-
end
|
524
|
-
|
525
|
-
desc specific_api_doc.delete(:desc), { params: {
|
526
|
-
'name' => {
|
527
|
-
desc: 'Resource name of mounted API',
|
528
|
-
type: 'string',
|
529
|
-
required: true
|
530
|
-
}
|
531
|
-
}.merge(specific_api_doc.delete(:params) || {}) }.merge(specific_api_doc)
|
532
|
-
|
533
|
-
get "#{@@mount_path}/:name" do
|
534
|
-
header['Access-Control-Allow-Origin'] = '*'
|
535
|
-
header['Access-Control-Request-Method'] = '*'
|
536
|
-
|
537
|
-
models = []
|
538
|
-
routes = target_class.combined_namespace_routes[params[:name]]
|
539
|
-
error!('Not Found', 404) unless routes
|
540
|
-
|
541
|
-
visible_ops = routes.reject do |route|
|
542
|
-
route.route_hidden.respond_to?(:call) ? route.route_hidden.call : route.route_hidden
|
543
|
-
end
|
544
|
-
|
545
|
-
ops = visible_ops.group_by do |route|
|
546
|
-
@@documentation_class.parse_path(route.route_path, api_version)
|
547
|
-
end
|
548
|
-
|
549
|
-
error!('Not Found', 404) unless ops.any?
|
550
|
-
|
551
|
-
apis = []
|
552
|
-
|
553
|
-
ops.each do |path, op_routes|
|
554
|
-
operations = op_routes.map do |route|
|
555
|
-
notes = @@documentation_class.as_markdown(route.route_notes)
|
556
|
-
|
557
|
-
http_codes = @@documentation_class.parse_http_codes(route.route_http_codes, models)
|
558
|
-
|
559
|
-
models |= @@models if @@models.present?
|
560
|
-
|
561
|
-
models |= Array(route.route_entity) if route.route_entity.present?
|
562
|
-
|
563
|
-
models = @@documentation_class.models_with_included_presenters(models.flatten.compact)
|
564
|
-
|
565
|
-
operation = {
|
566
|
-
notes: notes.to_s,
|
567
|
-
summary: route.route_description || '',
|
568
|
-
nickname: route.route_nickname || (route.route_method + route.route_path.gsub(/[\/:\(\)\.]/, '-')),
|
569
|
-
method: route.route_method,
|
570
|
-
parameters: @@documentation_class.parse_header_params(route.route_headers) + @@documentation_class.parse_params(route.route_params, route.route_path, route.route_method),
|
571
|
-
type: 'void'
|
572
|
-
}
|
573
|
-
operation[:authorizations] = route.route_authorizations unless route.route_authorizations.nil? || route.route_authorizations.empty?
|
574
|
-
if operation[:parameters].any? { | param | param[:type] == 'File' }
|
575
|
-
operation.merge!(consumes: ['multipart/form-data'])
|
576
|
-
end
|
577
|
-
operation.merge!(responseMessages: http_codes) unless http_codes.empty?
|
578
|
-
|
579
|
-
if route.route_entity
|
580
|
-
type = @@documentation_class.parse_entity_name(Array(route.route_entity).first)
|
581
|
-
operation.merge!('type' => type)
|
582
|
-
end
|
583
|
-
|
584
|
-
operation[:nickname] = route.route_nickname if route.route_nickname
|
585
|
-
operation
|
586
|
-
end.compact
|
587
|
-
apis << {
|
588
|
-
path: path,
|
589
|
-
operations: operations
|
590
|
-
}
|
591
|
-
end
|
592
|
-
|
593
|
-
# use custom resource naming if available
|
594
|
-
if target_class.combined_namespace_identifiers.key? params[:name]
|
595
|
-
resource_path = target_class.combined_namespace_identifiers[params[:name]]
|
596
|
-
else
|
597
|
-
resource_path = params[:name]
|
598
|
-
end
|
599
|
-
api_description = {
|
600
|
-
apiVersion: api_version,
|
601
|
-
swaggerVersion: '1.2',
|
602
|
-
resourcePath: "/#{resource_path}",
|
603
|
-
produces: @@documentation_class.content_types_for(target_class),
|
604
|
-
apis: apis
|
605
|
-
}
|
167
|
+
def parse_enum_or_range_values(values)
|
168
|
+
case values
|
169
|
+
when Range
|
170
|
+
parse_range_values(values) if values.first.is_a?(Integer)
|
171
|
+
when Proc
|
172
|
+
values_result = values.call
|
173
|
+
if values_result.is_a?(Range) && values_result.first.is_a?(Integer)
|
174
|
+
parse_range_values(values_result)
|
175
|
+
else
|
176
|
+
{ enum: values_result }
|
177
|
+
end
|
178
|
+
else
|
179
|
+
{ enum: values } if values
|
180
|
+
end
|
181
|
+
end
|
606
182
|
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
api_description[:authorizations] = authorizations if authorizations
|
183
|
+
def parse_range_values(values)
|
184
|
+
{ minimum: values.first, maximum: values.last }
|
185
|
+
end
|
611
186
|
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
end
|
187
|
+
def create_documentation_class
|
188
|
+
Class.new(Grape::API) do
|
189
|
+
extend GrapeSwagger::DocMethods
|
616
190
|
end
|
617
191
|
end
|
618
192
|
end
|
@@ -14,7 +14,7 @@ describe 'API Description' do
|
|
14
14
|
expect(routes.first.route_description).to eq 'Swagger compatible API description'
|
15
15
|
expect(routes.first.route_params).to eq({})
|
16
16
|
expect(routes.last.route_description).to eq 'Swagger compatible API description for specific API'
|
17
|
-
expect(routes.last.route_params).to eq('name' => { desc: 'Resource name of mounted API', type: '
|
17
|
+
expect(routes.last.route_params).to eq('name' => { desc: 'Resource name of mounted API', type: 'String', required: true })
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -34,7 +34,7 @@ describe 'API Description' do
|
|
34
34
|
expect(routes.first.route_params).to eq(x: 1)
|
35
35
|
expect(routes.first.route_xx).to eq(11)
|
36
36
|
expect(routes.last.route_description).to eq 'Second'
|
37
|
-
expect(routes.last.route_params).to eq('name' => { desc: 'Resource name of mounted API', type: '
|
37
|
+
expect(routes.last.route_params).to eq('name' => { desc: 'Resource name of mounted API', type: 'String', required: true }, y: 42)
|
38
38
|
expect(routes.last.route_yy).to eq(4242)
|
39
39
|
end
|
40
40
|
end
|