From 6bc69419429d0fdd07e0b4a1373cbcf133944799 Mon Sep 17 00:00:00 2001 From: nklein Date: Thu, 14 Nov 2019 13:14:35 +0000 Subject: [PATCH 1/4] Introduces the concept of a LazyBlock which gets excecuted on mount --- lib/grape.rb | 1 + lib/grape/api.rb | 10 ++++++++-- lib/grape/api/instance.rb | 13 +++++++------ lib/grape/util/lazy_block.rb | 25 +++++++++++++++++++++++++ lib/grape/util/lazy_value.rb | 5 +++++ spec/grape/api_remount_spec.rb | 26 ++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 lib/grape/util/lazy_block.rb 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..ab96bc3ec 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -13,12 +13,13 @@ 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, configuration) if conditional_option && block_given? end def mounted(&block) - return if base_instance? - evaluate_as_instance_with_configuration(block) + Grape::Util::LazyBlock.new do |configuration| + evaluate_as_instance_with_configuration(block, configuration) + end end def base=(grape_api) @@ -102,15 +103,15 @@ def prepare_routes def nest(*blocks, &block) blocks.reject!(&:nil?) if blocks.any? - evaluate_as_instance_with_configuration(block) if block_given? - blocks.each { |b| evaluate_as_instance_with_configuration(b) } + evaluate_as_instance_with_configuration(block, configuration) if block_given? + blocks.each { |b| evaluate_as_instance_with_configuration(b, configuration) } reset_validations! else instance_eval(&block) end end - def evaluate_as_instance_with_configuration(block) + def evaluate_as_instance_with_configuration(block, configuration) value_for_configuration = configuration if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy? self.configuration = value_for_configuration.evaluate 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..1a29b09af 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -97,6 +97,32 @@ 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 + 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 From 0b58a57d2a5655345a754f7bb19a4092263534c3 Mon Sep 17 00:00:00 2001 From: nklein Date: Thu, 14 Nov 2019 13:18:56 +0000 Subject: [PATCH 2/4] Fixes LazyBlock within a namespace --- lib/grape/api/instance.rb | 7 ++++++- spec/grape/api_remount_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index ab96bc3ec..2f72f339f 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -17,9 +17,14 @@ def given(conditional_option, &block) end def mounted(&block) - Grape::Util::LazyBlock.new do |configuration| + lazy_block = Grape::Util::LazyBlock.new do |configuration| evaluate_as_instance_with_configuration(block, configuration) end + if base_instance? + lazy_block + else + lazy_block.evaluate_from(configuration) + end end def base=(grape_api) diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index 1a29b09af..57b305ab0 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -121,6 +121,28 @@ def app 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 From 80133ecbd458673cc587123658c4d8c32c888a47 Mon Sep 17 00:00:00 2001 From: nklein Date: Thu, 14 Nov 2019 14:56:01 +0000 Subject: [PATCH 3/4] Adds more specs so that given can also be used for conditional lazy blocks --- lib/grape/api/instance.rb | 36 ++++++++--------- spec/grape/api_remount_spec.rb | 72 +++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index 2f72f339f..5d8ac7cd4 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -13,18 +13,11 @@ class << self attr_accessor :configuration def given(conditional_option, &block) - evaluate_as_instance_with_configuration(block, configuration) if conditional_option && block_given? + evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block_given? end def mounted(&block) - lazy_block = Grape::Util::LazyBlock.new do |configuration| - evaluate_as_instance_with_configuration(block, configuration) - end - if base_instance? - lazy_block - else - lazy_block.evaluate_from(configuration) - end + evaluate_as_instance_with_configuration(block, lazy: true) end def base=(grape_api) @@ -108,22 +101,29 @@ def prepare_routes def nest(*blocks, &block) blocks.reject!(&:nil?) if blocks.any? - evaluate_as_instance_with_configuration(block, configuration) if block_given? - blocks.each { |b| evaluate_as_instance_with_configuration(b, configuration) } + evaluate_as_instance_with_configuration(block) if block_given? + blocks.each { |b| evaluate_as_instance_with_configuration(b) } reset_validations! else instance_eval(&block) end end - def evaluate_as_instance_with_configuration(block, configuration) - 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/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb index 57b305ab0..f0f60a2a5 100644 --- a/spec/grape/api_remount_spec.rb +++ b/spec/grape/api_remount_spec.rb @@ -100,7 +100,7 @@ def app 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 + get(mounted { "api_name_#{configuration[:api_name]}" }) do 'success' end end @@ -126,7 +126,7 @@ def app subject(:a_remounted_api) do Class.new(Grape::API) do namespace :base do - get mounted { "api_name_#{configuration[:api_name]}" } do + get(mounted { "api_name_#{configuration[:api_name]}" }) do 'success' end end @@ -354,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 From 94782f34d76fbede9a5f953ba1875046100ee349 Mon Sep 17 00:00:00 2001 From: nklein Date: Thu, 14 Nov 2019 15:06:46 +0000 Subject: [PATCH 4/4] Documents expressions --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) 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`,