Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#### Features

* Your contribution here.
* [#1931](https://github.com/ruby-grape/grape/pull/1931): Introduces LazyBlock to generate expressions that will executed at mount time - [@myxoh](https://github.com/myxoh).
* [#1918](https://github.com/ruby-grape/grape/pull/1918): Helper methods to access controller context from middleware - [@NikolayRys](https://github.com/NikolayRys).
* [#1915](https://github.com/ruby-grape/grape/pull/1915): Micro optimizations in allocating hashes and arrays - [@dnesteryuk](https://github.com/dnesteryuk).
* [#1904](https://github.com/ruby-grape/grape/pull/1904): Allows Grape to load files on startup rather than on the first call - [@myxoh](https://github.com/myxoh).
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, with: { entity: mounted { configuration[:entity] || API::Enitities::Status } }
end

class V2 < Grape::API
version 'v2'
mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Enitities::V2::Status } }
end
```

## Versioning

There are four strategies in which clients can reach your API's endpoints: `:path`,
Expand Down
1 change: 1 addition & 0 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
10 changes: 8 additions & 2 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
26 changes: 16 additions & 10 deletions lib/grape/api/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions lib/grape/util/lazy_block.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lib/grape/util/lazy_value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions spec/grape/api_remount_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down