Skip to content

Instantly share code, notes, and snippets.

@falood
Created April 7, 2017 04:18
Show Gist options
  • Save falood/984e0884641a7690e8f2810867792fe5 to your computer and use it in GitHub Desktop.
Save falood/984e0884641a7690e8f2810867792fe5 to your computer and use it in GitHub Desktop.
test.rb
require 'grape/middleware/stack'
module Grape
# An Endpoint is the proxy scope in which all routing
# blocks are executed. In other words, any methods
# on the instance level of this class may be called
# from inside a `get`, `post`, etc.
class Endpoint
include Grape::DSL::Settings
include Grape::DSL::InsideRoute
attr_accessor :block, :source, :options
attr_reader :env, :request, :headers, :params
class << self
def new(*args, &block)
self == Endpoint ? Class.new(Endpoint).new(*args, &block) : super
end
def before_each(new_setup = false, &block)
@before_each ||= []
if new_setup == false
return @before_each unless block_given?
@before_each << block
else
@before_each = [new_setup]
end
end
def run_before_each(endpoint)
superclass.run_before_each(endpoint) unless self == Endpoint
before_each.each { |blk| blk.call(endpoint) if blk.respond_to?(:call) }
end
# @api private
#
# Create an UnboundMethod that is appropriate for executing an endpoint
# route.
#
# The unbound method allows explicit calls to +return+ without raising a
# +LocalJumpError+. The method will be removed, but a +Proc+ reference to
# it will be returned. The returned +Proc+ expects a single argument: the
# instance of +Endpoint+ to bind to the method during the call.
#
# @param [String, Symbol] method_name
# @return [Proc]
# @raise [NameError] an instance method with the same name already exists
def generate_api_method(method_name, &block)
if instance_methods.include?(method_name.to_sym) || instance_methods.include?(method_name.to_s)
raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name")
end
define_method(method_name, &block)
method = instance_method(method_name)
remove_method(method_name)
proc do |endpoint_instance|
ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do
method.bind(endpoint_instance).call
end
end
end
end
# Create a new endpoint.
# @param new_settings [InheritableSetting] settings to determine the params,
# validations, and other properties from.
# @param options [Hash] attributes of this endpoint
# @option options path [String or Array] the path to this endpoint, within
# the current scope.
# @option options method [String or Array] which HTTP method(s) can be used
# to reach this endpoint.
# @option options route_options [Hash]
# @note This happens at the time of API definition, so in this context the
# endpoint does not know if it will be mounted under a different endpoint.
# @yield a block defining what your API should do when this endpoint is hit
def initialize(new_settings, options = {}, &block)
require_option(options, :path)
require_option(options, :method)
self.inheritable_setting = new_settings.point_in_time_copy
route_setting(:saved_declared_params, namespace_stackable(:declared_params))
route_setting(:saved_validations, namespace_stackable(:validations))
namespace_stackable(:representations, []) unless namespace_stackable(:representations)
namespace_inheritable(:default_error_status, 500) unless namespace_inheritable(:default_error_status)
@options = options
@options[:path] = Array(options[:path])
@options[:path] << '/' if options[:path].empty?
@options[:method] = Array(options[:method])
@options[:route_options] ||= {}
@lazy_initialize_lock = Mutex.new
@lazy_initialized = nil
@block = nil
return unless block_given?
@source = block
@block = self.class.generate_api_method(method_name, &block)
end
# Update our settings from a given set of stackable parameters. Used when
# the endpoint's API is mounted under another one.
def inherit_settings(namespace_stackable)
inheritable_setting.route[:saved_validations] += namespace_stackable[:validations]
parent_declared_params = namespace_stackable[:declared_params]
if parent_declared_params
inheritable_setting.route[:declared_params] ||= []
inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten)
end
endpoints && endpoints.each { |e| e.inherit_settings(namespace_stackable) }
end
def require_option(options, key)
raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key)
end
def method_name
[options[:method],
Namespace.joined_space(namespace_stackable(:namespace)),
(namespace_stackable(:mount_path) || []).join('/'),
options[:path].join('/')]
.join(' ')
end
def routes
@routes ||= endpoints ? endpoints.collect(&:routes).flatten : to_routes
end
def reset_routes!
endpoints.each(&:reset_routes!) if endpoints
@namespace = nil
@routes = nil
end
def mount_in(router)
if endpoints
endpoints.each { |e| e.mount_in(router) }
else
reset_routes!
routes.each do |route|
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|
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)
all_requirements.reduce({}) do |base_requirements, single_requirements|
base_requirements.merge!(single_requirements)
end
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
def prepare_version
version = namespace_inheritable(:version) || []
return if version.empty?
version.length == 1 ? version.first.to_s : version
end
def merge_route_options(**default)
options[:route_options].clone.reverse_merge(**default)
end
def map_routes
options[:method].map { |method| options[:path].map { |path| yield method, path } }
end
def prepare_path(path)
path_settings = inheritable_setting.to_hash[:namespace_stackable].merge(inheritable_setting.to_hash[:namespace_inheritable])
Path.prepare(path, namespace, path_settings)
end
def namespace
@namespace ||= Namespace.joined_space_path(namespace_stackable(:namespace))
end
def call(env)
lazy_initialize!
dup.call!(env)
end
def call!(env)
env[Grape::Env::API_ENDPOINT] = self
@env = env
@app.call(env)
end
# Return the collection of endpoints within this endpoint.
# This is the case when an Grape::API mounts another Grape::API.
def endpoints
options[:app].endpoints if options[:app] && options[:app].respond_to?(:endpoints)
end
def equals?(e)
(options == e.options) && (inheritable_setting.to_hash == e.inheritable_setting.to_hash)
end
protected
def run
ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do
@header = {}
@request = Grape::Request.new(env)
@params = @request.params
@headers = @request.headers
cookies.read(@request)
self.class.run_before_each(self)
run_filters befores, :before
if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS])
raise Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allowed_methods) unless options?
header 'Allow', allowed_methods
response_object = ''
status 204
else
run_filters before_validations, :before_validation
run_validators validations, request
run_filters after_validations, :after_validation
response_object = @block ? @block.call(self) : nil
end
run_filters afters, :after
cookies.write(header)
# status verifies body presence when DELETE
@body ||= response_object
# The Body commonly is an Array of Strings, the application instance itself, or a File-like object
response_object = file || [body]
[status, header, response_object]
end
end
def build_stack(helpers)
stack = Grape::Middleware::Stack.new
stack.use Rack::Head
stack.use Class.new(Grape::Middleware::Error),
helpers: helpers,
format: namespace_inheritable(:format),
content_types: namespace_stackable_with_hash(:content_types),
default_status: namespace_inheritable(:default_error_status),
rescue_all: namespace_inheritable(:rescue_all),
rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions),
default_error_formatter: namespace_inheritable(:default_error_formatter),
error_formatters: namespace_stackable_with_hash(:error_formatters),
rescue_options: namespace_stackable_with_hash(:rescue_options) || {},
rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers) || {},
base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {},
all_rescue_handler: namespace_inheritable(:all_rescue_handler)
stack.concat namespace_stackable(:middleware)
if namespace_inheritable(:version)
stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]),
versions: namespace_inheritable(:version) ? namespace_inheritable(:version).flatten : nil,
version_options: namespace_inheritable(:version_options),
prefix: namespace_inheritable(:root_prefix),
mount_path: namespace_stackable(:mount_path).first
end
stack.use Grape::Middleware::Formatter,
format: namespace_inheritable(:format),
default_format: namespace_inheritable(:default_format) || :txt,
content_types: namespace_stackable_with_hash(:content_types),
formatters: namespace_stackable_with_hash(:formatters),
parsers: namespace_stackable_with_hash(:parsers)
builder = stack.build
builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
builder.to_app
end
def build_helpers
helpers = namespace_stackable(:helpers) || []
Module.new { helpers.each { |mod_to_include| include mod_to_include } }
end
private :build_stack, :build_helpers
def helpers
lazy_initialize! && @helpers
end
def lazy_initialize!
return true if @lazy_initialized
@lazy_initialize_lock.synchronize do
return true if @lazy_initialized
@helpers = build_helpers.tap { |mod| self.class.send(:include, mod) }
@app = options[:app] || build_stack(@helpers)
@lazy_initialized = true
end
end
def run_validators(validators, request)
validation_errors = []
validators.each do |validator|
begin
validator.validate(request)
rescue Grape::Exceptions::Validation => e
validation_errors << e
break if validator.fail_fast?
rescue Grape::Exceptions::ValidationArrayErrors => e
validation_errors += e.errors
break if validator.fail_fast?
end
end
validation_errors.any? && raise(Grape::Exceptions::ValidationErrors, errors: validation_errors, headers: header)
end
def run_filters(filters, type = :other)
ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
(filters || []).each { |filter| instance_eval(&filter) }
end
post_extension = DSL::InsideRoute.post_filter_methods(type)
extend post_extension if post_extension
end
def befores
namespace_stackable(:befores) || []
end
def before_validations
namespace_stackable(:before_validations) || []
end
def after_validations
namespace_stackable(:after_validations) || []
end
def afters
namespace_stackable(:afters) || []
end
def validations
route_setting(:saved_validations) || []
end
def options?
options[:options_route_enabled] &&
env[Grape::Http::Headers::REQUEST_METHOD] == Grape::Http::Headers::OPTIONS
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment