grape-swagger 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e9e2d67cf94749b685aff4146aa6f831acd41539
4
- data.tar.gz: 272fa686ecf0e978a0f07c41a42486a06ecfd56c
3
+ metadata.gz: 80476f6e354bf1964123a1f592e8a6ca9fd38d54
4
+ data.tar.gz: b9c15ab8ac8cacbd87e69e187a1afd2a838ff7d7
5
5
  SHA512:
6
- metadata.gz: 612b8bff2f52780da77e09b63a929cb035d3cc6f8bcda1a75b426c7267c67e60f86d30eefa149e05f6ec27571317cef910daf53026b7d3394d149ccafb96ce07
7
- data.tar.gz: 22b1bd381a910aedb712fa1efc53b36ded5965ae743dae44527f49208cc23243d8d97f3ff76694d984f06e2ede024d5d2b01feda3356c436a5890a44b3e06930
6
+ metadata.gz: 4f6d6e3da7140010e92a6a94a716572cf2da3093bc2233c172c6a9d0caa420a4108da068d73a145ae901dd61cc2c754c22974881cab22a5959fb55439fdfaeb1
7
+ data.tar.gz: 04db505e755cf5fa4f72684becaa3ad2a580a1f83baf566fb8c7407f2c1b425f809d813b1151c7b286f0f4ed90e587307be53c15f3ab9ef90b579af4d5c8d803
data/.rubocop_todo.yml CHANGED
@@ -1,42 +1,42 @@
1
1
  # This configuration was generated by `rubocop --auto-gen-config`
2
- # on 2014-11-29 13:48:01 -0500 using RuboCop version 0.27.0.
2
+ # on 2015-02-26 15:04:26 +0100 using RuboCop version 0.27.0.
3
3
  # The point is for the user to remove these configuration records
4
4
  # one by one as the offenses are removed from the code base.
5
5
  # Note that changes in the inspected code, or installation of new
6
6
  # versions of RuboCop, may require this file to be generated again.
7
7
 
8
- # Offense count: 8
8
+ # Offense count: 9
9
9
  Metrics/AbcSize:
10
- Max: 327
10
+ Max: 347
11
11
 
12
12
  # Offense count: 1
13
13
  # Configuration parameters: CountComments.
14
14
  Metrics/ClassLength:
15
- Max: 397
15
+ Max: 485
16
16
 
17
- # Offense count: 5
17
+ # Offense count: 6
18
18
  Metrics/CyclomaticComplexity:
19
- Max: 93
19
+ Max: 99
20
20
 
21
- # Offense count: 195
21
+ # Offense count: 289
22
22
  # Configuration parameters: AllowURI, URISchemes.
23
23
  Metrics/LineLength:
24
24
  Max: 254
25
25
 
26
- # Offense count: 13
26
+ # Offense count: 17
27
27
  # Configuration parameters: CountComments.
28
28
  Metrics/MethodLength:
29
- Max: 359
29
+ Max: 368
30
30
 
31
- # Offense count: 4
31
+ # Offense count: 5
32
32
  Metrics/PerceivedComplexity:
33
- Max: 96
33
+ Max: 101
34
34
 
35
- # Offense count: 7
35
+ # Offense count: 8
36
36
  Style/ClassVars:
37
37
  Enabled: false
38
38
 
39
- # Offense count: 69
39
+ # Offense count: 76
40
40
  Style/Documentation:
41
41
  Enabled: false
42
42
 
data/.travis.yml CHANGED
@@ -13,4 +13,5 @@ env:
13
13
  - GRAPE_VERSION=0.8.0
14
14
  - GRAPE_VERSION=0.9.0
15
15
  - GRAPE_VERSION=0.10.0
