Skip to content

Commit

Permalink
Improve performance of matchers based on String matcher
Browse files Browse the repository at this point in the history
Use the faster string-based segment matching instead of the slower
regexp-based segment matching.
  • Loading branch information
jeremyevans committed Sep 17, 2024
1 parent 9c1fb59 commit cfedb76
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 3 deletions.
33 changes: 31 additions & 2 deletions lib/roda/plugins/class_matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def self.load_dependencies(app)
def self.configure(app)
app.opts[:class_matchers] ||= {
Integer=>[/(\d{1,100})/, /\A\/(\d{1,100})(?=\/|\z)/, :_convert_class_Integer].freeze,
String=>[/([^\/]+)/, /\A\/([^\/]+)(?=\/|\z)/, nil].freeze
String=>[/([^\/]+)/, nil, nil].freeze
}
end

Expand All @@ -94,7 +94,11 @@ module ClassMethods
# the match block.
def class_matcher(klass, matcher, &block)
_symbol_class_matcher(Class, klass, matcher, block) do |meth, (_, regexp, convert_meth)|
define_method(meth){consume(regexp, convert_meth)}
if regexp
define_method(meth){consume(regexp, convert_meth)}
else
define_method(meth){_consume_segment(convert_meth)}
end
end
end

Expand All @@ -104,6 +108,31 @@ def freeze
super
end
end

module RequestMethods
# Use faster approach for segment matching. This is used for
# matchers based on the String class matcher, and avoids the
# use of regular expressions for scanning.
def _consume_segment(convert_meth)
rp = @remaining_path
if _match_class_String
if convert_meth
if captures = scope.send(convert_meth, @captures.pop)
if captures.is_a?(Array)
@captures.concat(captures)
else
@captures << captures
end
else
@remaining_path = rp
nil
end
else
true
end
end
end
end
end

register_plugin(:class_matchers, ClassMatchers)
Expand Down
7 changes: 6 additions & 1 deletion lib/roda/plugins/symbol_matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,12 @@ def _match_symbol(s)
if respond_to?(meth, true)
# Allow calling private match methods
_, re, convert_meth = send(meth)
consume(re, convert_meth)
if re
consume(re, convert_meth)
else
# defined in class_matchers plugin
_consume_segment(convert_meth)
end
else
super
end
Expand Down
18 changes: 18 additions & 0 deletions spec/plugin/class_matchers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,24 @@ def klass9.to_s; "klass9" end
body("/1000000000000000000001").must_equal ""
end

it "handles conversion block returning falsey for matcher based on String " do
app(:class_matchers) do |r|
r.is(Array){|s| s}
r.remaining_path
end
app.class_matcher(Array, String){|s| s*2 unless s == 'a'}
body('/a').must_equal '/a'
body('/b').must_equal 'bb'
end

it "yields multiple arguments in matcher based on String " do
app(:class_matchers) do |r|
r.is(Array){|s, s2| "#{s}-#{s2}"}
end
app.class_matcher(Array, String){|s| [s, s*2]}
body('/a').must_equal 'a-aa'
end

it "yields hash instances as single arguments" do
app(:class_matchers) do |r|
r.is('a', Array){|h| h.to_a.join(',')}
Expand Down

0 comments on commit cfedb76

Please sign in to comment.