-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from 10 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
ae879cb
Remountable APIs, allows to re-mount APIs that inherit from this
myxoh 091a331
Adds Readme.md to remountable APIs
myxoh ccec543
Adds Changelog.md
myxoh 641959c
Adds remounting section to the table of contents
myxoh 37cce04
Makes Remountable API a valid drop-in replacement to Grape::API
myxoh cf14fc7
Replaces APIs with RemountableAPI
myxoh 5fcc07b
Updates documentation
myxoh 691eca8
Changes APIInstance to API::Instance
myxoh ffe147b
Fixes README an specs descriptions
myxoh 1c6a9dc
Replaces keyword args with opts
myxoh 1421517
Refactors and comments
myxoh 330a6b9
Adds test for mounting multiple apps as one
myxoh 24295fd
Adds paragraph to upgrading explaining how to rollback behaviour
myxoh d4eee5b
Bumps up version and upgrades description
myxoh f8c3fee
Adds missing hash to UPGRADING.md
myxoh 9416023
Address problems with autoloading by searching for constants in instance
myxoh ce4966d
Better handles constants
myxoh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -366,6 +368,59 @@ class Twitter::API < Grape::API | |
end | ||
``` | ||
|
||
## Remounting | ||
|
||
You can mount the same endpoints in two different locations | ||
|
||
```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`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 ;)