Skip to content

Commit

Permalink
Replace rack-mount with new router
Browse files Browse the repository at this point in the history
  • Loading branch information
namusyaka committed Mar 14, 2016
1 parent 41808e3 commit 92f4787
Show file tree
Hide file tree
Showing 29 changed files with 556 additions and 188 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
0.15.1 (Next)
=============

#### Features

* [#1276](https://github.com/ruby-grape/grape/pull/1276): Replace rack-mount with new router - [@namusyaka](https://github.com/namusyaka).
* Your contribution here.

#### Fixes

0.15.0 (3/8/2016)
=================

Expand Down Expand Up @@ -32,7 +37,6 @@
* [#1197](https://github.com/ruby-grape/grape/pull/1290): Fix using JSON and Array[JSON] as groups when parameter is optional - [@lukeivers](https://github.com/lukeivers).

0.14.0 (12/07/2015)
===================

#### Features

Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2568,11 +2568,15 @@ Examine the routes at runtime.
```ruby
TwitterAPI::versions # yields [ 'v1', 'v2' ]
TwitterAPI::routes # yields an array of Grape::Route objects
TwitterAPI::routes[0].route_version # => 'v1'
TwitterAPI::routes[0].route_description # => 'Includes custom settings.'
TwitterAPI::routes[0].route_settings[:custom] # => { key: 'value' }
TwitterAPI::routes[0].version # => 'v1'
TwitterAPI::routes[0].description # => 'Includes custom settings.'
TwitterAPI::routes[0].settings[:custom] # => { key: 'value' }
```

Note that `Route#route_xyz` methods have been deprecated since 0.15.0.

Please use `Route#xyz` instead.

## Current Route and Endpoint

It's possible to retrieve the information about the current route from within an API call with `route`.
Expand Down
2 changes: 1 addition & 1 deletion grape.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Gem::Specification.new do |s|
s.license = 'MIT'

s.add_runtime_dependency 'rack', '>= 1.3.0'
s.add_runtime_dependency 'rack-mount'
s.add_runtime_dependency 'mustermann19', '~> 0.4.2'
s.add_runtime_dependency 'rack-accept'
s.add_runtime_dependency 'activesupport'
s.add_runtime_dependency 'multi_json', '>= 1.3.2'
Expand Down
3 changes: 1 addition & 2 deletions lib/grape.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require 'logger'
require 'rack'
require 'rack/mount'
require 'rack/builder'
require 'rack/accept'
require 'rack/auth/basic'
Expand Down Expand Up @@ -33,8 +32,8 @@ module Grape
eager_autoload do
autoload :API
autoload :Endpoint
autoload :Router

autoload :Route
autoload :Namespace

autoload :Path
Expand Down
52 changes: 29 additions & 23 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
require 'grape/router'

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 << self
attr_reader :instance
attr_reader :instance, :router

# A class-level lock to ensure the API is not compiled by multiple
# threads simultaneously within the same process.
Expand Down Expand Up @@ -87,24 +89,25 @@ def inherit_settings(other_settings)
# Builds the routes from the defined endpoints, effectively compiling
# this API into a usable form.
def initialize
@route_set = Rack::Mount::RouteSet.new
@router = Router.new
add_head_not_allowed_methods_and_options_methods
self.class.endpoints.each do |endpoint|
endpoint.mount_in(@route_set)
endpoint.mount_in(@router)
end

@route_set.freeze
@router.compile!
@router.freeze
end

# Handle a request. See Rack documentation for what `env` is.
def call(env)
result = @route_set.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, Rack::Mount adds a X-Cascade header to the response
# 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.
#
Expand All @@ -126,20 +129,23 @@ def cascade?
# will return an HTTP 405 response for any HTTP method that the resource
# cannot handle.
def add_head_not_allowed_methods_and_options_methods
methods_per_path = {}
routes_map = {}

self.class.endpoints.each do |endpoint|
routes = endpoint.routes
routes.each do |route|
route_path = route.route_path
.gsub(/\(.*\)/, '') # ignore any optional portions
.gsub(%r{\:[^\/.?]+}, ':x') # substitute variable names to avoid conflicts

methods_per_path[route_path] ||= []
methods_per_path[route_path] << route.route_method
# 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[: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
methods_per_path[route_path] = %w(GET PUT POST DELETE PATCH HEAD OPTIONS) if methods_per_path[route_path].compact.empty?
route_settings[:methods] = %w(GET PUT POST DELETE PATCH HEAD OPTIONS) if route_settings[:methods].include?('ANY')
end
end

Expand All @@ -149,7 +155,9 @@ def add_head_not_allowed_methods_and_options_methods
# informations again.
without_root_prefix do
without_versioning do
methods_per_path.each do |path, methods|
routes_map.each do |regexp, config|
methods = config[:methods]
path = config[:path]
allowed_methods = methods.dup

unless self.class.namespace_inheritable(:do_not_route_head)
Expand All @@ -159,18 +167,18 @@ def add_head_not_allowed_methods_and_options_methods
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)
generate_options_method(path, allow_header) unless allowed_methods.include?(Grape::Http::Headers::OPTIONS)
generate_options_method(path, allow_header, config) unless allowed_methods.include?(Grape::Http::Headers::OPTIONS)
end

generate_not_allowed_method(path, allowed_methods, allow_header)
generate_not_allowed_method(regexp, allowed_methods, allow_header, config[:endpoint])
end
end
end
end

# Generate an 'OPTIONS' route for a pre-exisiting user defined route
def generate_options_method(path, allow_header)
self.class.options(path, {}) do
def generate_options_method(path, allow_header, options = {})
self.class.options(path, options) do
header 'Allow', allow_header
status 204
''
Expand All @@ -179,15 +187,13 @@ def generate_options_method(path, allow_header)

# Generate a route that returns an HTTP 405 response for a user defined
# path on methods not specified
def generate_not_allowed_method(path, allowed_methods, allow_header)
def generate_not_allowed_method(path, allowed_methods, allow_header, endpoint = nil)
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?

self.class.route(not_allowed_methods, path) do
fail Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allow_header)
end
@router.associate_routes(path, not_allowed_methods, allow_header, endpoint)
end

