mustermann 3.0.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -2
- data/README.md +238 -261
- data/lib/mustermann/ast/compiler.rb +128 -30
- data/lib/mustermann/ast/converters.rb +41 -0
- data/lib/mustermann/ast/expander.rb +4 -5
- data/lib/mustermann/ast/fast_pattern.rb +122 -0
- data/lib/mustermann/ast/param_scanner.rb +38 -6
- data/lib/mustermann/ast/parser.rb +2 -3
- data/lib/mustermann/ast/pattern.rb +26 -4
- data/lib/mustermann/ast/transformer.rb +7 -0
- data/lib/mustermann/ast/translator.rb +11 -7
- data/lib/mustermann/composite.rb +25 -6
- data/lib/mustermann/concat.rb +16 -5
- data/lib/mustermann/error.rb +1 -0
- data/lib/mustermann/expander.rb +26 -4
- data/lib/mustermann/hybrid.rb +50 -0
- data/lib/mustermann/match.rb +155 -0
- data/lib/mustermann/pattern.rb +27 -32
- data/lib/mustermann/rails.rb +63 -0
- data/lib/mustermann/regexp_based.rb +70 -9
- data/lib/mustermann/router.rb +104 -0
- data/lib/mustermann/set/cache.rb +48 -0
- data/lib/mustermann/set/linear.rb +32 -0
- data/lib/mustermann/set/match.rb +23 -0
- data/lib/mustermann/set/strict_order.rb +29 -0
- data/lib/mustermann/set/trie.rb +270 -0
- data/lib/mustermann/set.rb +445 -0
- data/lib/mustermann/sinatra/safe_renderer.rb +1 -1
- data/lib/mustermann/sinatra/try_convert.rb +49 -11
- data/lib/mustermann/sinatra.rb +35 -11
- data/lib/mustermann/version.rb +1 -1
- data/lib/mustermann/versions.rb +47 -0
- data/lib/mustermann.rb +0 -15
- metadata +31 -45
- data/bench/capturing.rb +0 -57
- data/bench/regexp.rb +0 -21
- data/bench/simple_vs_sinatra.rb +0 -23
- data/bench/template_vs_addressable.rb +0 -26
- data/bench/uri_parser_object.rb +0 -16
- data/lib/mustermann/extension.rb +0 -3
- data/lib/mustermann/mapper.rb +0 -91
- data/lib/mustermann/pattern_cache.rb +0 -50
- data/lib/mustermann/simple_match.rb +0 -49
- data/lib/mustermann/to_pattern.rb +0 -51
- data/mustermann.gemspec +0 -18
- data/spec/ast_spec.rb +0 -15
- data/spec/composite_spec.rb +0 -163
- data/spec/concat_spec.rb +0 -127
- data/spec/equality_map_spec.rb +0 -42
- data/spec/expander_spec.rb +0 -123
- data/spec/identity_spec.rb +0 -127
- data/spec/mapper_spec.rb +0 -77
- data/spec/mustermann_spec.rb +0 -81
- data/spec/pattern_spec.rb +0 -54
- data/spec/regexp_based_spec.rb +0 -9
- data/spec/regular_spec.rb +0 -119
- data/spec/simple_match_spec.rb +0 -11
- data/spec/sinatra_spec.rb +0 -836
- data/spec/to_pattern_spec.rb +0 -70
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann'
|
|
3
|
+
require 'mustermann/set'
|
|
4
|
+
|
|
5
|
+
module Mustermann
|
|
6
|
+
# An extremely simple, Rack-compatible router implementation using {Mustermann::Set} for pattern matching.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# require 'mustermann/router'
|
|
10
|
+
#
|
|
11
|
+
# router = Mustermann::Router.new do
|
|
12
|
+
# get "/hello/:name" do |env|
|
|
13
|
+
# name = env["mustermann.match"][:name]
|
|
14
|
+
# [200, { "content-type" => "text/plain" }, ["Hello, #{name}!"]]
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # in config.ru
|
|
19
|
+
# use Rack::Head
|
|
20
|
+
# run router
|
|
21
|
+
#
|
|
22
|
+
# @example Routing to other applications
|
|
23
|
+
# router = Mustermann::Router.new do
|
|
24
|
+
# get "/users", MyApp::Users::Index
|
|
25
|
+
# get "/users/:id", MyApp::Users::Show
|
|
26
|
+
# post "/users", MyApp::Users::Create
|
|
27
|
+
# fallback MyApp::NotFound
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# router.path_for(MyApp::Users::Show, id: 42) # => "/users/42"
|
|
31
|
+
#
|
|
32
|
+
# @example As middleware
|
|
33
|
+
# use Mustermann::Router do
|
|
34
|
+
# get("/up") { [200, { "content-type" => "text/plain" }, ["Up!"]] }
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# run MyApp
|
|
38
|
+
#
|
|
39
|
+
# @see Mustermann::Set
|
|
40
|
+
# @see https://rack.github.io/rack/
|
|
41
|
+
class Router
|
|
42
|
+
NOT_FOUND = [404, { "content-type" => "text/plain", "x-cascade" => "pass" }, ["Not found"]].freeze
|
|
43
|
+
VERBS = %w[GET POST PUT PATCH DELETE OPTIONS LINK UNLINK].freeze
|
|
44
|
+
private_constant :VERBS, :NOT_FOUND
|
|
45
|
+
|
|
46
|
+
# Initializes a new router.
|
|
47
|
+
# @param key [String] The key under which the route match will be stored in the Rack environment hash (default: "mustermann.match").
|
|
48
|
+
# @param options [Hash] Options to be passed to the Mustermann patterns.
|
|
49
|
+
def initialize(fallback = nil, key: "mustermann.match", **options, &block)
|
|
50
|
+
@key = key
|
|
51
|
+
@sets = VERBS.to_h { |verb| [verb, Set.new(**options)] }
|
|
52
|
+
@fallback = fallback || ->(env) { NOT_FOUND.dup }
|
|
53
|
+
|
|
54
|
+
if block_given?
|
|
55
|
+
instance_exec(&block)
|
|
56
|
+
@sets.each_value(&:optimize!)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param env [Hash] The Rack environment hash for the request.
|
|
61
|
+
# @return [Array] The Rack response array (status, headers, body).
|
|
62
|
+
def call(env)
|
|
63
|
+
request_method = env["REQUEST_METHOD"] || "GET"
|
|
64
|
+
request_method = "GET" if request_method == "HEAD"
|
|
65
|
+
if routes = @sets[request_method] and match = routes.match(env["PATH_INFO"] || "/")
|
|
66
|
+
env[@key] = match
|
|
67
|
+
return match.value.call(env)
|
|
68
|
+
end
|
|
69
|
+
@fallback.call(env)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def fallback(fallback = nil, &block) = @fallback = fallback || block || @fallback
|
|
73
|
+
|
|
74
|
+
# Adds a route for the given verb and pattern, with the given target.
|
|
75
|
+
#
|
|
76
|
+
# @note Shorthand methods, like `get`, `post`, etc. dynamically are defined for all supported verbs.
|
|
77
|
+
#
|
|
78
|
+
# @param verb [String] HTTP verb (e.g. "GET", "POST")
|
|
79
|
+
# @param pattern [String, Mustermann::Pattern] Pattern string or Mustermann pattern (e.g. "/users/:id")
|
|
80
|
+
# @param target [#call, nil] The Rack application or middleware to call when the route matches. Can be passed a block as well.
|
|
81
|
+
# @yield [env] Block to be used as the target if no explicit target is given.
|
|
82
|
+
# @yieldparam env [Hash] The Rack environment hash for the request.
|
|
83
|
+
# @return [void]
|
|
84
|
+
def route(verb, pattern, target = nil, **options, &block)
|
|
85
|
+
raise ArgumentError, "need to provide target, :to or a block" unless target || block
|
|
86
|
+
raise ArgumentError, "unknown verb: #{verb}" unless VERBS.include?(verb)
|
|
87
|
+
@sets[verb].add(pattern, target || block)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Helps generate links
|
|
91
|
+
#
|
|
92
|
+
# @param app [#call] The Rack application or middleware for which to generate the path.
|
|
93
|
+
# @param (see Mustermann::Expander#expand)
|
|
94
|
+
# @return [String] The generated path.
|
|
95
|
+
def path_for(app, behavior = nil, params = {})
|
|
96
|
+
set = @sets.values.find { |s| s.has_value?(app) } || @sets[VERBS.first]
|
|
97
|
+
set.expand(app, behavior, params)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
VERBS.each do |verb|
|
|
101
|
+
define_method(verb.downcase) { |*args, **opts, &block| route(verb, *args, **opts, &block) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann/equality_map'
|
|
3
|
+
|
|
4
|
+
module Mustermann
|
|
5
|
+
class Set
|
|
6
|
+
class Cache
|
|
7
|
+
PLACEHOLDER = Object.new.freeze
|
|
8
|
+
EMPTY_ARRAY = [].freeze
|
|
9
|
+
|
|
10
|
+
def self.new(matcher) = defined?(ObjectSpace::WeakKeyMap) ? super : matcher
|
|
11
|
+
|
|
12
|
+
def initialize(matcher)
|
|
13
|
+
@matcher = matcher
|
|
14
|
+
reset_cache
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add(pattern)
|
|
18
|
+
@matcher.add(pattern)
|
|
19
|
+
reset_cache
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def match(string, all: false, peek: false)
|
|
23
|
+
cache = @match_cache[all][peek]
|
|
24
|
+
result = cache[string] ||= @matcher.match(string, all: all, peek: peek) || PLACEHOLDER
|
|
25
|
+
return result unless result.equal? PLACEHOLDER
|
|
26
|
+
all ? EMPTY_ARRAY : nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset_cache
|
|
30
|
+
@match_cache = {
|
|
31
|
+
true => {
|
|
32
|
+
true => ObjectSpace::WeakKeyMap.new,
|
|
33
|
+
false => ObjectSpace::WeakKeyMap.new
|
|
34
|
+
},
|
|
35
|
+
false => {
|
|
36
|
+
true => ObjectSpace::WeakKeyMap.new,
|
|
37
|
+
false => ObjectSpace::WeakKeyMap.new
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def optimize! = @matcher.optimize!
|
|
43
|
+
def track(...) = @matcher.track(...)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private_constant :Cache
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann/set/match'
|
|
3
|
+
|
|
4
|
+
module Mustermann
|
|
5
|
+
class Set
|
|
6
|
+
class Linear
|
|
7
|
+
def initialize(set, patterns = [])
|
|
8
|
+
@set = set
|
|
9
|
+
@patterns = patterns
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add(pattern)
|
|
13
|
+
@patterns << pattern
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def match(string, all: false, peek: false)
|
|
17
|
+
result = [] if all
|
|
18
|
+
@patterns.each do |pattern|
|
|
19
|
+
next unless match = peek ? pattern.peek_match(string) : pattern.match(string)
|
|
20
|
+
return Match.new(match, value: @set.values_for_pattern(pattern)&.first) unless all
|
|
21
|
+
values = @set.values_for_pattern(pattern) || [nil]
|
|
22
|
+
values.each { |value| result << Match.new(match, value:) }
|
|
23
|
+
end
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def optimize! = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private_constant :Linear
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann/match'
|
|
3
|
+
|
|
4
|
+
module Mustermann
|
|
5
|
+
class Set
|
|
6
|
+
|
|
7
|
+
# Subclass of {Mustermann::Match} that also includes the value associated with the pattern that produced the match.
|
|
8
|
+
class Match < Mustermann::Match
|
|
9
|
+
# @return the value associated with the pattern that produced the match, if any
|
|
10
|
+
attr_reader :value
|
|
11
|
+
|
|
12
|
+
# (see Mustermann::Match#initialize)
|
|
13
|
+
# @option options [Object] :value the value associated with the pattern that produced the match, if any
|
|
14
|
+
def initialize(*, value: nil, **)
|
|
15
|
+
@value = value
|
|
16
|
+
super(*, **)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @see Mustermann::Match#eql?
|
|
20
|
+
def eql?(other) = super && value == other.value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mustermann
|
|
4
|
+
class Set
|
|
5
|
+
class StrictOrder
|
|
6
|
+
def initialize(matcher)
|
|
7
|
+
@matcher = matcher
|
|
8
|
+
@order = {}
|
|
9
|
+
@count = 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add(...) = @matcher.add(...)
|
|
13
|
+
def optimize! = @matcher.optimize!
|
|
14
|
+
|
|
15
|
+
def match(string, all: false, peek: false)
|
|
16
|
+
possible = @matcher.match(string, all: true, peek: peek)
|
|
17
|
+
possible.sort_by! { |m| @order.dig(m.pattern, m.value) }
|
|
18
|
+
all ? possible : possible.first
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def track(pattern, value)
|
|
22
|
+
@order[pattern] ||= {}
|
|
23
|
+
@order[pattern][value] = @count += 1
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private_constant :StrictOrder
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann/ast/translator'
|
|
3
|
+
require 'mustermann/set/match'
|
|
4
|
+
|
|
5
|
+
module Mustermann
|
|
6
|
+
class Set
|
|
7
|
+
class Trie
|
|
8
|
+
class Translator < AST::Translator
|
|
9
|
+
translate(:node) { |trie, **o| trie[t.compile(node)] }
|
|
10
|
+
translate(:separator) { |trie, **options| trie[payload] }
|
|
11
|
+
|
|
12
|
+
translate(:root) do |trie, **options|
|
|
13
|
+
leaves = t(payload, trie, **options)
|
|
14
|
+
if leaves.is_a? Array
|
|
15
|
+
leaves.each { |leaf| leaf.patterns << t.pattern }
|
|
16
|
+
else
|
|
17
|
+
leaves.patterns << t.pattern
|
|
18
|
+
end
|
|
19
|
+
leaves
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
translate(:char) do |trie, **options|
|
|
23
|
+
return trie if payload.empty?
|
|
24
|
+
trie[payload]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
translate(:optional) do |trie, **options|
|
|
28
|
+
[*t(payload, trie, **options), trie]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
translate(Array) do |trie, **options|
|
|
32
|
+
i = 0
|
|
33
|
+
while i < size
|
|
34
|
+
element = self[i]
|
|
35
|
+
if element.is_a? :char or element.is_a? :separator
|
|
36
|
+
trie = t(element, trie, **options)
|
|
37
|
+
i += 1
|
|
38
|
+
elsif element.is_a? :splat and self[i + 1]&.is_a? :separator
|
|
39
|
+
# Compile splat+separator together so the splat is bounded by the separator,
|
|
40
|
+
# then continue building the trie for the remaining elements.
|
|
41
|
+
trie = trie[t.compile(self[i..i + 1])]
|
|
42
|
+
i += 2
|
|
43
|
+
elsif element.is_a? :splat or !self[i + 1]&.is_a? :separator
|
|
44
|
+
return trie[t.compile(self[i..-1])]
|
|
45
|
+
else
|
|
46
|
+
trie = t(element, trie, **options)
|
|
47
|
+
return trie.flat_map { |node| t(self[i + 1..-1], node, **options) } if trie.is_a? Array
|
|
48
|
+
i += 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
trie
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attr_reader :pattern
|
|
55
|
+
|
|
56
|
+
def initialize(pattern)
|
|
57
|
+
@pattern = pattern
|
|
58
|
+
@compiler = pattern.compiler.new
|
|
59
|
+
@options = pattern.options
|
|
60
|
+
super()
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# \G anchors to a position passed to String#match, avoiding substring allocation.
|
|
64
|
+
def compile(node, **options) = /\G#{@compiler.translate(node, **@options, **options)}/
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attr_reader :patterns, :set, :static, :dynamic
|
|
68
|
+
|
|
69
|
+
def initialize(set, patterns = [])
|
|
70
|
+
@set = set
|
|
71
|
+
@patterns = []
|
|
72
|
+
@dynamic = {}
|
|
73
|
+
@static = {}
|
|
74
|
+
@stride = nil
|
|
75
|
+
@fast_static = nil
|
|
76
|
+
@byte_lookup = nil
|
|
77
|
+
@dynamic_entries = nil
|
|
78
|
+
patterns.each { |pattern| add(pattern) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def [](key)
|
|
82
|
+
case key
|
|
83
|
+
when String then @static[key] ||= Trie.new(@set)
|
|
84
|
+
when Regexp then @dynamic[key] ||= Trie.new(@set)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def match(string, all: false, peek: false, position: 0, params: {})
|
|
89
|
+
optimize! if @stride.nil?
|
|
90
|
+
return build_matches(string, params, all:) if position >= string.size
|
|
91
|
+
result = [] if all
|
|
92
|
+
|
|
93
|
+
if @fast_static
|
|
94
|
+
stride = @stride
|
|
95
|
+
if node = @fast_static[string[position, stride]]
|
|
96
|
+
if nested_result = node.match(string, all:, peek:, position: position + stride, params:)
|
|
97
|
+
return nested_result unless all
|
|
98
|
+
result.concat(nested_result)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
elsif @byte_lookup
|
|
102
|
+
if node = @byte_lookup[string.getbyte(position)]
|
|
103
|
+
if nested_result = node.match(string, all:, peek:, position: position + 1, params:)
|
|
104
|
+
return nested_result unless all
|
|
105
|
+
result.concat(nested_result)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
unless @dynamic_entries.empty?
|
|
111
|
+
anchored = nil
|
|
112
|
+
base_params = all ? params : nil
|
|
113
|
+
@dynamic_entries.each do |matcher, node, capture_names, fast_name|
|
|
114
|
+
if fast_name
|
|
115
|
+
# Fast path: unconstrained single-segment capture — no regex, no MatchData.
|
|
116
|
+
end_pos = string.index('/', position) || string.size
|
|
117
|
+
next if end_pos == position
|
|
118
|
+
edge_params = all ? base_params.dup : params
|
|
119
|
+
edge_params[fast_name] = string.byteslice(position, end_pos - position)
|
|
120
|
+
nested_result = node.match(string, all:, params: edge_params, peek:, position: end_pos)
|
|
121
|
+
return nested_result unless all
|
|
122
|
+
result.concat(nested_result)
|
|
123
|
+
next
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
regexp_match = matcher.match(string, position)
|
|
127
|
+
# Non-greedy patterns (e.g. splat .*?) can match 0 chars on non-empty input, making
|
|
128
|
+
# no progress. Retry with an end-of-string anchor so they consume the full remainder.
|
|
129
|
+
if regexp_match && regexp_match.end(0) == position
|
|
130
|
+
anchored ||= {}
|
|
131
|
+
anchored_matcher = anchored[matcher] ||= Regexp.new(matcher.source + '\z')
|
|
132
|
+
regexp_match = anchored_matcher.match(string, position)
|
|
133
|
+
end
|
|
134
|
+
next unless regexp_match
|
|
135
|
+
|
|
136
|
+
edge_params = all ? base_params.dup : params
|
|
137
|
+
capture_names.each do |name|
|
|
138
|
+
value = regexp_match[name]
|
|
139
|
+
next unless value
|
|
140
|
+
existing = edge_params[name]
|
|
141
|
+
edge_params[name] = existing ? (existing.is_a?(Array) ? existing << value : [existing, value]) : value
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
nested_result = node.match(string, all:, params: edge_params, peek:, position: regexp_match.end(0))
|
|
145
|
+
return nested_result unless all
|
|
146
|
+
result.concat(nested_result)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if peek
|
|
151
|
+
matches = build_matches(string, params, all:, matched_length: position, post_match: string[position..], pre_match: '')
|
|
152
|
+
return matches unless all
|
|
153
|
+
result.concat(matches)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
result
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
NIL_VALUES = [nil].freeze
|
|
160
|
+
|
|
161
|
+
def build_matches(string, params, all: false, matched_length: string.size, post_match: '', pre_match: '')
|
|
162
|
+
result = [] if all
|
|
163
|
+
matched = string[0, matched_length]
|
|
164
|
+
|
|
165
|
+
@patterns.each do |pattern|
|
|
166
|
+
next if pattern.except_regexp&.match?(matched)
|
|
167
|
+
|
|
168
|
+
pattern_params = build_pattern_params(pattern, params)
|
|
169
|
+
|
|
170
|
+
values = @set.values_for_pattern(pattern) || NIL_VALUES
|
|
171
|
+
values.each do |value|
|
|
172
|
+
match = Set::Match.new(pattern, string, matched:, params: pattern_params, value:, post_match:, pre_match:)
|
|
173
|
+
return match unless all
|
|
174
|
+
result << match
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
result
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def build_pattern_params(pattern, params)
|
|
182
|
+
return params if pattern.identity_params?(params)
|
|
183
|
+
|
|
184
|
+
result = {}
|
|
185
|
+
params.each do |key, raw|
|
|
186
|
+
if raw.is_a?(Array)
|
|
187
|
+
val = raw.flat_map { |v| pattern.map_param(key, v) }
|
|
188
|
+
val = val.first if val.size < 2 && !pattern.always_array?(key)
|
|
189
|
+
else
|
|
190
|
+
val = pattern.map_param(key, raw)
|
|
191
|
+
val = [val] if pattern.always_array?(key)
|
|
192
|
+
end
|
|
193
|
+
result[key] = val
|
|
194
|
+
end
|
|
195
|
+
result
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def add(pattern)
|
|
199
|
+
@stride = nil
|
|
200
|
+
@fast_static = nil
|
|
201
|
+
@byte_lookup = nil
|
|
202
|
+
@dynamic_entries = nil
|
|
203
|
+
Translator.new(pattern).translate(pattern.to_ast, self)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Compacts the trie by replacing sequential single-char static lookups with a
|
|
207
|
+
# single stride-length hash lookup. The stride is the minimum number of static
|
|
208
|
+
# steps all paths from this node share before hitting a dynamic edge or branch.
|
|
209
|
+
def optimize!
|
|
210
|
+
depth = min_static_depth
|
|
211
|
+
if depth > 1
|
|
212
|
+
@fast_static = build_stride_hash(depth)
|
|
213
|
+
@byte_lookup = nil
|
|
214
|
+
@stride = depth
|
|
215
|
+
@fast_static.each_value(&:optimize!)
|
|
216
|
+
elsif @static.empty?
|
|
217
|
+
@fast_static = nil
|
|
218
|
+
@byte_lookup = nil
|
|
219
|
+
@stride = 1
|
|
220
|
+
# no children to recurse into
|
|
221
|
+
else
|
|
222
|
+
@fast_static = nil
|
|
223
|
+
@byte_lookup = Array.new(256)
|
|
224
|
+
@static.each { |k, v| @byte_lookup[k.getbyte(0)] = v }
|
|
225
|
+
@stride = 1
|
|
226
|
+
@static.each_value(&:optimize!)
|
|
227
|
+
end
|
|
228
|
+
@dynamic.each_value(&:optimize!)
|
|
229
|
+
@dynamic_entries = @dynamic.map do |matcher, node|
|
|
230
|
+
names = matcher.names.each(&:freeze)
|
|
231
|
+
# Detect unconstrained single-segment captures: can use fast string.index instead of regex.
|
|
232
|
+
# Two conditions: (1) the edge is a bare capture (source starts with \G(?<name>), no leading
|
|
233
|
+
# static chars) and (2) the capture's character class excludes '/' (PATH_INFO never has '?' or '#').
|
|
234
|
+
fast = if names.size == 1
|
|
235
|
+
name = names.first
|
|
236
|
+
matcher.source.start_with?("\\G(?<#{name}>") && !matcher.match?('/') ? name : nil
|
|
237
|
+
end
|
|
238
|
+
[matcher, node, names, fast]
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
protected
|
|
243
|
+
|
|
244
|
+
# Returns the minimum number of guaranteed static steps from this node across
|
|
245
|
+
# all possible paths, before encountering a dynamic edge, a terminal pattern,
|
|
246
|
+
# or an empty node. Branching is allowed; only the minimum depth matters.
|
|
247
|
+
def min_static_depth
|
|
248
|
+
return 0 if @dynamic.any?
|
|
249
|
+
return 0 if @patterns.any?
|
|
250
|
+
return 0 if @static.empty?
|
|
251
|
+
1 + @static.values.map { |node| node.min_static_depth }.min
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
private
|
|
255
|
+
|
|
256
|
+
# Builds a hash whose keys are +stride+-character strings and whose values are
|
|
257
|
+
# the trie nodes reached after consuming exactly those characters.
|
|
258
|
+
def build_stride_hash(stride)
|
|
259
|
+
stride.times.reduce({ "" => self }) do |frontier, _|
|
|
260
|
+
frontier.each_with_object({}) do |(prefix, node), nxt|
|
|
261
|
+
node.static.each { |char, child| nxt[prefix + char] = child }
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
private_constant :Trie
|
|
269
|
+
end
|
|
270
|
+
end
|