lib/grape/endpoint.rb
# frozen_string_literal: true
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(...)
self == Endpoint ? Class.new(Endpoint).new(...) : super
end
def before_each(new_setup = false, &block)
@before_each ||= []
if new_setup == false
return @before_each unless block
@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)
raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name") if method_defined?(method_name)
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_call(endpoint_instance)
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
# now +namespace_stackable(:declared_params)+ contains all params defined for
# this endpoint and its parents, but later it will be cleaned up,
# see +reset_validations!+ in lib/grape/dsl/validations.rb
route_setting(:declared_params, namespace_stackable(:declared_params).flatten)
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
@status = nil
@stream = nil
@body = nil
@proc = nil
return unless block
@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]
inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params
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!)
@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]
methods << Grape::Http::Headers::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET
methods.each do |method|
route = Grape::Router::Route.new(method, route.origin, **route.attributes.to_h) unless route.request_method == method
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
{}.merge!(*namespace_stackable(:namespace).map(&:requirements)).tap do |requirements|
endpoint_requirements = options.dig(:route_options, :requirements)
requirements.merge!(endpoint_requirements) if endpoint_requirements
end
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(: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.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].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, build_params_with: namespace_inheritable(:build_params_with))
@params = @request.params
@headers = @request.headers
begin
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.new(header.merge('Allow' => allowed_methods)) unless options?
header Grape::Http::Headers::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 = execute
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 Stream-like object
response_object = stream || [body]
[status, header, response_object]
ensure
run_filters finallies, :finally
end
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),
grape_exceptions_rescue_handler: namespace_inheritable(:grape_exceptions_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)&.flatten,
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 execute
@block&.call(self)
end
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 = []
ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do
validators.each do |validator|
validator.validate(request)
rescue Grape::Exceptions::Validation => e
validation_errors << e
break if validator.fail_fast?
rescue Grape::Exceptions::ValidationArrayErrors => e
validation_errors.concat e.errors
break if validator.fail_fast?
end
end
validation_errors.any? && raise(Grape::Exceptions::ValidationErrors.new(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 finallies
namespace_stackable(:finallies)
end
def validations
return enum_for(:validations) unless block_given?
route_setting(:saved_validations)&.each do |saved_validation|
yield Grape::Validations::ValidatorFactory.create_validator(**saved_validation)
end
end
def options?
options[:options_route_enabled] &&
env[Grape::Http::Headers::REQUEST_METHOD] == Grape::Http::Headers::OPTIONS
end
def method_missing(name, *_args)
raise NoMethodError.new("undefined method `#{name}' for #{self.class} in `#{route.origin}' endpoint")
end
def respond_to_missing?(method_name, include_private = false)
super
end
end
end