# Allows definition of endpoints that ignore the versioning configuration
Expand Down
4 changes: 2 additions & 2 deletions lib/grape/dsl/inside_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,10 @@ def present(*args)
#
# desc "Returns the route description."
# get '/' do
# route.route_description
# route.description
# end
def route
env[Grape::Env::RACK_ROUTING_ARGS][:route_info]
env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
end

# Attempt to locate the Entity class for a given object, if not given
Expand Down
3 changes: 2 additions & 1 deletion lib/grape/dsl/routing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def mount(mounts)
in_setting = inheritable_setting

if app.respond_to?(:inheritable_setting, true)
mount_path = Rack::Mount::Utils.normalize_path(path)
mount_path = Grape::Router.normalize_path(path)
app.top_level_setting.namespace_stackable[:mount_path] = mount_path

app.inherit_settings(inheritable_setting)
Expand All @@ -98,6 +98,7 @@ def mount(mounts)
method: :any,
path: path,
app: app,
forward_match: !app.respond_to?(:inheritable_setting),
for: self
)
end
Expand Down
103 changes: 42 additions & 61 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def method_name
end

def routes
@routes ||= endpoints ? endpoints.collect(&:routes).flatten : prepare_routes
@routes ||= endpoints ? endpoints.collect(&:routes).flatten : to_routes
end

def reset_routes!
Expand All @@ -119,29 +119,36 @@ def reset_routes!
@routes = nil
end

def mount_in(route_set)
def mount_in(router)
if endpoints
endpoints.each do |e|
e.mount_in(route_set)
end
endpoints.each { |e| e.mount_in(router) }
else
reset_routes!

routes.each do |route|
methods = [route.route_method]
if !namespace_inheritable(:do_not_route_head) && route.route_method == Grape::Http::Headers::GET
methods = [route.request_method]
if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET
methods << Grape::Http::Headers::HEAD
end
methods.each do |method|
route_set.add_route(self, {
path_info: route.route_compiled,
request_method: method
}, route_info: route)
unless route.request_method.to_s.upcase == method
route = Grape::Router::Route.new(method, route.origin, route.attributes.to_h)
end
router.append(route.apply(self))
end
end
end
end

def to_routes
route_options = prepare_default_route_attributes
map_routes do |method, path|
path = prepare_path(path)
params = merge_route_options(route_options.merge(suffix: path.suffix))
route = Router::Route.new(method, path.path, params)
route.apply(self)
end.flatten
end

def prepare_routes_requirements
endpoint_requirements = options[:route_options][:requirements] || {}
all_requirements = (namespace_stackable(:namespace).map(&:requirements) << endpoint_requirements)
Expand All @@ -150,41 +157,30 @@ def prepare_routes_requirements
end
end

def prepare_routes_path_params(path)
path_params = {}

# named parameters in the api path
regex = Rack::Mount::RegexpWithNamedGroups.new(path)
named_params = regex.named_captures.map { |nc| nc[0] } - %w(version format)
named_params.each { |named_param| path_params[named_param] = '' }
def prepare_default_route_attributes
{
namespace: namespace,
version: prepare_version,
requirements: prepare_routes_requirements,
prefix: namespace_inheritable(:root_prefix),
anchor: options[:route_options].fetch(:anchor, true),
settings: inheritable_setting.route.except(:saved_declared_params, :saved_validations),
forward_match: options[:forward_match]
}
end

# route parameters declared via desc or appended to the api declaration
route_params = options[:route_options][:params]
path_params.merge! route_params if route_params
def prepare_version
version = namespace_inheritable(:version) || []
return if version.length == 0
version.length == 1 ? version.first.to_s : version
end

path_params
def merge_route_options(default = {})
options[:route_options].clone.reverse_merge(default)
end

def prepare_routes
options[:method].map do |method|
options[:path].map do |path|
prepared_path = prepare_path(path)
anchor = options[:route_options].fetch(:anchor, true)
path = compile_path(prepared_path, anchor && !options[:app], prepare_routes_requirements)
request_method = (method.to_s.upcase unless method == :any)

Route.new(options[:route_options].clone.merge(
prefix: namespace_inheritable(:root_prefix),
version: namespace_inheritable(:version) ? namespace_inheritable(:version).join('|') : nil,
namespace: namespace,
method: request_method,
path: prepared_path,
params: prepare_routes_path_params(path),
compiled: path,
settings: inheritable_setting.route.except(:saved_declared_params, :saved_validations)
))
end
end.flatten
def map_routes
options[:method].map { |method| options[:path].map { |path| yield method, path } }
end

def prepare_path(path)
Expand All @@ -196,13 +192,6 @@ def namespace
@namespace ||= Namespace.joined_space_path(namespace_stackable(:namespace))
end

def compile_path(prepared_path, anchor = true, requirements = {})
endpoint_options = {}
endpoint_options[:version] = /#{namespace_inheritable(:version).join('|')}/ if namespace_inheritable(:version)
endpoint_options.merge!(requirements)
Rack::Mount::Strexp.compile(prepared_path, endpoint_options, %w( / . ? ), anchor)
end

def call(env)
lazy_initialize!
dup.call!(env)
Expand Down Expand Up @@ -275,11 +264,7 @@ def build_stack
(namespace_stackable(:middleware) || []).each do |m|
m = m.dup
block = m.pop if m.last.is_a?(Proc)
if block
b.use(*m, &block)
else
b.use(*m)
end
block ? b.use(*m, &block) : b.use(*m)
end

if namespace_inheritable(:version)
Expand All @@ -305,9 +290,7 @@ def build_stack
def build_helpers
helpers = namespace_stackable(:helpers) || []
Module.new do
helpers.each do |mod_to_include|
include mod_to_include
end
helpers.each { |mod_to_include| include mod_to_include }
end
end

Expand Down Expand Up @@ -348,9 +331,7 @@ def run_validators(validators, request)

def run_filters(filters, type = :other)
ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
(filters || []).each do |filter|
instance_eval(&filter)
end
(filters || []).each { |filter| instance_eval(&filter) }
end
post_extension = DSL::InsideRoute.post_filter_methods(type)
extend post_extension if post_extension
Expand Down
Loading

0 comments on commit 92f4787

Please sign in to comment.