Skip to content

More powerful API Configuration #1888

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 11, 2019
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.
* [#1888](https://github.com/ruby-grape/grape/pull/1888): Makes the `configuration` hash widly available - [@myxoh](https://github.com/myxoh).
* [#1864](https://github.com/ruby-grape/grape/pull/1864): Adds `finally` on the API - [@myxoh](https://github.com/myxoh).
* [#1869](https://github.com/ruby-grape/grape/pull/1869): Fix issue with empty headers after `error!` method call - [@anaumov](https://github.com/anaumov).

Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ Assuming that the post and comment endpoints are mounted in `/posts` and `/comme

### Mount Configuration

You can configure remountable endpoints to change small details according to where they are mounted.
You can configure remountable endpoints to change how they behave according to where they are mounted.

```ruby
class Voting::API < Grape::API
Expand All @@ -437,6 +437,35 @@ class Comment::API < Grape::API
end
```

You can access `configuration` on the class (to use as dynamic attributes), inside blocks (like namespace)

If you want logic happening given on an `configuration`, you can use the helper `given`.

```ruby
class ConditionalEndpoint::API < Grape::API
given configuration[:some_setting] do
get 'mount_this_endpoint_conditionally' do
configuration[:configurable_response]
end
end
end
```

If you want a block of logic running every time an endpoint is mounted (within which you can access the `configuration` Hash)


```ruby
class ConditionalEndpoint::API < Grape::API
mounted do
YourLogger.info "This API was mounted at: #{Time.now}"

get configuration[:endpoint_name] do
configuration[:configurable_response]
end
end
end
```

## Versioning

There are four strategies in which clients can reach your API's endpoints: `:path`,
Expand Down
2 changes: 2 additions & 0 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ module ServeFile

require 'grape/config'
require 'grape/util/content_types'
require 'grape/util/lazy_value'
require 'grape/util/endpoint_configuration'

require 'grape/validations/validators/base'
require 'grape/validations/attributes_iterator'
Expand Down
43 changes: 36 additions & 7 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Grape
# should subclass this class in order to build an API.
class API
# Class methods that we want to call on the API rather than on the API object
NON_OVERRIDABLE = (Class.new.methods + %i[call call!]).freeze
NON_OVERRIDABLE = (Class.new.methods + %i[call call! configuration]).freeze

class << self
attr_accessor :base_instance, :instances
Expand Down Expand Up @@ -75,7 +75,7 @@ def const_missing(*args)
# too much, you may actually want to provide a new API rather than remount it.
def mount_instance(opts = {})
instance = Class.new(@base_parent)
instance.configuration = opts[:configuration] || {}
instance.configuration = Grape::Util::EndpointConfiguration.new(opts[:configuration] || {})
instance.base = self
replay_setup_on(instance)
instance
Expand All @@ -84,8 +84,8 @@ def mount_instance(opts = {})
# Replays the set up to produce an API as defined in this class, can be called
# on classes that inherit from Grape::API
def replay_setup_on(instance)
@setup.each do |setup_stage|
instance.send(setup_stage[:method], *setup_stage[:args], &setup_stage[:block])
@setup.each do |setup_step|
replay_step_on(instance, setup_step)
end
end

Expand All @@ -110,14 +110,43 @@ def method_missing(method, *args, &block)

# Adds a new stage to the set up require to get a Grape::API up and running
def add_setup(method, *args, &block)
setup_stage = { method: method, args: args, block: block }
@setup << setup_stage
setup_step = { method: method, args: args, block: block }
@setup << setup_step
last_response = nil
@instances.each do |instance|
last_response = instance.send(setup_stage[:method], *setup_stage[:args], &setup_stage[:block])
last_response = replay_step_on(instance, setup_step)
end
last_response
end

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])
end

# Skips steps that contain arguments to be lazily executed (on re-mount time)
def skip_immediate_run?(instance, args)
instance.base_instance? &&
(any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) })
end

def any_lazy?(args)
args.any? { |argument| argument.respond_to?(:lazy?) && argument.lazy? }
end

def evaluate_arguments(configuration, *args)
args.map do |argument|
if argument.respond_to?(:lazy?) && argument.lazy?
configuration.fetch(argument.access_keys).evaluate
elsif argument.is_a?(Hash)
argument.map { |key, value| [key, evaluate_arguments(configuration, value).first] }.to_h
elsif argument.is_a?(Array)
evaluate_arguments(configuration, *argument)
else
argument
end
end
end
end
end
end
27 changes: 25 additions & 2 deletions lib/grape/api/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ class << self
attr_reader :base
attr_accessor :configuration

def given(conditional_option, &block)
evaluate_as_instance_with_configuration(block) if conditional_option && block_given?
end

def mounted(&block)
return if base_instance?
evaluate_as_instance_with_configuration(block)
end

def base=(grape_api)
@base = grape_api
grape_api.instances << self
Expand All @@ -21,6 +30,10 @@ def to_s
(base && base.to_s) || super
end

def base_instance?
self == base.base_instance
end

# A class-level lock to ensure the API is not compiled by multiple
# threads simultaneously within the same process.
LOCK = Mutex.new
Expand Down Expand Up @@ -84,14 +97,24 @@ def prepare_routes
def nest(*blocks, &block)
blocks.reject!(&:nil?)
if blocks.any?
instance_eval(&block) if block_given?
blocks.each { |b| instance_eval(&b) }
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)
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

def inherited(subclass)
subclass.reset!
subclass.logger = logger.clone
Expand Down
8 changes: 6 additions & 2 deletions lib/grape/dsl/desc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ module Desc
#
def desc(description, options = {}, &config_block)
if block_given?
config_class = desc_container
configuration = defined?(configuration) && configuration.respond_to?(:evaluate) ? configuration.evaluate : {}
config_class = desc_container(configuration)

config_class.configure do
description description
Expand Down Expand Up @@ -84,7 +85,7 @@ def unset_description_field(field)
end

# Returns an object which configures itself via an instance-context DSL.
def desc_container
def desc_container(endpoint_configuration)
Module.new do
include Grape::Util::StrictHashConfiguration.module(
:summary,
Expand All @@ -105,6 +106,9 @@ def desc_container
:security,
:tags
)
config_context.define_singleton_method(:configuration) do
endpoint_configuration
end

def config_context.success(*args)
entity(*args)
Expand Down
4 changes: 4 additions & 0 deletions lib/grape/dsl/inside_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ def version
env[Grape::Env::API_VERSION]
end

def configuration
options[:for].configuration.evaluate
end

# End the request and display an error to the
# end user with the specified message.
#
Expand Down
6 changes: 6 additions & 0 deletions lib/grape/util/endpoint_configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Grape
module Util
class EndpointConfiguration < LazyValueHash
end
end
end
90 changes: 90 additions & 0 deletions lib/grape/util/lazy_value.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
module Grape
module Util
class LazyValue
attr_reader :access_keys
def initialize(value, access_keys = [])
@value = value
@access_keys = access_keys
end

def evaluate
@value
end

def lazy?
true
end

def reached_by(parent_access_keys, access_key)
@access_keys = parent_access_keys + [access_key]
self
end

def to_s
evaluate.to_s
end
end

class LazyValueEnumerable < LazyValue
def [](key)
if @value_hash[key].nil?
LazyValue.new(nil).reached_by(access_keys, key)
else
@value_hash[key].reached_by(access_keys, key)
end
end

def fetch(access_keys)
fetched_keys = access_keys.dup
value = self[fetched_keys.shift]
fetched_keys.any? ? value.fetch(fetched_keys) : value
end

def []=(key, value)
@value_hash[key] = if value.is_a?(Hash)
LazyValueHash.new(value)
elsif value.is_a?(Array)
LazyValueArray.new(value)
else
LazyValue.new(value)
end
end
end

class LazyValueArray < LazyValueEnumerable
def initialize(array)
super
@value_hash = []
array.each_with_index do |value, index|
self[index] = value
end
end

def evaluate
evaluated = []
@value_hash.each_with_index do |value, index|
evaluated[index] = value.evaluate
end
evaluated
end
end

class LazyValueHash < LazyValueEnumerable
def initialize(hash)
super
@value_hash = {}.with_indifferent_access
hash.each do |key, value|
self[key] = value
end
end

def evaluate
evaluated = {}.with_indifferent_access
@value_hash.each do |key, value|
evaluated[key] = value.evaluate
end
evaluated
end
end
end
end
4 changes: 4 additions & 0 deletions lib/grape/validations/params_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ def initialize(opts, &block)
configure_declared_params
end

def configuration
@api.configuration.evaluate
end

# @return [Boolean] whether or not this entire scope needs to be
# validated
def should_validate?(parameters)
Expand Down
Loading