Skip to content
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

Remountable APIs, allows to re-mounting all APIs #1803

Merged
merged 17 commits into from
Oct 27, 2018
Merged
Show file tree
Hide file tree
Changes from 10 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
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Naming/HeredocDelimiterNaming:
# Configuration parameters: AutoCorrect.
Performance/HashEachMethods:
Exclude:
- 'lib/grape/api.rb'
- 'lib/grape/api/instance.rb'
- 'lib/grape/middleware/versioner/header.rb'

# Offense count: 1
Expand Down
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.
* [#1795](https://github.com/ruby-grape/grape/pull/1803): Adds the ability to re-mount all endpoints in any location - [@myxoh](https://github.com/bschmeck).
* [#1795](https://github.com/ruby-grape/grape/pull/1795): Fix vendor/subtype parsing of an invalid Accept header - [@bschmeck](https://github.com/bschmeck).
* [#1791](https://github.com/ruby-grape/grape/pull/1791): Support `summary`, `hidden`, `deprecated`, `is_array`, `nickname`, `produces`, `consumes`, `tags` options in `desc` block - [@darren987469](https://github.com/darren987469).

Expand Down
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
- [Alongside Sinatra (or other frameworks)](#alongside-sinatra-or-other-frameworks)
- [Rails](#rails)
- [Modules](#modules)
- [Remounting](#remounting)
- [Configuration](#configuration)
- [Versioning](#versioning)
- [Path](#path)
- [Header](#header)
Expand Down Expand Up @@ -366,6 +368,59 @@ class Twitter::API < Grape::API
end
```

## Remounting

You can mount the same endpoints in two different locations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, we have periods at the end of sentences ;)


```ruby
class Voting::API < Grape::API
namespace 'votes' do
get do
# Your logic
end

post do
# Your logic
end
end
end

class Post::API < Grape::API
mount Voting::API
end

class Comment::API < Grape::API
mount Voting::API
end
```

Assuming that the post and comment endpoints are mounted in `/posts` and `/comments`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

you should now be able to do `get /posts/votes`, `post /posts/votes`, `get /comments/votes`

### Configuration

You can configure remountable endpoints for small details changing according to where
they are mounted

```ruby
class Voting::API < Grape::API
namespace 'votes' do
desc "Vote for your #{configuration[:votable]}"
get do
# Your logic
end
end
end

class Post::API < Grape::API
mount Voting::API, with: { votable: 'posts' }
end

class Comment::API < Grape::API
mount Voting::API, with: { votable: 'comments' }
end
```

## Versioning

There are four strategies in which clients can reach your API's endpoints: `:path`,
Expand Down
256 changes: 54 additions & 202 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
@@ -1,233 +1,85 @@
require 'grape/router'
require 'grape/api/instance'

module Grape
# The API class is the primary entry point for creating Grape APIs. Users
# should subclass this class in order to build an API.
class API
include Grape::DSL::API
# Class methods that we want to call on the API rather than on the API object
NON_OVERRIDABLE = %I[define_singleton_method instance_variable_set inspect class is_a? ! kind_of? respond_to?].freeze

class << self
attr_reader :instance

# A class-level lock to ensure the API is not compiled by multiple
# threads simultaneously within the same process.
LOCK = Mutex.new

# Clears all defined routes, endpoints, etc., on this API.
def reset!
reset_endpoints!
reset_routes!
reset_validations!
end

# Parses the API's definition and compiles it into an instance of
# Grape::API.
def compile
@instance ||= new
attr_accessor :base_instance
# When inherited, will create a list of all instances (times the API was mounted)
# It will listen to the setup required to mount that endpoint, and replicate it on any new instance
def inherited(remountable_class, base_instance_parent = Grape::API::Instance)
remountable_class.initial_setup(base_instance_parent)
remountable_class.override_all_methods
remountable_class.make_inheritable
end

# Wipe the compiled API so we can recompile after changes were made.
def change!
@instance = nil
# Initialize the instance variables on the remountable class, and the base_instance
# an instance that will be used to create the set up but will not be mounted
def initial_setup(base_instance_parent)
@instances = []
@setup = []
@base_parent = base_instance_parent
@base_instance = mount_instance
end

# This is the interface point between Rack and Grape; it accepts a request
# from Rack and ultimately returns an array of three values: the status,
# the headers, and the body. See [the rack specification]
# (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
def call(env)
LOCK.synchronize { compile } unless instance
call!(env)
end

# A non-synchronized version of ::call.
def call!(env)
instance.call(env)
end

# (see #cascade?)
def cascade(value = nil)
if value.nil?
inheritable_setting.namespace_inheritable.keys.include?(:cascade) ? !namespace_inheritable(:cascade).nil? : true
else
namespace_inheritable(:cascade, value)
# Redefines all methods so that are forwarded to add_setup and recorded
def override_all_methods
(base_instance.methods - NON_OVERRIDABLE).each do |method_override|
define_singleton_method(method_override) do |*args, &block|
add_setup(method_override, *args, &block)
end
end
end

# see Grape::Router#recognize_path
def recognize_path(path)
LOCK.synchronize { compile } unless instance
instance.router.recognize_path(path)
end

protected

def prepare_routes
endpoints.map(&:routes).flatten
end

# Execute first the provided block, then each of the
# block passed in. Allows for simple 'before' setups
# of settings stack pushes.
def nest(*blocks, &block)
blocks.reject!(&:nil?)
if blocks.any?
instance_eval(&block) if block_given?
blocks.each { |b| instance_eval(&b) }
reset_validations!
else
instance_eval(&block)
# When classes inheriting from this API child, we also want the instances to inherit from our instance
def make_inheritable
define_singleton_method(:inherited) do |sub_remountable|
Grape::API.inherited(sub_remountable, base_instance)
end
end

def inherited(subclass)
subclass.reset!
subclass.logger = logger.clone
# The remountable class can have a configuration hash to provide some dynamic class-level variables.
# For instance, a descripcion could be done using: `desc configuration[:description]` if it may vary
# depending on where the endpoint is mounted. Use with care, if you find yourself using configuration
# 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.instance_variable_set(:@configuration, opts[:configuration] || {})
instance.define_singleton_method(:configuration) { @configuration }
replay_setup_on(instance)
@instances << instance
instance
end

def inherit_settings(other_settings)
top_level_setting.inherit_from other_settings.point_in_time_copy

# Propagate any inherited params down to our endpoints, and reset any
# compiled routes.
endpoints.each do |e|
e.inherit_settings(top_level_setting.namespace_stackable)
e.reset_routes!
# 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])
end

reset_routes!
end
end

attr_reader :router

# Builds the routes from the defined endpoints, effectively compiling
# this API into a usable form.
def initialize
@router = Router.new
add_head_not_allowed_methods_and_options_methods
self.class.endpoints.each do |endpoint|
endpoint.mount_in(@router)
def respond_to?(method, include_private = false)
super(method, include_private) || base_instance.respond_to?(method, include_private)
end

@router.compile!
@router.freeze
end

# Handle a request. See Rack documentation for what `env` is.
def call(env)
result = @router.call(env)
result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade?
result
end

# Some requests may return a HTTP 404 error if grape cannot find a matching
# route. In this case, Grape::Router adds a X-Cascade header to the response
# and sets it to 'pass', indicating to grape's parents they should keep
# looking for a matching route on other resources.
#
# In some applications (e.g. mounting grape on rails), one might need to trap
# errors from reaching upstream. This is effectivelly done by unsetting
# X-Cascade. Default :cascade is true.
def cascade?
return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.keys.include?(:cascade)
return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options) && self.class.namespace_inheritable(:version_options).key?(:cascade)
true
end

reset!

private
private

# For every resource add a 'OPTIONS' route that returns an HTTP 204 response
# with a list of HTTP methods that can be called. Also add a route that
# will return an HTTP 405 response for any HTTP method that the resource
# cannot handle.
def add_head_not_allowed_methods_and_options_methods
routes_map = {}

self.class.endpoints.each do |endpoint|
routes = endpoint.routes
routes.each do |route|
# using the :any shorthand produces [nil] for route methods, substitute all manually
route_key = route.pattern.to_regexp
routes_map[route_key] ||= {}
route_settings = routes_map[route_key]
route_settings[:pattern] = route.pattern
route_settings[:requirements] = route.requirements
route_settings[:path] = route.origin
route_settings[:methods] ||= []
route_settings[:methods] << route.request_method
route_settings[:endpoint] = route.app

# using the :any shorthand produces [nil] for route methods, substitute all manually
route_settings[:methods] = %w[GET PUT POST DELETE PATCH HEAD OPTIONS] if route_settings[:methods].include?('*')
end
end

# The paths we collected are prepared (cf. Path#prepare), so they
# contain already versioning information when using path versioning.
# Disable versioning so adding a route won't prepend versioning
# informations again.
without_root_prefix do
without_versioning do
routes_map.each do |_, config|
methods = config[:methods]
allowed_methods = methods.dup

unless self.class.namespace_inheritable(:do_not_route_head)
allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET)
end

allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods).join(', ')

unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS)
config[:endpoint].options[:options_route_enabled] = true
end

attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header)
generate_not_allowed_method(config[:pattern], attributes)
end
# 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
last_response = nil
@instances.each do |instance|
last_response = instance.send(setup_stage[:method], *setup_stage[:args], &setup_stage[:block])
end
last_response
end
end

# Generate a route that returns an HTTP 405 response for a user defined
# path on methods not specified
def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
not_allowed_methods = %w[GET PUT POST DELETE PATCH HEAD] - allowed_methods
not_allowed_methods << Grape::Http::Headers::OPTIONS if self.class.namespace_inheritable(:do_not_route_options)

return if not_allowed_methods.empty?

@router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
end

# Allows definition of endpoints that ignore the versioning configuration
# used by the rest of your API.
def without_versioning(&_block)
old_version = self.class.namespace_inheritable(:version)
old_version_options = self.class.namespace_inheritable(:version_options)

self.class.namespace_inheritable_to_nil(:version)
self.class.namespace_inheritable_to_nil(:version_options)

yield

self.class.namespace_inheritable(:version, old_version)
self.class.namespace_inheritable(:version_options, old_version_options)
end

# Allows definition of endpoints that ignore the root prefix used by the
# rest of your API.
def without_root_prefix(&_block)
old_prefix = self.class.namespace_inheritable(:root_prefix)

self.class.namespace_inheritable_to_nil(:root_prefix)

yield

self.class.namespace_inheritable(:root_prefix, old_prefix)
end
end
end
Loading