Merge pull request #220 from croeck/namespace-nesting
Standalone appearance of nested routes
This commit is contained in:
commit
a0d446daae
|
|
@ -1,42 +1,42 @@
|
|||
# This configuration was generated by `rubocop --auto-gen-config`
|
||||
# on 2015-02-12 10:19:50 -0600 using RuboCop version 0.27.0.
|
||||
# on 2015-02-26 15:04:26 +0100 using RuboCop version 0.27.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 8
|
||||
# Offense count: 9
|
||||
Metrics/AbcSize:
|
||||
Max: 334
|
||||
Max: 346
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/ClassLength:
|
||||
Max: 413
|
||||
Max: 466
|
||||
|
||||
# Offense count: 5
|
||||
# Offense count: 6
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 97
|
||||
Max: 99
|
||||
|
||||
# Offense count: 232
|
||||
# Offense count: 289
|
||||
# Configuration parameters: AllowURI, URISchemes.
|
||||
Metrics/LineLength:
|
||||
Max: 254
|
||||
|
||||
# Offense count: 16
|
||||
# Offense count: 17
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/MethodLength:
|
||||
Max: 361
|
||||
Max: 367
|
||||
|
||||
# Offense count: 4
|
||||
# Offense count: 5
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 98
|
||||
Max: 101
|
||||
|
||||
# Offense count: 8
|
||||
Style/ClassVars:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 70
|
||||
# Offense count: 76
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,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).
|
||||
* [#200](https://github.com/tim-vandecasteele/grape-swagger/pull/200): Treat `type: Symbol` as string form parameter - [@ypresto](https://github.com/ypresto).
|
||||
* [#207](https://github.com/tim-vandecasteele/grape-swagger/pull/207): Support grape `mutually_exclusive` - [@mintuhouse](https://github.com/mintuhouse).
|
||||
* [#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).
|
||||
|
||||
#### Fixes
|
||||
|
||||
|
|
|
|||
38
README.md
38
README.md
|
|
@ -191,6 +191,44 @@ You can specify a swagger nickname to use instead of the auto generated name by
|
|||
desc 'Get a full list of pets', nickname: 'getAllPets'
|
||||
```
|
||||
|
||||
## Expose nested namespace as standalone route
|
||||
Use the `nested: false` property in the `swagger` option to make nested namespaces appear as standalone resources.
|
||||
This option can help to structure and keep the swagger schema simple.
|
||||
|
||||
namespace 'store/order', desc: 'Order operations within a store', swagger: { nested: false } do
|
||||
get :order_id do
|
||||
...
|
||||
end
|
||||
end
|
||||
|
||||
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.
|
||||
|
||||
It is also possible to expose a namspace within another already exposed namespace:
|
||||
|
||||
namespace 'store/order', desc: 'Order operations within a store', swagger: { nested: false } do
|
||||
get :order_id do
|
||||
...
|
||||
end
|
||||
namespace 'actions', desc: 'Order actions' do, nested: false
|
||||
get 'evaluate' do
|
||||
...
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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.
|
||||
|
||||
### With a custom name
|
||||
Auto generated names for the standalone version of complex nested resource do not have a nice look.
|
||||
You can set a custom name with the `name` property inside the `swagger` option, but only if the namespace gets exposed as standalone route.
|
||||
The name should not contain whitespaces or any other special characters due to further issues within swagger-ui.
|
||||
|
||||
namespace 'store/order', desc: 'Order operations within a store', swagger: { nested: false, name: 'Store-orders' } do
|
||||
get :order_id do
|
||||
...
|
||||
end
|
||||
end
|
||||
|
||||
## Grape Entities
|
||||
|
||||
Add the [grape-entity](https://github.com/agileanimal/grape-entity) gem to our Gemfile.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ require 'grape-swagger/markdown/redcarpet_adapter'
|
|||
module Grape
|
||||
class API
|
||||
class << self
|
||||
attr_reader :combined_routes, :combined_namespaces
|
||||
attr_reader :combined_routes, :combined_namespaces, :combined_namespace_routes, :combined_namespace_identifiers
|
||||
|
||||
def add_swagger_documentation(options = {})
|
||||
documentation_class = create_documentation_class
|
||||
|
|
@ -33,6 +33,13 @@ module Grape
|
|||
|
||||
@combined_namespaces = {}
|
||||
combine_namespaces(self)
|
||||
|
||||
@combined_namespace_routes = {}
|
||||
@combined_namespace_identifiers = {}
|
||||
combine_namespace_routes(@combined_namespaces)
|
||||
|
||||
exclusive_route_keys = @combined_routes.keys - @combined_namespaces.keys
|
||||
exclusive_route_keys.each { |key| @combined_namespace_routes[key] = @combined_routes[key] }
|
||||
documentation_class
|
||||
end
|
||||
|
||||
|
|
@ -45,12 +52,77 @@ module Grape
|
|||
else
|
||||
endpoint.settings.stack.last[:namespace]
|
||||
end
|
||||
@combined_namespaces[ns.space] = ns if ns
|
||||
# use the full namespace here (not the latest level only)
|
||||
# and strip leading slash
|
||||
@combined_namespaces[endpoint.namespace.sub(/^\//, '')] = ns if ns
|
||||
|
||||
combine_namespaces(endpoint.options[:app]) if endpoint.options[:app]
|
||||
end
|
||||
end
|
||||
|
||||
def combine_namespace_routes(namespaces)
|
||||
# iterate over each single namespace
|
||||
namespaces.each do |name, namespace|
|
||||
# get the parent route for the namespace
|
||||
parent_route_name = name.match(%r{^/?([^/]*).*$})[1]
|
||||
parent_route = @combined_routes[parent_route_name]
|
||||
# fetch all routes that are within the current namespace
|
||||
namespace_routes = parent_route.collect do |route|
|
||||
route if (route.route_path.start_with?("/#{name}") || route.route_path.start_with?("/:version/#{name}")) &&
|
||||
(route.instance_variable_get(:@options)[:namespace] == "/#{name}" || route.instance_variable_get(:@options)[:namespace] == "/:version/#{name}")
|
||||
end.compact
|
||||
|
||||
if namespace.options.key?(:swagger) && namespace.options[:swagger][:nested] == false
|
||||
# Namespace shall appear as standalone resource, use specified name or use normalized path as name
|
||||
if namespace.options[:swagger].key?(:name)
|
||||
identifier = namespace.options[:swagger][:name].gsub(/ /, '-')
|
||||
else
|
||||
identifier = name.gsub(/_/, '-').gsub(/\//, '_')
|
||||
end
|
||||
@combined_namespace_identifiers[identifier] = name
|
||||
@combined_namespace_routes[identifier] = namespace_routes
|
||||
|
||||
# get all nested namespaces below the current namespace
|
||||
sub_namespaces = standalone_sub_namespaces(name, namespaces)
|
||||
# convert namespace to route names
|
||||
sub_ns_paths = sub_namespaces.collect { |ns_name, _| "/#{ns_name}" }
|
||||
sub_ns_paths_versioned = sub_namespaces.collect { |ns_name, _| "/:version/#{ns_name}" }
|
||||
# get the actual route definitions for the namespace path names
|
||||
sub_routes = parent_route.collect do |route|
|
||||
route if sub_ns_paths.include?(route.instance_variable_get(:@options)[:namespace]) || sub_ns_paths_versioned.include?(route.instance_variable_get(:@options)[:namespace])
|
||||
end.compact
|
||||
# add all determined routes of the sub namespaces to standalone resource
|
||||
@combined_namespace_routes[identifier].push(*sub_routes)
|
||||
else
|
||||
# default case when not explicitly specified or nested == true
|
||||
standalone_namespaces = namespaces.reject { |_, ns| !ns.options.key?(:swagger) || !ns.options[:swagger].key?(:nested) || ns.options[:swagger][:nested] != false }
|
||||
parent_standalone_namespaces = standalone_namespaces.reject { |ns_name, _| !name.start_with?(ns_name) }
|
||||
# add only to the main route if the namespace is not within any other namespace appearing as standalone resource
|
||||
if parent_standalone_namespaces.empty?
|
||||
# default option, append namespace methods to parent route
|
||||
@combined_namespace_routes[parent_route_name] = [] unless @combined_namespace_routes.key?(parent_route_name)
|
||||
@combined_namespace_routes[parent_route_name].push(*namespace_routes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def standalone_sub_namespaces(name, namespaces)
|
||||
# assign all nested namespace routes to this resource, too
|
||||
# (unless they are assigned to another standalone namespace themselves)
|
||||
sub_namespaces = {}
|
||||
# fetch all namespaces that are children of the current namespace
|
||||
namespaces.each { |ns_name, ns| sub_namespaces[ns_name] = ns if ns_name.start_with?(name) && ns_name != name }
|
||||
# remove the sub namespaces if they are assigned to another standalone namespace themselves
|
||||
sub_namespaces.each do |sub_name, sub_ns|
|
||||
# skip if sub_ns is standalone, too
|
||||
next unless sub_ns.options.key?(:swagger) && sub_ns.options[:swagger][:nested] == false
|
||||
# remove all namespaces that are nested below this standalone sub_ns
|
||||
sub_namespaces.each { |sub_sub_name, _| sub_namespaces.delete(sub_sub_name) if sub_sub_name.start_with?(sub_name) }
|
||||
end
|
||||
sub_namespaces
|
||||
end
|
||||
|
||||
def get_non_nested_params(params)
|
||||
# Duplicate the params as we are going to modify them
|
||||
dup_params = params.each_with_object(Hash.new) do |(param, value), dparams|
|
||||
|
|
@ -394,20 +466,21 @@ module Grape
|
|||
header['Access-Control-Allow-Origin'] = '*'
|
||||
header['Access-Control-Request-Method'] = '*'
|
||||
|
||||
routes = target_class.combined_routes
|
||||
namespaces = target_class.combined_namespaces
|
||||
namespace_routes = target_class.combined_namespace_routes
|
||||
|
||||
if @@hide_documentation_path
|
||||
routes.reject! { |route, _value| "/#{route}/".index(@@documentation_class.parse_path(@@mount_path, nil) << '/') == 0 }
|
||||
namespace_routes.reject! { |route, _value| "/#{route}/".index(@@documentation_class.parse_path(@@mount_path, nil) << '/') == 0 }
|
||||
end
|
||||
|
||||
routes_array = routes.keys.map do |local_route|
|
||||
next if routes[local_route].map(&:route_hidden).all? { |value| value.respond_to?(:call) ? value.call : value }
|
||||
namespace_routes_array = namespace_routes.keys.map do |local_route|
|
||||
next if namespace_routes[local_route].map(&:route_hidden).all? { |value| value.respond_to?(:call) ? value.call : value }
|
||||
|
||||
url_format = '.{format}' unless @@hide_format
|
||||
|
||||
description = namespaces[local_route] && namespaces[local_route].options[:desc]
|
||||
description ||= "Operations about #{local_route.pluralize}"
|
||||
original_namespace_name = target_class.combined_namespace_identifiers.key?(local_route) ? target_class.combined_namespace_identifiers[local_route] : local_route
|
||||
description = namespaces[original_namespace_name] && namespaces[original_namespace_name].options[:desc]
|
||||
description ||= "Operations about #{original_namespace_name.pluralize}"
|
||||
|
||||
{
|
||||
path: "/#{local_route}#{url_format}",
|
||||
|
|
@ -419,7 +492,7 @@ module Grape
|
|||
apiVersion: api_version,
|
||||
swaggerVersion: '1.2',
|
||||
produces: @@documentation_class.content_types_for(target_class),
|
||||
apis: routes_array,
|
||||
apis: namespace_routes_array,
|
||||
info: @@documentation_class.parse_info(extra_info)
|
||||
}
|
||||
|
||||
|
|
@ -441,7 +514,7 @@ module Grape
|
|||
header['Access-Control-Request-Method'] = '*'
|
||||
|
||||
models = []
|
||||
routes = target_class.combined_routes[params[:name]]
|
||||
routes = target_class.combined_namespace_routes[params[:name]]
|
||||
error!('Not Found', 404) unless routes
|
||||
|
||||
visible_ops = routes.reject do |route|
|
||||
|
|
@ -496,10 +569,16 @@ module Grape
|
|||
}
|
||||
end
|
||||
|
||||
# use custom resource naming if available
|
||||
if target_class.combined_namespace_identifiers.key? params[:name]
|
||||
resource_path = target_class.combined_namespace_identifiers[params[:name]]
|
||||
else
|
||||
resource_path = params[:name]
|
||||
end
|
||||
api_description = {
|
||||
apiVersion: api_version,
|
||||
swaggerVersion: '1.2',
|
||||
resourcePath: "/#{params[:name]}",
|
||||
resourcePath: "/#{resource_path}",
|
||||
produces: @@documentation_class.content_types_for(target_class),
|
||||
apis: apis
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,215 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'Standalone namespace API' do
|
||||
let(:json_body) { JSON.parse(last_response.body) }
|
||||
|
||||
describe 'with "path" versioning' do
|
||||
before do
|
||||
class StandaloneApiWithPathVersioning < Grape::API
|
||||
format :json
|
||||
version 'v1', using: :path
|
||||
|
||||
namespace :store do
|
||||
get
|
||||
# will be assigned to standalone the namespace below
|
||||
namespace :orders do
|
||||
get :order_id
|
||||
end
|
||||
end
|
||||
|
||||
namespace 'store/orders', swagger: { nested: false } do
|
||||
post :order_idx
|
||||
namespace 'actions' do
|
||||
get 'dummy'
|
||||
end
|
||||
namespace 'actions2', swagger: { nested: false } do
|
||||
get 'dummy2'
|
||||
namespace 'actions22' do
|
||||
get 'dummy22'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace 'store/:store_id/orders', swagger: { nested: false, name: 'specific-store-orders' } do
|
||||
delete :order_id
|
||||
end
|
||||
|
||||
add_swagger_documentation api_version: 'v1'
|
||||
end
|
||||
end
|
||||
|
||||
def app
|
||||
StandaloneApiWithPathVersioning
|
||||
end
|
||||
|
||||
describe 'retrieves swagger-documentation on /swagger_doc' do
|
||||
before { get '/v1/swagger_doc' }
|
||||
|
||||
it 'that contains all api paths' do
|
||||
expect(json_body['apis']).to eq(
|
||||
[
|
||||
{ 'path' => '/store.{format}', 'description' => 'Operations about stores' },
|
||||
{ 'path' => '/store_orders.{format}', 'description' => 'Operations about store/orders' },
|
||||
{ 'path' => '/store_orders_actions2.{format}', 'description' => 'Operations about store/orders/actions2s' },
|
||||
{ 'path' => '/specific-store-orders.{format}', 'description' => 'Operations about store/:store_id/orders' },
|
||||
{ 'path' => '/swagger_doc.{format}', 'description' => 'Operations about swagger_docs' }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/store' do
|
||||
before { get '/v1/swagger_doc/store' }
|
||||
it 'does not include standalone namespaces' do
|
||||
apis = json_body['apis']
|
||||
# shall include 1 route, GET on store
|
||||
expect(apis.length).to eql(1)
|
||||
expect(apis[0]['operations'][0]['method']).to eql('GET')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/store_orders' do
|
||||
before { get '/v1/swagger_doc/store_orders' }
|
||||
it 'does not assign namespaces within standalone namespaces to the general resource' do
|
||||
apis = json_body['apis']
|
||||
# shall include 3 routes, get on store, get with order_id and get dummy on actions
|
||||
expect(apis.length).to eql(3)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/store_orders_actions2' do
|
||||
before { get '/v1/swagger_doc/store_orders_actions2' }
|
||||
it 'does appear as standalone namespace within another standalone namespace' do
|
||||
apis = json_body['apis']
|
||||
# shall include 2 routes, get on dummy2 and get on dummy22
|
||||
expect(apis.length).to eql(2)
|
||||
expect(apis[0]['operations'][0]['method']).to eql('GET')
|
||||
expect(apis[1]['operations'][0]['method']).to eql('GET')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/specific-store-orders' do
|
||||
before { get '/v1/swagger_doc/specific-store-orders' }
|
||||
it 'does show the one route' do
|
||||
apis = json_body['apis']
|
||||
# shall include 1 routes, delete action
|
||||
expect(apis.length).to eql(1)
|
||||
expect(apis[0]['operations'][0]['method']).to eql('DELETE')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/store_orders' do
|
||||
before { get '/v1/swagger_doc/store_orders_actions2' }
|
||||
it 'does work with standalone in standalone namespaces' do
|
||||
apis = json_body['apis']
|
||||
# shall include 2 routes, dummy2 and dummy22
|
||||
expect(apis.length).to eql(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with header versioning' do
|
||||
before do
|
||||
class StandaloneApiWithHeaderVersioning < Grape::API
|
||||
format :json
|
||||
version 'v1', using: :header, vendor: 'grape-swagger'
|
||||
|
||||
namespace :store do
|
||||
get
|
||||
# will be assigned to standalone the namespace below
|
||||
namespace :orders do
|
||||
get :order_id
|
||||
end
|
||||
end
|
||||
|
||||
namespace 'store/orders', swagger: { nested: false } do
|
||||
post :order_idx
|
||||
namespace 'actions' do
|
||||
get 'dummy'
|
||||
end
|
||||
namespace 'actions2', swagger: { nested: false } do
|
||||
get 'dummy2'
|
||||
namespace 'actions22' do
|
||||
get 'dummy22'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace 'store/:store_id/orders', swagger: { nested: false, name: 'specific-store-orders' } do
|
||||
delete :order_id
|
||||
end
|
||||
|
||||
add_swagger_documentation api_version: 'v1'
|
||||
end
|
||||
end
|
||||
|
||||
def app
|
||||
StandaloneApiWithHeaderVersioning
|
||||
end
|
||||
|
||||
describe 'retrieves swagger-documentation on /swagger_doc' do
|
||||
before { get 'swagger_doc' }
|
||||
|
||||
it 'that contains all api paths' do
|
||||
expect(json_body['apis']).to eq(
|
||||
[
|
||||
{ 'path' => '/store.{format}', 'description' => 'Operations about stores' },
|
||||
{ 'path' => '/store_orders.{format}', 'description' => 'Operations about store/orders' },
|
||||
{ 'path' => '/store_orders_actions2.{format}', 'description' => 'Operations about store/orders/actions2s' },
|
||||
{ 'path' => '/specific-store-orders.{format}', 'description' => 'Operations about store/:store_id/orders' },
|
||||
{ 'path' => '/swagger_doc.{format}', 'description' => 'Operations about swagger_docs' }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/store' do
|
||||
before { get '/swagger_doc/store' }
|
||||
it 'does not include standalone namespaces' do
|
||||
apis = json_body['apis']
|
||||
# shall include 1 route, GET on store
|
||||
expect(apis.length).to eql(1)
|
||||
expect(apis[0]['operations'][0]['method']).to eql('GET')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/store_orders' do
|
||||
before { get '/swagger_doc/store_orders' }
|
||||
it 'does not assign namespaces within standalone namespaces to the general resource' do
|
||||
apis = json_body['apis']
|
||||
# shall include 3 routes, get on store, get with order_id and get dummy on actions
|
||||
expect(apis.length).to eql(3)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/store_orders_actions2' do
|
||||
before { get '/swagger_doc/store_orders_actions2' }
|
||||
it 'does appear as standalone namespace within another standalone namespace' do
|
||||
apis = json_body['apis']
|
||||
# shall include 2 routes, get on dummy2 and get on dummy22
|
||||
expect(apis.length).to eql(2)
|
||||
expect(apis[0]['operations'][0]['method']).to eql('GET')
|
||||
expect(apis[1]['operations'][0]['method']).to eql('GET')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/specific-store-orders' do
|
||||
before { get '/swagger_doc/specific-store-orders' }
|
||||
it 'does show the one route' do
|
||||
apis = json_body['apis']
|
||||
# shall include 1 routes, delete action
|
||||
expect(apis.length).to eql(1)
|
||||
expect(apis[0]['operations'][0]['method']).to eql('DELETE')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrieved namespace swagger-documentation on /swagger_doc/store_orders' do
|
||||
before { get '/swagger_doc/store_orders_actions2' }
|
||||
it 'does work with standalone in standalone namespaces' do
|
||||
apis = json_body['apis']
|
||||
# shall include 2 routes, dummy2 and dummy22
|
||||
expect(apis.length).to eql(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue