diff --git a/README.md b/README.md index 0f28b89a4..8b58a6159 100644 --- a/README.md +++ b/README.md @@ -485,6 +485,42 @@ class ConditionalEndpoint::API < Grape::API end ``` +More complex results can be achieved by using mounted as an expression within which the `configuration` is already evaluated as a Hash. + +```ruby +class ExpressionEndpointAPI < Grape::API + get(mounted { configuration[:route_name] || 'default_name' }) do + # some logic + end +end +``` + +```ruby +class BasicAPI < Grape::API + desc 'Statuses index' do + params: mounted { configuration[:entity] || API::Entities::Status }.documentation + end + params do + requires :all, using: mounted { configuration[:entity] || API::Entities::Status }.documentation + end + get '/statuses' do + statuses = Status.all + type = current_user.admin? ? :full : :default + present statuses, with: mounted { configuration[:entity] || API::Entities::Status }, type: type + end +end + +class V1 < Grape::API + version 'v1' + mount BasicAPI +end + +class V2 < Grape::API + version 'v2' + mount BasicAPI, with: { entity: API::Enitities::V2::Status } +end +``` + ## Versioning There are four strategies in which clients can reach your API's endpoints: `:path`, diff --git a/lib/grape.rb b/lib/grape.rb index 79818a1ab..e211e046e 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -221,6 +221,7 @@ module ServeFile require 'grape/config' require 'grape/util/content_types' require 'grape/util/lazy_value' +require 'grape/util/lazy_block' require 'grape/util/endpoint_configuration' require 'grape/validations/validators/base' diff --git a/lib/grape/api.rb b/lib/grape/api.rb index fa707d2e9..ac9a7513a 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -149,7 +149,13 @@ def add_setup(method, *args, &block) def replay_step_on(instance, setup_step) return if skip_immediate_run?(instance, setup_step[:args]) - instance.send(setup_step[:method], *evaluate_arguments(instance.configuration, *setup_step[:args]), &setup_step[:block]) + args = evaluate_arguments(instance.configuration, *setup_step[:args]) + response = instance.send(setup_step[:method], *args, &setup_step[:block]) + if skip_immediate_run?(instance, [response]) + response + else + evaluate_arguments(instance.configuration, response).first + end end # Skips steps that contain arguments to be lazily executed (on re-mount time) @@ -165,7 +171,7 @@ def any_lazy?(args) def evaluate_arguments(configuration, *args) args.map do |argument| if argument.respond_to?(:lazy?) && argument.lazy? - configuration.fetch(argument.access_keys).evaluate + argument.evaluate_from(configuration) elsif argument.is_a?(Hash) argument.map { |key, value| [key, evaluate_arguments(configuration, value).first] }.to_h elsif argument.is_a?(Array) diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index 0d3d43d21..5d8ac7cd4 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -13,12 +13,11 @@ class << self attr_accessor :configuration def given(conditional_option, &block) - evaluate_as_instance_with_configuration(block) if conditional_option && block_given? + evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block_given? end def mounted(&block) - return if base_instance? - evaluate_as_instance_with_configuration(block) + evaluate_as_instance_with_configuration(block, lazy: true) end def base=(grape_api) @@ -110,14 +109,21 @@ def nest(*blocks, &block) end end - def evaluate_as_instance_with_configuration(block) - value_for_configuration = configuration - if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy? - self.configuration = value_for_configuration.evaluate + def evaluate_as_instance_with_configuration(block, lazy: false) + lazy_block = Grape::Util::LazyBlock.new do |configuration| + value_for_configuration = configuration + if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy? + self.configuration = value_for_configuration.evaluate + end + response = instance_eval(&block) + self.configuration = value_for_configuration + response + end + if base_instance? && lazy + lazy_block + else + lazy_block.evaluate_from(configuration) end - response = instance_eval(&block) - self.configuration = value_for_configuration - response end def inherited(subclass) diff --git a/lib/grape/util/lazy_block.rb b/lib/grape/util/lazy_block.rb new file mode 100644 index 000000000..7fe842e19 --- /dev/null +++ b/lib/grape/util/lazy_block.rb @@ -0,0 +1,25 @@ +module Grape + module Util + class LazyBlock + def initialize(&new_block) + @block = new_block + end + + def evaluate_from(configuration) + @block.call(configuration) + end + + def evaluate + @block.call({}) + end + + def lazy? + true + end + + def to_s + evaluate.to_s + end + end + end +end diff --git a/lib/grape/util/lazy_value.rb b/lib/grape/util/lazy_value.rb index 91add6508..0d01c66f0 100644 --- a/lib/grape/util/lazy_value.rb +++ b/lib/grape/util/lazy_value.rb @@ -7,6 +7,11 @@ def initialize(value, access_keys = []) @access_keys = access_keys end + def evaluate_from(configuration) + matching_lazy_value = configuration.fetch(@access_keys) + matching_lazy_value.evaluate + end + def evaluate @value end diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index ea4ae3b3e..f0f60a2a5 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -97,6 +97,54 @@ def app end end + context 'when using an expression derived from a configuration' do + subject(:a_remounted_api) do + Class.new(Grape::API) do + get(mounted { "api_name_#{configuration[:api_name]}" }) do + 'success' + end + end + end + + before do + root_api.mount a_remounted_api, with: { + api_name: 'a_name' + } + end + + it 'mounts the endpoint with the name' do + get 'api_name_a_name' + expect(last_response.body).to eq 'success' + end + + it 'does not mount the endpoint with a null name' do + get 'api_name_' + expect(last_response.body).not_to eq 'success' + end + + context 'when the expression lives in a namespace' do + subject(:a_remounted_api) do + Class.new(Grape::API) do + namespace :base do + get(mounted { "api_name_#{configuration[:api_name]}" }) do + 'success' + end + end + end + end + + it 'mounts the endpoint with the name' do + get 'base/api_name_a_name' + expect(last_response.body).to eq 'success' + end + + it 'does not mount the endpoint with a null name' do + get 'base/api_name_' + expect(last_response.body).not_to eq 'success' + end + end + end + context 'when executing a standard block within a `mounted` block with all dynamic params' do subject(:a_remounted_api) do Class.new(Grape::API) do @@ -306,6 +354,74 @@ def app end end + context 'a very complex configuration example' do + before do + top_level_api = Class.new(Grape::API) do + remounted_api = Class.new(Grape::API) do + get configuration[:endpoint_name] do + configuration[:response] + end + end + + expression_namespace = mounted { configuration[:namespace].to_s * 2 } + given(mounted { configuration[:should_mount_expressed] != false }) do + namespace expression_namespace do + mount remounted_api, with: { endpoint_name: configuration[:endpoint_name], response: configuration[:endpoint_response] } + end + end + end + root_api.mount top_level_api, with: configuration_options + end + + context 'when the namespace should be mounted' do + let(:configuration_options) do + { + should_mount_expressed: true, + namespace: 'bang', + endpoint_name: 'james', + endpoint_response: 'bond' + } + end + + it 'gets a response' do + get 'bangbang/james' + expect(last_response.body).to eq 'bond' + end + end + + context 'when should be mounted is nil' do + let(:configuration_options) do + { + should_mount_expressed: nil, + namespace: 'bang', + endpoint_name: 'james', + endpoint_response: 'bond' + } + end + + it 'gets a response' do + get 'bangbang/james' + expect(last_response.body).to eq 'bond' + end + end + + context 'when it should not be mounted' do + let(:configuration_options) do + { + should_mount_expressed: false, + namespace: 'bang', + endpoint_name: 'james', + endpoint_response: 'bond' + } + end + + it 'gets a response' do + get 'bangbang/james' + expect(last_response.body).not_to eq 'bond' + end + end + end + context 'when the configuration is read in a helper' do subject(:a_remounted_api) do Class.new(Grape::API) do