diff --git a/CHANGELOG.md b/CHANGELOG.md index 66429c9b3..4996a2add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ #### Features -* Your contribution here. +* [#888](https://github.com/ruby-grape/grape-swagger/pull/888): Add default_route_visibility to easily hide all endpoints - [@dmoss18](https://github.com/dmoss18) #### Fixes diff --git a/README.md b/README.md index 4451eddd7..a507a38cb 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,7 @@ end * [array_use_braces](#array_use_braces) * [api_documentation](#api_documentation) * [specific_api_documentation](#specific_api_documentation) +* [default_route_visibility](#default_route_visibility) * [consumes](#consumes) * [produces](#produces) @@ -434,6 +435,23 @@ add_swagger_documentation \ specific_api_documentation: { desc: 'Reticulated splines API swagger-compatible endpoint documentation.' } ``` +#### default_route_visibility +By default, grape-swagger will include all routes in the genrated documentation. You can configure this via `default_route_visibility`: +```rb +add_swagger_documentation \ + default_route_visibility: :hidden +``` +Grape-swagger will now *exclude* all routes in the generated documentation. You can then explicitly mark specific routes public like this: +```rb +desc 'Get all accounts', public: true +get 'accounts' do + ['account1', 'account2'] +end +``` + +**Please note**: Marking a route `public: false` or `hidden: false` will simply fall back to the overall `default_route_visibility`. You should consider `public` and `hidden` to be flags that only take effect when their value is truthy. + + #### consumes Customize the Swagger API default global `consumes` field value. @@ -452,10 +470,12 @@ add_swagger_documentation \ produces: ['text/plain'] ``` + ## Routes Configuration * [Swagger Header Parameters](#headers) * [Hiding an Endpoint](#hiding) +* [Hiding all Endpoints](#hiding-all-endpoints) * [Overriding Auto-Generated Nicknames](#overriding-auto-generated-nicknames) * [Specify endpoint details](#details) * [Overriding the route summary](#summary) @@ -530,6 +550,34 @@ state: desc 'Conditionally hide this endpoint', hidden: lambda { ENV['EXPERIMENTAL'] != 'true' } ``` +#### Hiding all endpoints +You can hide all endpoints by default via `default_endpoint_visibility: :hidden`. You'll need to explicitly add `public: true` to each endpoint. This is functionally the inverse of the above [Hiding an Endpoint](#hiding) section. + +You can show an endpoint by adding ```public: true``` in the description of the endpoint: +```ruby +desc 'Show this endpoint', public: true +``` + +Or by adding ```public: true``` on the verb method of the endpoint, such as `get`, `post` and `put`: + +```ruby +get '/kittens', public: true do +``` + +Or by using a route setting: + +```ruby +route_setting :swagger, { public: true } +get '/kittens' do +``` + +Endpoints can be conditionally shown by providing a callable object such as a lambda which evaluates to the desired +state: + +```ruby +desc 'Conditionally hide this endpoint', public: lambda { ENV['EXPERIMENTAL'] != 'true' } +``` + #### Overriding Auto-Generated Nicknames diff --git a/lib/grape-swagger/doc_methods.rb b/lib/grape-swagger/doc_methods.rb index efb850e34..820476dda 100644 --- a/lib/grape-swagger/doc_methods.rb +++ b/lib/grape-swagger/doc_methods.rb @@ -38,7 +38,8 @@ module DocMethods specific_api_documentation: { desc: 'Swagger compatible API description for specific API' }, endpoint_auth_wrapper: nil, swagger_endpoint_guard: nil, - token_owner: nil + token_owner: nil, + default_route_visibility: :public }.freeze FORMATTER_METHOD = %i[format default_format default_error_formatter].freeze diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index 27e123dd5..649240164 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -3,6 +3,7 @@ require 'active_support' require 'active_support/core_ext/string/inflections' require 'grape-swagger/endpoint/params_parser' +require 'grape-swagger/endpoint/visibility' module Grape class Endpoint # rubocop:disable Metrics/ClassLength @@ -96,7 +97,7 @@ def add_definitions_from(models) # path object def path_item(routes, options) routes.each do |route| - next if hidden?(route, options) + next if GrapeSwagger::Endpoint::Visibility.hidden_route?(route, options) @item, path = GrapeSwagger::DocMethods::PathString.build(route, options) @entity = route.entity || route.options[:success] @@ -177,7 +178,7 @@ def consumes_object(route, format) def params_object(route, options, path) parameters = build_request_params(route, options).each_with_object([]) do |(param, value), memo| - next if hidden_parameter?(value) + next if GrapeSwagger::Endpoint::Visibility.hidden_parameter?(value) value = { required: false }.merge(value) if value.is_a?(Hash) _, value = default_type([[param, value]]).first if value == '' @@ -435,24 +436,6 @@ def model_name(name) GrapeSwagger::DocMethods::DataType.parse_entity_name(name) end - def hidden?(route, options) - route_hidden = route.settings.try(:[], :swagger).try(:[], :hidden) - route_hidden = route.options[:hidden] if route.options.key?(:hidden) - return route_hidden unless route_hidden.is_a?(Proc) - - options[:token_owner] ? route_hidden.call(send(options[:token_owner].to_sym)) : route_hidden.call - end - - def hidden_parameter?(value) - return false if value[:required] - - if value.dig(:documentation, :hidden).is_a?(Proc) - value.dig(:documentation, :hidden).call - else - value.dig(:documentation, :hidden) - end - end - def success_code_from_entity(route, entity) default_code = GrapeSwagger::DocMethods::StatusCodes.get[route.request_method.downcase.to_sym] if entity.is_a?(Hash) diff --git a/lib/grape-swagger/endpoint/visibility.rb b/lib/grape-swagger/endpoint/visibility.rb new file mode 100644 index 000000000..a3f8eeace --- /dev/null +++ b/lib/grape-swagger/endpoint/visibility.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module GrapeSwagger + module Endpoint + class Visibility + class << self + def hidden_route?(route, options) + return !public_route?(route, options) if options[:default_route_visibility] == :hidden + + scan_route_for_value(:hidden, route, options) + end + + def public_route?(route, options) + scan_route_for_value(:public, route, options) + end + + def hidden_parameter?(value) + return false if value[:required] + + if value.dig(:documentation, :hidden).is_a?(Proc) + value.dig(:documentation, :hidden).call + else + value.dig(:documentation, :hidden) + end + end + + private + + def scan_route_for_value(key, route, options) + key = key.to_sym + route_value = route.settings.try(:[], :swagger).try(:[], key) + route_value = route.options[key] if route.options.key?(key) + return route_value unless route_value.is_a?(Proc) + + options[:token_owner] ? route_value.call(send(options[:token_owner].to_sym)) : route_value.call + end + end + end + end +end diff --git a/spec/issues/888_default_route_visbility_spec.rb b/spec/issues/888_default_route_visbility_spec.rb new file mode 100644 index 000000000..cace69546 --- /dev/null +++ b/spec/issues/888_default_route_visbility_spec.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'default endpoint visibility' do + let(:documentation_options) do + { default_route_visibility: default_visibility } + end + let(:app) do + swagger_options = documentation_options + options = route_options + + Class.new(Grape::API) do + desc 'Get all accounts', options + resource :accounts do + get do + [{ message: 'hello world' }] + end + end + + add_swagger_documentation(swagger_options) + end + end + + shared_examples 'public endpoint' do + it 'exposes endpoint' do + get_route = subject.dig('paths', '/accounts', 'get') + expect(get_route).to be_present + expect(get_route['description']).to eq 'Get all accounts' + end + end + + shared_examples 'hidden endpoint' do + it 'hides endpoint' do + expect(subject.dig('paths', '/accounts')).to be_nil + end + end + + subject do + get '/swagger_doc' + JSON.parse(last_response.body) + end + + context 'with :public default visibility' do + let(:default_visibility) { :public } + + context 'with endpoint marked hidden: true' do + let(:route_options) do + { hidden: true } + end + + it_behaves_like 'hidden endpoint' + end + + context 'with endpoint marked public: true' do + let(:route_options) do + { public: true } + end + + it_behaves_like 'public endpoint' + end + + context 'with blank endpoint options' do + let(:route_options) do + {} + end + + it_behaves_like 'public endpoint' + end + + context 'with endpoint marked hidden: false' do + let(:route_options) do + { hidden: false } + end + + it_behaves_like 'public endpoint' + end + + context 'with endpoint marked public: false' do + let(:route_options) do + { public: false } + end + + it_behaves_like 'public endpoint' + end + end + + context 'with :hidden default visibility' do + let(:default_visibility) { :hidden } + + context 'with endpoint marked public: true' do + let(:route_options) do + { public: true } + end + + it_behaves_like 'public endpoint' + end + + context 'with endpoint marked hidden: true' do + let(:route_options) do + { hidden: true } + end + + it_behaves_like 'hidden endpoint' + end + + context 'with blank endpoint options' do + let(:route_options) do + {} + end + + it_behaves_like 'hidden endpoint' + end + + context 'with endpoint marked public: false' do + let(:route_options) do + { public: false } + end + + it_behaves_like 'hidden endpoint' + end + + context 'with endpoint marked hidden: false' do + let(:route_options) do + { hidden: false } + end + + it_behaves_like 'hidden endpoint' + end + end + + context 'with no visibility specified' do + let(:documentation_options) do + {} + end + + context 'with endpoint marked public: true' do + let(:route_options) do + { public: true } + end + + it_behaves_like 'public endpoint' + end + + context 'with endpoint marked hidden: true' do + let(:route_options) do + { hidden: true } + end + + it_behaves_like 'hidden endpoint' + end + + context 'with blank endpoint options' do + let(:route_options) do + {} + end + + it_behaves_like 'public endpoint' + end + + context 'with endpoint marked public: false' do + let(:route_options) do + { public: false } + end + + it_behaves_like 'public endpoint' + end + + context 'with endpoint marked hidden: false' do + let(:route_options) do + { hidden: false } + end + + it_behaves_like 'public endpoint' + end + end +end diff --git a/spec/lib/oapi_tasks_spec.rb b/spec/lib/oapi_tasks_spec.rb index 38bdca700..ab6626563 100644 --- a/spec/lib/oapi_tasks_spec.rb +++ b/spec/lib/oapi_tasks_spec.rb @@ -34,7 +34,7 @@ class Base < Grape::API end it 'accepts class name as a string' do - expect(described_class.new('::Api::Base').send(:api_class)).to eq(Api::Base) + expect(described_class.new('Api::Base').send(:api_class)).to eq(Api::Base) end end