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 Grape::Path #3

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [#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).
* [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
18 changes: 9 additions & 9 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,13 @@ def mount_in(router)
end

def to_routes
route_options = prepare_default_route_attributes
map_routes do |method, path|
path = prepare_path(path)
route_options[:suffix] = path.suffix
params = options[:route_options].merge(route_options)
route = Grape::Router::Route.new(method, path.path, params)
default_route_options = prepare_default_route_attributes
default_path_settings = prepare_default_path_settings

map_routes do |method, raw_path|
prepared_path = Path.new(raw_path, namespace, default_path_settings)
params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options
route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params)
route.apply(self)
end.flatten
end
Expand Down Expand Up @@ -200,11 +201,10 @@ def map_routes
options[:method].map { |method| options[:path].map { |path| yield method, path } }
end

def prepare_path(path)
def prepare_default_path_settings
namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
path_settings = namespace_stackable_hash.merge!(namespace_inheritable_hash)
Path.new(path, namespace, path_settings)
namespace_stackable_hash.merge!(namespace_inheritable_hash)
end

def namespace
Expand Down
95 changes: 39 additions & 56 deletions lib/grape/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,66 @@
module Grape
# Represents a path to an endpoint.
class Path
attr_reader :raw_path, :namespace, :settings
DEFAULT_FORMAT_SEGMENT = '(/.:format)'
NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT = '(.:format)'
VERSION_SEGMENT = ':version'

def initialize(raw_path, namespace, settings)
@raw_path = raw_path
@namespace = namespace
@settings = settings
end
attr_reader :origin, :suffix

def mount_path
settings[:mount_path]
def initialize(raw_path, raw_namespace, settings)
@origin = PartsCache[build_parts(raw_path, raw_namespace, settings)]
@suffix = build_suffix(raw_path, raw_namespace, settings)
end

def root_prefix
settings[:root_prefix]
def to_s
"#{origin}#{suffix}"
end

def uses_specific_format?
return false unless settings.key?(:format) && settings.key?(:content_types)
private

settings[:format] && Array(settings[:content_types]).size == 1
def build_suffix(raw_path, raw_namespace, settings)
if uses_specific_format?(settings)
"(.#{settings[:format]})"
elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path))
NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT
else
DEFAULT_FORMAT_SEGMENT
end
end

def uses_path_versioning?
return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)

settings[:version] && settings[:version_options][:using] == :path
def build_parts(raw_path, raw_namespace, settings)
[].tap do |parts|
add_part(parts, settings[:mount_path])
add_part(parts, settings[:root_prefix])
parts << VERSION_SEGMENT if uses_path_versioning?(settings)
add_part(parts, raw_namespace)
add_part(parts, raw_path)
end
end

def namespace?
namespace&.match?(/^\S/) && not_slash?(namespace)
def add_part(parts, value)
parts << value if value && not_slash?(value)
end

def path?
raw_path&.match?(/^\S/) && not_slash?(raw_path)
def not_slash?(value)
value != '/'
end

def suffix
if uses_specific_format?
"(.#{settings[:format]})"
elsif !uses_path_versioning? || (namespace? || path?)
'(.:format)'
else
'(/.:format)'
end
end
def uses_specific_format?(settings)
return false unless settings.key?(:format) && settings.key?(:content_types)

def path
PartsCache[parts]
settings[:format] && Array(settings[:content_types]).size == 1
end

def path_with_suffix
"#{path}#{suffix}"
end
def uses_path_versioning?(settings)
return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)

def to_s
path_with_suffix
settings[:version] && settings[:version_options][:using] == :path
end

private
def valid_part?(part)
part&.match?(/^\S/) && not_slash?(part)
end

class PartsCache < Grape::Util::Cache
def initialize
Expand All @@ -71,23 +72,5 @@ def initialize
end
end
end

def parts
[].tap do |parts|
add_part(parts, mount_path)
add_part(parts, root_prefix)
parts << ':version' if uses_path_versioning?
add_part(parts, namespace)
add_part(parts, raw_path)
end
end

def add_part(parts, value)
parts << value if value && not_slash?(value)
end

def not_slash?(value)
value != '/'
end
end
end
43 changes: 24 additions & 19 deletions lib/grape/router/pattern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ class Pattern

attr_reader :origin, :path, :pattern, :to_regexp

def_delegators :pattern, :named_captures, :params
def_delegators :pattern, :params
def_delegators :to_regexp, :===
alias match? ===

def initialize(pattern, options)
@origin = pattern
@path = build_path(pattern, options)
@pattern = build_pattern(@path, options)
def initialize(origin, suffix, options)
@origin = origin
@path = build_path(origin, options[:anchor], suffix)
@pattern = build_pattern(@path, options[:params], options[:format], options[:version], options[:requirements])
@to_regexp = @pattern.to_regexp
end

Expand All @@ -28,33 +28,34 @@ def captures_default

private

def build_pattern(path, options)
def build_pattern(path, params, format, version, requirements)
Mustermann::Grape.new(
path,
uri_decode: true,
params: options[:params],
capture: extract_capture(options)
params: params,
capture: extract_capture(format, version, requirements)
)
end

def build_path(pattern, options)
PatternCache[[build_path_from_pattern(pattern, options), options[:suffix]]]
def build_path(pattern, anchor, suffix)
PatternCache[[build_path_from_pattern(pattern, anchor), suffix]]
end

def extract_capture(options)
sliced_options = options
.slice(:format, :version)
.delete_if { |_k, v| v.blank? }
.transform_values { |v| Array.wrap(v).map(&:to_s) }
return sliced_options if options[:requirements].blank?
def extract_capture(format, version, requirements)
capture = {}.tap do |h|
h[:format] = map_str(format) if format.present?
h[:version] = map_str(version) if version.present?
end

return capture if requirements.blank?

options[:requirements].merge(sliced_options)
requirements.merge(capture)
end

def build_path_from_pattern(pattern, options)
def build_path_from_pattern(pattern, anchor)
if pattern.end_with?('*path')
pattern.dup.insert(pattern.rindex('/') + 1, '?')
elsif options[:anchor]
elsif anchor
pattern
elsif pattern.end_with?('/')
"#{pattern}?*path"
Expand All @@ -63,6 +64,10 @@ def build_path_from_pattern(pattern, options)
end
end

def map_str(value)
Array.wrap(value).map(&:to_s)
end

class PatternCache < Grape::Util::Cache
def initialize
super
Expand Down
12 changes: 8 additions & 4 deletions lib/grape/router/route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ class Router
class Route < BaseRoute
extend Forwardable

FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) }
NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) }

attr_reader :app, :request_method

def_delegators :pattern, :path, :origin

def initialize(method, pattern, options)
def initialize(method, origin, path, options)
@request_method = upcase_method(method)
@pattern = Grape::Router::Pattern.new(pattern, options)
@pattern = Grape::Router::Pattern.new(origin, path, options)
@match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD
super(options)
end

Expand All @@ -31,7 +35,7 @@ def apply(app)
def match?(input)
return false if input.blank?

options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input)
@match_function.call(input, pattern)
end

def params(input = nil)
Expand All @@ -46,7 +50,7 @@ def params(input = nil)
private

def params_without_input
pattern.captures_default.merge(attributes.params)
@params_without_input ||= pattern.captures_default.merge(attributes.params)
end

def upcase_method(method)
Expand Down
Loading