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

Optimize hash alloc #2512

Merged
merged 11 commits into from
Nov 12, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [#2500](https://github.com/ruby-grape/grape/pull/2500): Remove deprecated `file` method - [@ericproulx](https://github.com/ericproulx).
* [#2501](https://github.com/ruby-grape/grape/pull/2501): Remove deprecated `except` and `proc` options in values validator - [@ericproulx](https://github.com/ericproulx).
* [#2502](https://github.com/ruby-grape/grape/pull/2502): Remove deprecation `options` in `desc` - [@ericproulx](https://github.com/ericproulx).
* [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
2 changes: 1 addition & 1 deletion lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def const_missing(*args)
# For instance, a description 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)
def mount_instance(opts = {})
instance = Class.new(@base_parent)
instance.configuration = Grape::Util::EndpointConfiguration.new(opts[:configuration] || {})
instance.base = self
Expand Down
80 changes: 22 additions & 58 deletions lib/grape/api/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,88 +194,52 @@ 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
versioned_route_configs = collect_route_config_per_pattern
# The paths we collected are prepared (cf. Path#prepare), so they
# contain already versioning information when using path versioning.
all_routes = self.class.endpoints.map(&:routes).flatten

# Disable versioning so adding a route won't prepend versioning
# informations again.
without_root_prefix do
without_versioning do
versioned_route_configs.each do |config|
next if config[:options][:matching_wildchar]

allowed_methods = config[:methods].dup

allowed_methods |= [Rack::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Rack::GET)

allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Rack::OPTIONS] | allowed_methods)

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

attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header)
generate_not_allowed_method(config[:pattern], **attributes)
end
end
end
without_root_prefix_and_versioning { collect_route_config_per_pattern(all_routes) }
end

def collect_route_config_per_pattern
all_routes = self.class.endpoints.map(&:routes).flatten
def collect_route_config_per_pattern(all_routes)
routes_by_regexp = all_routes.group_by(&:pattern_regexp)

# Build the configuration based on the first endpoint and the collection of methods supported.
routes_by_regexp.values.map do |routes|
last_route = routes.last # Most of the configuration is taken from the last endpoint
matching_wildchar = routes.any? { |route| route.request_method == '*' }
{
options: { matching_wildchar: matching_wildchar },
pattern: last_route.pattern,
requirements: last_route.requirements,
path: last_route.origin,
endpoint: last_route.app,
methods: matching_wildchar ? Grape::Http::Headers::SUPPORTED_METHODS : routes.map(&:request_method)
}
end
end
routes_by_regexp.each_value do |routes|
last_route = routes.last # Most of the configuration is taken from the last endpoint
next if routes.any? { |route| route.request_method == '*' }

# 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)
supported_methods =
if self.class.namespace_inheritable(:do_not_route_options)
Grape::Http::Headers::SUPPORTED_METHODS
else
Grape::Http::Headers::SUPPORTED_METHODS_WITHOUT_OPTIONS
end
not_allowed_methods = supported_methods - allowed_methods
@router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
allowed_methods = routes.map(&:request_method)
allowed_methods |= [Rack::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Rack::GET)

allow_header = self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Rack::OPTIONS] | allowed_methods
last_route.app.options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Rack::OPTIONS)

