From a2e30482a42a79d7e896538ccdf76fec69e6a5be Mon Sep 17 00:00:00 2001 From: Devin Moss Date: Tue, 14 Feb 2023 17:16:21 -0600 Subject: [PATCH 1/3] Add default_route_visibility to Defaults --- lib/grape-swagger/doc_methods.rb | 1 + lib/grape-swagger/endpoint.rb | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/grape-swagger/doc_methods.rb b/lib/grape-swagger/doc_methods.rb index efb850e34..1f55c06e8 100644 --- a/lib/grape-swagger/doc_methods.rb +++ b/lib/grape-swagger/doc_methods.rb @@ -39,6 +39,7 @@ module DocMethods endpoint_auth_wrapper: nil, swagger_endpoint_guard: 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 96817317b..118b50115 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -419,11 +419,22 @@ def model_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) + return !public?(route, options) if options[:default_route_visibility] == :hidden - options[:token_owner] ? route_hidden.call(send(options[:token_owner].to_sym)) : route_hidden.call + scan_route_for_value(:hidden, route, options) + end + + def public?(route, options) + scan_route_for_value(:public, route, options) + end + + 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 def hidden_parameter?(value) From 9d4c384ce0b15ee2e8ab2e5ffe7c7cf311410cf0 Mon Sep 17 00:00:00 2001 From: Devin Moss Date: Tue, 14 Feb 2023 17:24:03 -0600 Subject: [PATCH 2/3] Changelog and spec tests --- CHANGELOG.md | 2 +- lib/grape-swagger/doc_methods.rb | 2 +- .../888_default_route_visbility_spec.rb | 177 ++++++++++++++++++ 3 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 spec/issues/888_default_route_visbility_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index c84d381b8..e6bf666b0 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/lib/grape-swagger/doc_methods.rb b/lib/grape-swagger/doc_methods.rb index 1f55c06e8..820476dda 100644 --- a/lib/grape-swagger/doc_methods.rb +++ b/lib/grape-swagger/doc_methods.rb @@ -38,7 +38,7 @@ 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 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 From fdd06fb4131312b09d83f19bc04cff22c07a4ab0 Mon Sep 17 00:00:00 2001 From: Devin Moss Date: Wed, 15 Feb 2023 14:35:48 -0600 Subject: [PATCH 3/3] Rubocop and README update --- README.md | 46 ++++++++++++++++++++++++ lib/grape-swagger/endpoint.rb | 34 ++---------------- lib/grape-swagger/endpoint/visibility.rb | 40 +++++++++++++++++++++ spec/lib/oapi_tasks_spec.rb | 4 +-- 4 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 lib/grape-swagger/endpoint/visibility.rb diff --git a/README.md b/README.md index d4b475904..d95b48d07 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) You can pass a hash with optional configuration settings to ```add_swagger_documentation```. The examples show the default value. @@ -432,10 +433,27 @@ 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. + ## 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) @@ -510,6 +528,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/endpoint.rb b/lib/grape-swagger/endpoint.rb index 118b50115..1eceef978 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 @@ -95,7 +96,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] @@ -176,7 +177,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 == '' @@ -418,35 +419,6 @@ def model_name(name) GrapeSwagger::DocMethods::DataType.parse_entity_name(name) end - def hidden?(route, options) - return !public?(route, options) if options[:default_route_visibility] == :hidden - - scan_route_for_value(:hidden, route, options) - end - - def public?(route, options) - scan_route_for_value(:public, route, options) - end - - 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 - - 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/lib/oapi_tasks_spec.rb b/spec/lib/oapi_tasks_spec.rb index 733854f5e..ab6626563 100644 --- a/spec/lib/oapi_tasks_spec.rb +++ b/spec/lib/oapi_tasks_spec.rb @@ -30,11 +30,11 @@ class Base < Grape::API describe '.new' do it 'accepts class name as a constant' 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 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