Skip to content

Commit 92573ea

Browse files
authored
Optimize Grape::Path -> Reduce Hash Allocation (#2513)
* Grape::Path is now exposing only path and suffix Last parameter is now double splatted * Remove double splat operator * Optimize default path settings Use const static function for route match? * Revert compile_many_routes.rb * Add CHANGELOG.md
1 parent 86648fa commit 92573ea

File tree

6 files changed

+92
-251
lines changed

6 files changed

+92
-251
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* [#2501](https://github.com/ruby-grape/grape/pull/2501): Remove deprecated `except` and `proc` options in values validator - [@ericproulx](https://github.com/ericproulx).
88
* [#2502](https://github.com/ruby-grape/grape/pull/2502): Remove deprecation `options` in `desc` - [@ericproulx](https://github.com/ericproulx).
99
* [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx).
10+
* [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx).
1011
* Your contribution here.
1112

1213
#### Fixes

lib/grape/endpoint.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,13 @@ def mount_in(router)
160160
end
161161

162162
def to_routes
163-
route_options = prepare_default_route_attributes
164-
map_routes do |method, path|
165-
path = prepare_path(path)
166-
route_options[:suffix] = path.suffix
167-
params = options[:route_options].merge(route_options)
168-
route = Grape::Router::Route.new(method, path.path, params)
163+
default_route_options = prepare_default_route_attributes
164+
default_path_settings = prepare_default_path_settings
165+
166+
map_routes do |method, raw_path|
167+
prepared_path = Path.new(raw_path, namespace, default_path_settings)
168+
params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options
169+
route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params)
169170
route.apply(self)
170171
end.flatten
171172
end
@@ -200,11 +201,10 @@ def map_routes
200201
options[:method].map { |method| options[:path].map { |path| yield method, path } }
201202
end
202203

203-
def prepare_path(path)
204+
def prepare_default_path_settings
204205
namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
205206
namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
206-
path_settings = namespace_stackable_hash.merge!(namespace_inheritable_hash)
207-
Path.new(path, namespace, path_settings)
207+
namespace_stackable_hash.merge!(namespace_inheritable_hash)
208208
end
209209

210210
def namespace

lib/grape/path.rb

Lines changed: 39 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,66 @@
33
module Grape
44
# Represents a path to an endpoint.
55
class Path
6-
attr_reader :raw_path, :namespace, :settings
6+
DEFAULT_FORMAT_SEGMENT = '(/.:format)'
7+
NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT = '(.:format)'
8+
VERSION_SEGMENT = ':version'
79

8-
def initialize(raw_path, namespace, settings)
9-
@raw_path = raw_path
10-
@namespace = namespace
11-
@settings = settings
12-
end
10+
attr_reader :origin, :suffix
1311

14-
def mount_path
15-
settings[:mount_path]
12+
def initialize(raw_path, raw_namespace, settings)
13+
@origin = PartsCache[build_parts(raw_path, raw_namespace, settings)]
14+
@suffix = build_suffix(raw_path, raw_namespace, settings)
1615
end
1716

18-
def root_prefix
19-
settings[:root_prefix]
17+
def to_s
18+
"#{origin}#{suffix}"
2019
end
2120

22-
def uses_specific_format?
23-
return false unless settings.key?(:format) && settings.key?(:content_types)
21+
private
2422

25-
settings[:format] && Array(settings[:content_types]).size == 1
23+
def build_suffix(raw_path, raw_namespace, settings)
24+
if uses_specific_format?(settings)
25+
"(.#{settings[:format]})"
26+
elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path))
27+
NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT
28+
else
29+
DEFAULT_FORMAT_SEGMENT
30+
end
2631
end
2732

28-
def uses_path_versioning?
29-
return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
30-
31-
settings[:version] && settings[:version_options][:using] == :path
33+
def build_parts(raw_path, raw_namespace, settings)
34+
[].tap do |parts|
35+
add_part(parts, settings[:mount_path])
36+
add_part(parts, settings[:root_prefix])
37+
parts << VERSION_SEGMENT if uses_path_versioning?(settings)
38+
add_part(parts, raw_namespace)
39+
add_part(parts, raw_path)
40+
end
3241
end
3342

34-
def namespace?
35-
namespace&.match?(/^\S/) && not_slash?(namespace)
43+
def add_part(parts, value)
44+
parts << value if value && not_slash?(value)
3645
end
3746

38-
def path?
39-
raw_path&.match?(/^\S/) && not_slash?(raw_path)
47+
def not_slash?(value)
48+
value != '/'
4049
end
4150

42-
def suffix
43-
if uses_specific_format?
44-
"(.#{settings[:format]})"
45-
elsif !uses_path_versioning? || (namespace? || path?)
46-
'(.:format)'
47-
else
48-
'(/.:format)'
49-
end
50-
end
51+
def uses_specific_format?(settings)
52+
return false unless settings.key?(:format) && settings.key?(:content_types)
5153