@router.associate_routes(last_route.pattern, {
endpoint: last_route.app,
allow_header: allow_header
})
end
end

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

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

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)
self.class.namespace_inheritable(:root_prefix, old_root_prefix)
end
end
end
Expand Down
15 changes: 7 additions & 8 deletions lib/grape/dsl/inside_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ def self.post_filter_methods(type)
# has completed
module PostBeforeFilter
def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
options = options.reverse_merge(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
declared_params ||= optioned_declared_params(**options)
options.reverse_merge!(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
declared_params ||= optioned_declared_params(options[:include_parent_namespaces])

res = if passed_params.is_a?(Array)
declared_array(passed_params, options, declared_params, params_nested_path)
Expand Down Expand Up @@ -120,8 +120,8 @@ def optioned_param_key(declared_param, options)
options[:stringify] ? declared_param.to_s : declared_param.to_sym
end

def optioned_declared_params(**options)
declared_params = if options[:include_parent_namespaces]
def optioned_declared_params(include_parent_namespaces)
declared_params = if include_parent_namespaces
# Declared params including parent namespaces
route_setting(:declared_params)
else
Expand Down Expand Up @@ -199,10 +199,9 @@ def rack_response(message, status = 200, headers = { Rack::CONTENT_TYPE => conte
# Redirect to a new url.
#
# @param url [String] The url to be redirect.
# @param options [Hash] The options used when redirect.
# :permanent, default false.
# :body, default a short message including the URL.
def redirect(url, permanent: false, body: nil, **_options)
# @param permanent [Boolean] default false.
# @param body default a short message including the URL.
def redirect(url, permanent: false, body: nil)
body_message = body
if permanent
status 301
Expand Down
4 changes: 2 additions & 2 deletions lib/grape/dsl/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def requires(*attrs, &block)
require_required_and_optional_fields(attrs.first, opts)
else
validate_attributes(attrs, opts, &block)
block ? new_scope(orig_attrs, &block) : push_declared_params(attrs, **opts.slice(:as))
block ? new_scope(orig_attrs, &block) : push_declared_params(attrs, opts.slice(:as))
end
end

Expand All @@ -162,7 +162,7 @@ def optional(*attrs, &block)
else
validate_attributes(attrs, opts, &block)

block ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, **opts.slice(:as))
block ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, opts.slice(:as))
end
end

Expand Down
17 changes: 5 additions & 12 deletions lib/grape/dsl/routing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,19 +175,12 @@ def route(methods, paths = ['/'], route_options = {}, &block)
# end
# end
def namespace(space = nil, options = {}, &block)
@namespace_description = nil unless instance_variable_defined?(:@namespace_description) && @namespace_description

if space || block
within_namespace do
previous_namespace_description = @namespace_description
@namespace_description = (@namespace_description || {}).deep_merge(namespace_setting(:description) || {})
nest(block) do
namespace_stackable(:namespace, Namespace.new(space, **options)) if space
end
@namespace_description = previous_namespace_description
return Namespace.joined_space_path(namespace_stackable(:namespace)) unless space || block

within_namespace do
nest(block) do
namespace_stackable(:namespace, Namespace.new(space, options)) if space
end
else
Namespace.joined_space_path(namespace_stackable(:namespace))
end
end

Expand Down
143 changes: 70 additions & 73 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,16 @@ def reset_routes!
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 << Rack::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Rack::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
return endpoints.each { |e| e.mount_in(router) } if endpoints

reset_routes!
routes.each do |route|
router.append(route.apply(self))
next unless !namespace_inheritable(:do_not_route_head) && route.request_method == Rack::GET

route.dup.then do |head_route|
head_route.convert_to_head_request!
router.append(head_route.apply(self))
end
end
end
Expand All @@ -164,8 +163,9 @@ 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_options[:suffix] = path.suffix
params = options[:route_options].merge(route_options)
route = Grape::Router::Route.new(method, path.path, params)
route.apply(self)
end.flatten
end
Expand Down Expand Up @@ -196,10 +196,6 @@ def prepare_version
version.length == 1 ? version.first : 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
Expand Down Expand Up @@ -259,9 +255,10 @@ def run
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?
allow_header_value = allowed_methods.join(', ')
raise Grape::Exceptions::MethodNotAllowed.new(header.merge('Allow' => allow_header_value)) unless options?

header Grape::Http::Headers::ALLOW, allowed_methods
header Grape::Http::Headers::ALLOW, allow_header_value
response_object = ''
status 204
else
Expand All @@ -287,59 +284,6 @@ def run
end
end

def build_stack(helpers)
stack = Grape::Middleware::Stack.new

content_types = namespace_stackable_with_hash(:content_types)
format = namespace_inheritable(:format)

stack.use Rack::Head
stack.use Class.new(Grape::Middleware::Error),
helpers: helpers,
format: format,
content_types: 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).present?
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: format,
default_format: namespace_inheritable(:default_format) || :txt,
content_types: 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)
return if helpers.empty?

Module.new { helpers.each { |mod_to_include| include mod_to_include } }
end

private :build_stack, :build_helpers

def execute
@block&.call(self)
end
Expand Down Expand Up @@ -411,13 +355,66 @@ 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)
yield Grape::Validations::ValidatorFactory.create_validator(saved_validation)
end
end

def options?
options[:options_route_enabled] &&
env[Rack::REQUEST_METHOD] == Rack::OPTIONS
end

private

def build_stack(helpers)
stack = Grape::Middleware::Stack.new

content_types = namespace_stackable_with_hash(:content_types)
format = namespace_inheritable(:format)

stack.use Rack::Head
stack.use Class.new(Grape::Middleware::Error),
helpers: helpers,
format: format,
content_types: 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).present?
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: format,
default_format: namespace_inheritable(:default_format) || :txt,
content_types: 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)
return if helpers.empty?

Module.new { helpers.each { |mod_to_include| include mod_to_include } }
end
end
end
Loading