16
+ - GRAPE_VERSION=0.10.1
16
17
  - GRAPE_VERSION=HEAD
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ### 0.10.0 (March 10, 2015)
2
+
3
+ #### Features
4
+
5
+ * [#217](https://github.com/tim-vandecasteele/grape-swagger/pull/217): Support Array of entities for proper rendering of grape-entity input dependencies - [@swistaczek](https://github.com/swistaczek).
6
+ * [#214](https://github.com/tim-vandecasteele/grape-swagger/pull/214): Allow anything that responds to `call` to be used in `:hidden` - [@zbelzer](https://github.com/zbelzer).
7
+ * [#196](https://github.com/tim-vandecasteele/grape-swagger/pull/196): If `:type` is omitted, see if it's available in `:using` - [@jhollinger](https://github.com/jhollinger).
8
+ * [#200](https://github.com/tim-vandecasteele/grape-swagger/pull/200): Treat `type: Symbol` as string form parameter - [@ypresto](https://github.com/ypresto).
9
+ * [#207](https://github.com/tim-vandecasteele/grape-swagger/pull/207): Support grape `mutually_exclusive` - [@mintuhouse](https://github.com/mintuhouse).
10
+ * [#220](https://github.com/tim-vandecasteele/grape-swagger/pull/220): Support standalone appearance of namespace routes with a custom name instead of forced nesting - [@croeck](https://github.com/croeck).
11
+
12
+ #### Fixes
13
+
14
+ * [#221](https://github.com/tim-vandecasteele/grape-swagger/pull/221): Fixed group parameters' name with type Array - [@u2](https://github.com/u2).
15
+ * [#211](https://github.com/tim-vandecasteele/grape-swagger/pull/211): Fixed the dependency, just `require 'grape'` - [@u2](https://github.com/u2).
16
+ * [#210](https://github.com/tim-vandecasteele/grape-swagger/pull/210): Fixed the range `:values` option, now exposed as `enum` parameters - [@u2](https://github.com/u2).
17
+ * [#208](https://github.com/tim-vandecasteele/grape-swagger/pull/208): Fixed `Float` parameters, exposed as Swagger `float` types - [@u2](https://github.com/u2).
18
+ * [#216](https://github.com/tim-vandecasteele/grape-swagger/pull/216), [#192](https://github.com/tim-vandecasteele/grape-swagger/issues/192), [#189](https://github.com/tim-vandecasteele/grape-swagger/issues/189): Fixed API route paths matching for root endpoints with `grape ~> 0.10.0`, specific `format` and `:path` versioning - [@dm1try](https://github.com/dm1try), [@minch](https://github.com/minch).
19
+
1
20
  ### 0.9.0 (December 19, 2014)
2
21
 
3
22
  * [#91](https://github.com/tim-vandecasteele/grape-swagger/issues/91): Fixed empty field for group parameters' name with type hash or Array - [@dukedave](https://github.com/dukedave).
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # grape-swagger
2
2
 
3
- [![Build Status](https://travis-ci.org/tim-vandecasteele/grape-swagger.svg?branch=master)](https://travis-ci.org/tim-vandecasteele/grape-swagger)
3
+ [![Gem Version](http://img.shields.io/gem/v/grape-swagger.svg)](http://badge.fury.io/rb/grape-swagger)
4
+ [![Build Status](http://img.shields.io/travis/tim-vandecasteele/grape-swagger.svg)](https://travis-ci.org/tim-vandecasteele/grape-swagger)
5
+ [![Code Climate](https://codeclimate.com/github/tim-vandecasteele/grape-swagger.svg)](https://codeclimate.com/github/tim-vandecasteele/grape-swagger)
4
6
 
5
7
  ## What is grape-swagger?
6
8
 
@@ -174,6 +176,13 @@ You can hide an endpoint by adding ```hidden: true``` in the description of the
174
176
  desc 'Hide this endpoint', hidden: true
175
177
  ```
176
178
 
179
+ Endpoints can be conditionally hidden by providing a callable object such as a lambda which evaluates to the desired
180
+ state:
181
+
182
+ ``` ruby
183
+ desc 'Conditionally hide this endpoint', hidden: lambda { ENV['EXPERIMENTAL'] != 'true' }
184
+ ```
185
+
177
186
  ## Overriding Auto-Generated Nicknames
178
187
 
179
188
  You can specify a swagger nickname to use instead of the auto generated name by adding `:nickname 'string'``` in the description of the endpoint.
@@ -182,6 +191,44 @@ You can specify a swagger nickname to use instead of the auto generated name by
182
191
  desc 'Get a full list of pets', nickname: 'getAllPets'
183
192
  ```
184
193
 
194
+ ## Expose nested namespace as standalone route
195
+ Use the `nested: false` property in the `swagger` option to make nested namespaces appear as standalone resources.
196
+ This option can help to structure and keep the swagger schema simple.
197
+
198
+ namespace 'store/order', desc: 'Order operations within a store', swagger: { nested: false } do
199
+ get :order_id do
200
+ ...
201
+ end
202
+ end
203
+
204
+ All routes that belong to this namespace (here: the `GET /order_id`) will then be assigned to the `store_order` route instead of the `store` resource route.
205
+
206
+ It is also possible to expose a namspace within another already exposed namespace:
207
+
208
+ namespace 'store/order', desc: 'Order operations within a store', swagger: { nested: false } do
209
+ get :order_id do
210
+ ...
211
+ end
212
+ namespace 'actions', desc: 'Order actions' do, nested: false
213
+ get 'evaluate' do
214
+ ...
215
+ end
216
+ end
217
+ end
218
+
219
+ Here, the `GET /order_id` appears as operation of the `store_order` resource and the `GET /evaluate` as operation of the `store_orders_actions` route.
220
+
221
+ ### With a custom name
222
+ Auto generated names for the standalone version of complex nested resource do not have a nice look.
223
+ You can set a custom name with the `name` property inside the `swagger` option, but only if the namespace gets exposed as standalone route.
224
+ The name should not contain whitespaces or any other special characters due to further issues within swagger-ui.
225
+
226
+ namespace 'store/order', desc: 'Order operations within a store', swagger: { nested: false, name: 'Store-orders' } do
227
+ get :order_id do
228
+ ...
229
+ end
230
+ end
231
+
185
232
  ## Grape Entities
186
233
 
187
234
  Add the [grape-entity](https://github.com/agileanimal/grape-entity) gem to our Gemfile.
@@ -227,7 +274,7 @@ end
227
274
 
228
275
  ### Relationships
229
276
 
230
- Put the full name of the relationship's class in `type`, leaving out any modules named `Entities` or `Entity`. So for the entity class `API::Entities::Address`, you would put `type: 'API::Address'`.
277
+ You may safely omit `type` from relationships, as it can be inferred. However, if you need to specify or override it, use the full name of the class leaving out any modules named `Entities` or `Entity`.
231
278
 
232
279
  #### 1xN
233
280
 
data/UPGRADING.md CHANGED
@@ -1,6 +1,26 @@
1
1
  Upgrading Grape-swagger
2
2
  =======================
3
3
 
4
+ ### Upgrading to >= 0.9.0
5
+
6
+ #### Changes in Configuration
7
+
8
+ If you're using [grape-swagger-rails](https://github.com/BrandyMint/grape-swagger-rails), remove the `.json` extension from `GrapeSwaggerRails.options.url`.
9
+
10
+ For example, change
11
+
12
+ ```ruby
13
+ GrapeSwaggerRails.options.url = '/api/v1/swagger_doc.json'
14
+ ```
15
+
16
+ to
17
+
18
+ ```ruby
19
+ GrapeSwaggerRails.options.url = '/api/v1/swagger_doc'
20
+ ```
21
+
22
+ See [#187](https://github.com/tim-vandecasteele/grape-swagger/issues/187) for more information.
23
+
4
24
  ### Upgrading to >= 0.8.0
5
25
 
6
26
  #### Changes in Configuration
data/lib/grape-swagger.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'grape'
1
2
  require 'grape-swagger/version'
2
3
  require 'grape-swagger/errors'
3
4
  require 'grape-swagger/markdown'
@@ -7,7 +8,7 @@ require 'grape-swagger/markdown/redcarpet_adapter'
7
8
  module Grape
8
9
  class API
9
10
  class << self
10
- attr_reader :combined_routes, :combined_namespaces
11
+ attr_reader :combined_routes, :combined_namespaces, :combined_namespace_routes, :combined_namespace_identifiers
11
12
 
12
13
  def add_swagger_documentation(options = {})
13
14
  documentation_class = create_documentation_class
@@ -20,7 +21,7 @@ module Grape
20
21
  route_path = route.route_path
21
22
  route_match = route_path.split(/^.*?#{route.route_prefix.to_s}/).last
22
23
  next unless route_match
23
- route_match = route_match.match('\/([\w|-]*?)[\.\/\(]') || route_match.match('\/([\w|-]*)')
24
+ route_match = route_match.match('\/([\w|-]*?)[\.\/\(]') || route_match.match('\/([\w|-]*)$')
24
25
  next unless route_match
25
26
  resource = route_match.captures.first
26
27
  next if resource.empty?
@@ -32,6 +33,13 @@ module Grape
32
33
 
33
34
  @combined_namespaces = {}
34
35
  combine_namespaces(self)
36
+
37
+ @combined_namespace_routes = {}
38
+ @combined_namespace_identifiers = {}
39
+ combine_namespace_routes(@combined_namespaces)
40
+
41
+ exclusive_route_keys = @combined_routes.keys - @combined_namespaces.keys
42
+ exclusive_route_keys.each { |key| @combined_namespace_routes[key] = @combined_routes[key] }
35
43
  documentation_class
36
44
  end
37
45
 
@@ -44,12 +52,112 @@ module Grape
44
52
  else
45
53
  endpoint.settings.stack.last[:namespace]
46
54
  end
47
- @combined_namespaces[ns.space] = ns if ns
55
+ # use the full namespace here (not the latest level only)
56
+ # and strip leading slash
57
+ @combined_namespaces[endpoint.namespace.sub(/^\//, '')] = ns if ns
48
58
 
49
59
  combine_namespaces(endpoint.options[:app]) if endpoint.options[:app]
50
60
  end
51
61
  end
52
62
 
63
+ def combine_namespace_routes(namespaces)
64
+ # iterate over each single namespace
65
+ namespaces.each do |name, namespace|
66
+ # get the parent route for the namespace
67
+ parent_route_name = name.match(%r{^/?([^/]*).*$})[1]
68
+ parent_route = @combined_routes[parent_route_name]
69
+ # fetch all routes that are within the current namespace
70
+ namespace_routes = parent_route.collect do |route|
71
+ route if (route.route_path.start_with?("/#{name}") || route.route_path.start_with?("/:version/#{name}")) &&
72
+ (route.instance_variable_get(:@options)[:namespace] == "/#{name}" || route.instance_variable_get(:@options)[:namespace] == "/:version/#{name}")
73
+ end.compact
74
+
75
+ if namespace.options.key?(:swagger) && namespace.options[:swagger][:nested] == false
76
+ # Namespace shall appear as standalone resource, use specified name or use normalized path as name
77
+ if namespace.options[:swagger].key?(:name)
78
+ identifier = namespace.options[:swagger][:name].gsub(/ /, '-')
79
+ else
80
+ identifier = name.gsub(/_/, '-').gsub(/\//, '_')
81
+ end
82
+ @combined_namespace_identifiers[identifier] = name
83
+ @combined_namespace_routes[identifier] = namespace_routes
84
+
85
+ # get all nested namespaces below the current namespace
86
+ sub_namespaces = standalone_sub_namespaces(name, namespaces)
87
+ # convert namespace to route names
88
+ sub_ns_paths = sub_namespaces.collect { |ns_name, _| "/#{ns_name}" }
89
+ sub_ns_paths_versioned = sub_namespaces.collect { |ns_name, _| "/:version/#{ns_name}" }
90
+ # get the actual route definitions for the namespace path names
91
+ sub_routes = parent_route.collect do |route|
92
+ route if sub_ns_paths.include?(route.instance_variable_get(:@options)[:namespace]) || sub_ns_paths_versioned.include?(route.instance_variable_get(:@options)[:namespace])
93
+ end.compact
94
+ # add all determined routes of the sub namespaces to standalone resource
95
+ @combined_namespace_routes[identifier].push(*sub_routes)
96
+ else
97
+ # default case when not explicitly specified or nested == true
98
+ standalone_namespaces = namespaces.reject { |_, ns| !ns.options.key?(:swagger) || !ns.options[:swagger].key?(:nested) || ns.options[:swagger][:nested] != false }
99
+ parent_standalone_namespaces = standalone_namespaces.reject { |ns_name, _| !name.start_with?(ns_name) }
100
+ # add only to the main route if the namespace is not within any other namespace appearing as standalone resource
101
+ if parent_standalone_namespaces.empty?
102
+ # 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)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def standalone_sub_namespaces(name, namespaces)
111
+ # assign all nested namespace routes to this resource, too
112
+ # (unless they are assigned to another standalone namespace themselves)
113
+ sub_namespaces = {}
114
+ # fetch all namespaces that are children of the current namespace
115
+ namespaces.each { |ns_name, ns| sub_namespaces[ns_name] = ns if ns_name.start_with?(name) && ns_name != name }
116
+ # remove the sub namespaces if they are assigned to another standalone namespace themselves
117
+ sub_namespaces.each do |sub_name, sub_ns|
118
+ # skip if sub_ns is standalone, too
119
+ next unless sub_ns.options.key?(:swagger) && sub_ns.options[:swagger][:nested] == false
120
+ # remove all namespaces that are nested below this standalone sub_ns
121
+ sub_namespaces.each { |sub_sub_name, _| sub_namespaces.delete(sub_sub_name) if sub_sub_name.start_with?(sub_name) }
122
+ end
123
+ sub_namespaces
124
+ end
125
+
126
+ def get_non_nested_params(params)
127
+ # Duplicate the params as we are going to modify them
128
+ dup_params = params.each_with_object(Hash.new) do |(param, value), dparams|
129
+ dparams[param] = value.dup
130
+ end
131
+
132
+ dup_params.reject do |param, value|
133
+ is_nested_param = /^#{ Regexp.quote param }\[.+\]$/
134
+ 0 < dup_params.count do |p, _|
135
+ match = p.match(is_nested_param)
136
+ dup_params[p][:required] = false if match && !value[:required]
137
+ match
138
+ end
139
+ end
140
+ end
141
+
142
+ def parse_array_params(params)
143
+ modified_params = {}
144
+ array_param = nil
145
+ params.each_key do |k|
146
+ if params[k].is_a?(Hash) && params[k][:type] == 'Array'
147
+ array_param = k
148
+ else
149
+ new_key = k
150
+ unless array_param.nil?
151
+ if k.to_s.start_with?(array_param.to_s + '[')
152
+ new_key = array_param.to_s + '[]' + k.to_s.split(array_param)[1]
153
+ end
154
+ end
155
+ modified_params[new_key] = params[k]
156
+ end
157
+ end
158
+ modified_params
159
+ end
160
+
53
161
  def create_documentation_class
54
162
  Class.new(Grape::API) do
55
163
  class << self
@@ -64,10 +172,9 @@ module Grape
64
172
  def parse_params(params, path, method)
65
173
  params ||= []
66
174
 
67
- non_nested_parent_params = params.reject do |param, _|
68
- is_nested_param = /^#{ Regexp.quote param }\[.+\]$/
69
- params.keys.any? { |p| p.match is_nested_param }
70
- end
175
+ parsed_array_params = parse_array_params(params)
176
+
177
+ non_nested_parent_params = get_non_nested_params(parsed_array_params)
71
178
 
72
179
  non_nested_parent_params.map do |param, value|
73
180
  items = {}
@@ -80,7 +187,7 @@ module Grape
80
187
  'File'
81
188
  when 'Virtus::Attribute::Boolean'
82
189
  'boolean'
83
- when 'Boolean', 'Date', 'Integer', 'String'
190
+ when 'Boolean', 'Date', 'Integer', 'String', 'Float'
84
191
  raw_data_type.downcase
85
192
  when 'BigDecimal'
86
193
  'long'
@@ -88,6 +195,8 @@ module Grape
88
195
  'dateTime'
89
196
  when 'Numeric'
90
197
  'double'
198
+ when 'Symbol'
199
+ 'string'
91
200
  else
92
201
  @@documentation_class.parse_entity_name(raw_data_type)
93
202
  end
@@ -96,6 +205,7 @@ module Grape
96
205
  default_value = value.is_a?(Hash) ? value[:default] : nil
97
206
  is_array = value.is_a?(Hash) ? (value[:is_array] || false) : false
98
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)
99
209
  enum_values = enum_values.call if enum_values && enum_values.is_a?(Proc)
100
210
 
101
211
  if value.is_a?(Hash) && value.key?(:param_type)
@@ -220,11 +330,17 @@ module Grape
220
330
 
221
331
  required << property_name.to_s if p.delete(:required)
222
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
+
223
339
  if p.delete(:is_array)
224
- p[:items] = generate_typeref(p[:type])
340
+ p[:items] = generate_typeref(type)
225
341
  p[:type] = 'array'
226
342
  else
227
- p.merge! generate_typeref(p.delete(:type))
343
+ p.merge! generate_typeref(type)
228
344
  end
229
345
 
230
346
  # rename Grape Entity's "desc" to "description"
@@ -239,7 +355,6 @@ module Grape
239
355
  end
240
356
 
241
357
  properties[property_name] = p
242
-
243
358
  end
244
359
 
245
360
  result[name] = {
@@ -372,20 +487,21 @@ module Grape
372
487
  header['Access-Control-Allow-Origin'] = '*'
373
488
  header['Access-Control-Request-Method'] = '*'
374
489
 
375
- routes = target_class.combined_routes
376
490
  namespaces = target_class.combined_namespaces
491
+ namespace_routes = target_class.combined_namespace_routes
377
492
 
378
493
  if @@hide_documentation_path
379
- routes.reject! { |route, _value| "/#{route}/".index(@@documentation_class.parse_path(@@mount_path, nil) << '/') == 0 }
494
+ namespace_routes.reject! { |route, _value| "/#{route}/".index(@@documentation_class.parse_path(@@mount_path, nil) << '/') == 0 }
380
495
  end
381
496
 
382
- routes_array = routes.keys.map do |local_route|
383
- next if routes[local_route].all?(&:route_hidden)
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 }
384
499
 
385
500
  url_format = '.{format}' unless @@hide_format
386
501
 
387
- description = namespaces[local_route] && namespaces[local_route].options[:desc]
388
- description ||= "Operations about #{local_route.pluralize}"
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}"
389
505
 
390
506
  {
391
507
  path: "/#{local_route}#{url_format}",
@@ -397,7 +513,7 @@ module Grape
397
513
  apiVersion: api_version,
398
514
  swaggerVersion: '1.2',
399
515
  produces: @@documentation_class.content_types_for(target_class),
400
- apis: routes_array,
516
+ apis: namespace_routes_array,
401
517
  info: @@documentation_class.parse_info(extra_info)
402
518
  }
403
519
 
@@ -419,10 +535,14 @@ module Grape
419
535
  header['Access-Control-Request-Method'] = '*'
420
536
 
421
537
  models = []
422
- routes = target_class.combined_routes[params[:name]]
538
+ routes = target_class.combined_namespace_routes[params[:name]]
423
539
  error!('Not Found', 404) unless routes
424
540
 
425
- ops = routes.reject(&:route_hidden).group_by do |route|
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|
426
546
  @@documentation_class.parse_path(route.route_path, api_version)
427
547
  end
428
548
 
@@ -438,7 +558,7 @@ module Grape
438
558
 
439
559
  models |= @@models if @@models.present?
440
560
 
441
- models |= [route.route_entity] if route.route_entity.present?
561
+ models |= Array(route.route_entity) if route.route_entity.present?
442
562
 
443
563
  models = @@documentation_class.models_with_included_presenters(models.flatten.compact)
444
564
 
@@ -457,7 +577,7 @@ module Grape
457
577
  operation.merge!(responseMessages: http_codes) unless http_codes.empty?
458
578
 
459
579
  if route.route_entity
460
- type = @@documentation_class.parse_entity_name(route.route_entity)
580
+ type = @@documentation_class.parse_entity_name(Array(route.route_entity).first)
461
581
  operation.merge!('type' => type)
462
582
  end
463
583
 
@@ -470,10 +590,16 @@ module Grape
470
590
  }
471
591
  end
472
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
473
599
  api_description = {
474
600
  apiVersion: api_version,
475
601
  swaggerVersion: '1.2',
476
- resourcePath: "/#{params[:name]}",
602
+ resourcePath: "/#{resource_path}",
477
603
  produces: @@documentation_class.content_types_for(target_class),
478
604
  apis: apis
479
605
  }