52-
def path
53-
PartsCache[parts]
54+
settings[:format] && Array(settings[:content_types]).size == 1
5455
end
5556

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

60-
def to_s
61-
path_with_suffix
60+
settings[:version] && settings[:version_options][:using] == :path
6261
end
6362

64-
private
63+
def valid_part?(part)
64+
part&.match?(/^\S/) && not_slash?(part)
65+
end
6566

6667
class PartsCache < Grape::Util::Cache
6768
def initialize
@@ -71,23 +72,5 @@ def initialize
7172
end
7273
end
7374
end
74-
75-
def parts
76-
[].tap do |parts|
77-
add_part(parts, mount_path)
78-
add_part(parts, root_prefix)
79-
parts << ':version' if uses_path_versioning?
80-
add_part(parts, namespace)
81-
add_part(parts, raw_path)
82-
end
83-
end
84-
85-
def add_part(parts, value)
86-
parts << value if value && not_slash?(value)
87-
end
88-
89-
def not_slash?(value)
90-
value != '/'
91-
end
9275
end
9376
end

lib/grape/router/pattern.rb

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ class Pattern
99

1010
attr_reader :origin, :path, :pattern, :to_regexp
1111

12-
def_delegators :pattern, :named_captures, :params
12+
def_delegators :pattern, :params
1313
def_delegators :to_regexp, :===
1414
alias match? ===
1515

16-
def initialize(pattern, options)
17-
@origin = pattern
18-
@path = build_path(pattern, options)
19-
@pattern = build_pattern(@path, options)
16+
def initialize(origin, suffix, options)
17+
@origin = origin
18+
@path = build_path(origin, options[:anchor], suffix)
19+
@pattern = build_pattern(@path, options[:params], options[:format], options[:version], options[:requirements])
2020
@to_regexp = @pattern.to_regexp
2121
end
2222

@@ -28,33 +28,34 @@ def captures_default
2828

2929
private
3030

31-
def build_pattern(path, options)
31+
def build_pattern(path, params, format, version, requirements)
3232
Mustermann::Grape.new(
3333
path,
3434
uri_decode: true,
35-
params: options[:params],
36-
capture: extract_capture(options)
35+
params: params,
36+
capture: extract_capture(format, version, requirements)
3737
)
3838
end
3939

40-
def build_path(pattern, options)
41-
PatternCache[[build_path_from_pattern(pattern, options), options[:suffix]]]
40+
def build_path(pattern, anchor, suffix)
41+
PatternCache[[build_path_from_pattern(pattern, anchor), suffix]]
4242
end
4343

44-
def extract_capture(options)
45-
sliced_options = options
46-
.slice(:format, :version)
47-
.delete_if { |_k, v| v.blank? }
48-
.transform_values { |v| Array.wrap(v).map(&:to_s) }
49-
return sliced_options if options[:requirements].blank?
44+
def extract_capture(format, version, requirements)
45+
capture = {}.tap do |h|
46+
h[:format] = map_str(format) if format.present?
47+
h[:version] = map_str(version) if version.present?
48+
end
49+
50+
return capture if requirements.blank?
5051

51-
options[:requirements].merge(sliced_options)
52+
requirements.merge(capture)
5253
end
5354

54-
def build_path_from_pattern(pattern, options)
55+
def build_path_from_pattern(pattern, anchor)
5556
if pattern.end_with?('*path')
5657
pattern.dup.insert(pattern.rindex('/') + 1, '?')
57-
elsif options[:anchor]
58+
elsif anchor
5859
pattern
5960
elsif pattern.end_with?('/')
6061
"#{pattern}?*path"
@@ -63,6 +64,10 @@ def build_path_from_pattern(pattern, options)
6364
end
6465
end
6566

67+
def map_str(value)
68+
Array.wrap(value).map(&:to_s)
69+
end
70+
6671
class PatternCache < Grape::Util::Cache
6772
def initialize
6873
super

lib/grape/router/route.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ class Router
55
class Route < BaseRoute
66
extend Forwardable
77

8+
FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) }
9+
NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) }
10+
811
attr_reader :app, :request_method
912

1013
def_delegators :pattern, :path, :origin
1114

12-
def initialize(method, pattern, options)
15+
def initialize(method, origin, path, options)
1316
@request_method = upcase_method(method)
14-
@pattern = Grape::Router::Pattern.new(pattern, options)
17+
@pattern = Grape::Router::Pattern.new(origin, path, options)
18+
@match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD
1519
super(options)
1620
end
1721

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

34-
options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input)
38+
@match_function.call(input, pattern)
3539
end
3640

3741
def params(input = nil)
@@ -46,7 +50,7 @@ def params(input = nil)
4650
private
4751

4852
def params_without_input
49-
pattern.captures_default.merge(attributes.params)
53+
@params_without_input ||= pattern.captures_default.merge(attributes.params)
5054
end
5155

5256
def upcase_method(method)

0 commit comments

Comments
 (0)