ruby_routes 0.2.0 → 1.1.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/lib/ruby_routes/constant.rb +57 -0
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +13 -0
- data/lib/ruby_routes/lru_strategies/miss_strategy.rb +10 -0
- data/lib/ruby_routes/node.rb +43 -2
- data/lib/ruby_routes/radix_tree.rb +101 -67
- data/lib/ruby_routes/route/small_lru.rb +43 -0
- data/lib/ruby_routes/route.rb +284 -161
- data/lib/ruby_routes/route_set.rb +137 -17
- data/lib/ruby_routes/segment.rb +20 -0
- data/lib/ruby_routes/segments/base_segment.rb +21 -0
- data/lib/ruby_routes/segments/dynamic_segment.rb +23 -0
- data/lib/ruby_routes/segments/static_segment.rb +18 -0
- data/lib/ruby_routes/segments/wildcard_segment.rb +27 -0
- data/lib/ruby_routes/version.rb +1 -1
- metadata +10 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15debcef313430cfc799afcb9ba0b6f9bd8292226d023d7e854afc608d5ede64
|
4
|
+
data.tar.gz: 1d7c971980a984738c6239cc1727376b74ab61ae824ee054a92f254451574bcf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f272672f91127b65fab8ed4ae2eacfb95fe55310fc4bcd017b5656a0eb475e05255884f19416520ce8b36ec6ff51602561b6f9519befc1f7cf7448bca4ab3f9
|
7
|
+
data.tar.gz: c3cc386c128531ef9bb268bb904c806e1632c3d88609fd3de5077d8143f0fa56a1089ea0915de3bf2e1116c0d0d812402877480d6edc702f8923d3ee3252ef9a
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative 'segments/dynamic_segment'
|
2
|
+
require_relative 'segments/static_segment'
|
3
|
+
require_relative 'segments/wildcard_segment'
|
4
|
+
require_relative 'lru_strategies/hit_strategy'
|
5
|
+
require_relative 'lru_strategies/miss_strategy'
|
6
|
+
|
7
|
+
module RubyRoutes
|
8
|
+
module Constant
|
9
|
+
SEGMENTS = {
|
10
|
+
42 => RubyRoutes::Segments::WildcardSegment, # '*'
|
11
|
+
58 => RubyRoutes::Segments::DynamicSegment, # ':'
|
12
|
+
:default => RubyRoutes::Segments::StaticSegment
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
SEGMENT_MATCHERS = {
|
16
|
+
static: lambda do |node, segment, _idx, _segments, _params|
|
17
|
+
child = node.static_children[segment]
|
18
|
+
child ? [child, false] : nil
|
19
|
+
end,
|
20
|
+
|
21
|
+
dynamic: lambda do |node, segment, _idx, _segments, params|
|
22
|
+
return nil unless node.dynamic_child
|
23
|
+
nxt = node.dynamic_child
|
24
|
+
params[nxt.param_name.to_s] = segment if params && nxt.param_name
|
25
|
+
[nxt, false]
|
26
|
+
end,
|
27
|
+
|
28
|
+
wildcard: lambda do |node, _segment, idx, segments, params|
|
29
|
+
return nil unless node.wildcard_child
|
30
|
+
nxt = node.wildcard_child
|
31
|
+
params[nxt.param_name.to_s] = segments[idx..-1].join('/') if params && nxt.param_name
|
32
|
+
[nxt, true]
|
33
|
+
end,
|
34
|
+
|
35
|
+
# default returns nil (no match). RadixTree#find will then return [nil, {}].
|
36
|
+
default: lambda { |_node, _segment, _idx, _segments, _params| nil }
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
# singleton instances to avoid per-LRU allocations
|
40
|
+
LRU_HIT_STRATEGY = RubyRoutes::LruStrategies::HitStrategy.new.freeze
|
41
|
+
LRU_MISS_STRATEGY = RubyRoutes::LruStrategies::MissStrategy.new.freeze
|
42
|
+
|
43
|
+
# Descriptor factories for segment classification (O(1) dispatch by first byte).
|
44
|
+
DESCRIPTOR_FACTORIES = {
|
45
|
+
42 => ->(s) { { type: :splat, name: (s[1..-1] || 'splat') } }, # '*'
|
46
|
+
58 => ->(s) { { type: :param, name: s[1..-1] } }, # ':'
|
47
|
+
:default => ->(s) { { type: :static, value: s } }
|
48
|
+
}.freeze
|
49
|
+
|
50
|
+
def self.segment_descriptor(raw)
|
51
|
+
s = raw.to_s
|
52
|
+
key = s.empty? ? :default : s.getbyte(0)
|
53
|
+
factory = DESCRIPTOR_FACTORIES[key] || DESCRIPTOR_FACTORIES[:default]
|
54
|
+
factory.call(s)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/ruby_routes/node.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative 'segment'
|
2
|
+
|
1
3
|
module RubyRoutes
|
2
4
|
class Node
|
3
5
|
attr_accessor :static_children, :dynamic_child, :wildcard_child,
|
@@ -12,13 +14,52 @@ module RubyRoutes
|
|
12
14
|
@is_endpoint = false
|
13
15
|
end
|
14
16
|
|
17
|
+
# Fast traversal: minimal allocations, streamlined branching
|
18
|
+
# Returns [next_node_or_nil, should_break_bool] or [nil, false] if no match.
|
19
|
+
def traverse_for(segment, index, segments, params)
|
20
|
+
# Static match: O(1) hash lookup
|
21
|
+
child = @static_children[segment]
|
22
|
+
return [child, false] if child
|
23
|
+
|
24
|
+
# Dynamic match: single segment capture
|
25
|
+
if (dyn = @dynamic_child)
|
26
|
+
params[dyn.param_name] = segment if params
|
27
|
+
return [dyn, false]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Wildcard match: consume remainder (last resort)
|
31
|
+
if (wild = @wildcard_child)
|
32
|
+
if params
|
33
|
+
# Build remainder path without intermediate array allocation
|
34
|
+
remainder = segments[index..-1]
|
35
|
+
params[wild.param_name] = remainder.size == 1 ? remainder[0] : remainder.join('/')
|
36
|
+
end
|
37
|
+
return [wild, true]
|
38
|
+
end
|
39
|
+
|
40
|
+
# No match
|
41
|
+
[nil, false]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Pre-cache param names as strings to avoid repeated .to_s calls
|
45
|
+
def param_name
|
46
|
+
@param_name_str ||= @param_name&.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def param_name=(name)
|
50
|
+
@param_name = name
|
51
|
+
@param_name_str = nil # invalidate cache
|
52
|
+
end
|
53
|
+
|
54
|
+
# Normalize method once and cache string keys
|
15
55
|
def add_handler(method, handler)
|
16
|
-
|
56
|
+
method_key = method.to_s.upcase
|
57
|
+
@handlers[method_key] = handler
|
17
58
|
@is_endpoint = true
|
18
59
|
end
|
19
60
|
|
20
61
|
def get_handler(method)
|
21
|
-
@handlers[method
|
62
|
+
@handlers[method] # assume already normalized upstream
|
22
63
|
end
|
23
64
|
end
|
24
65
|
end
|
@@ -1,13 +1,11 @@
|
|
1
|
+
require_relative 'segment'
|
2
|
+
|
1
3
|
module RubyRoutes
|
2
4
|
class RadixTree
|
3
5
|
class << self
|
4
6
|
# Allow RadixTree.new(path, options...) to act as a convenience factory
|
5
|
-
# returning a Route (this matches test usage where specs call
|
6
|
-
# RadixTree.new('/path', to: 'controller#action')).
|
7
|
-
# Calling RadixTree.new with no arguments returns an actual RadixTree instance.
|
8
7
|
def new(*args, &block)
|
9
8
|
if args.any?
|
10
|
-
# Delegate to Route initializer when args are provided
|
11
9
|
RubyRoutes::Route.new(*args, &block)
|
12
10
|
else
|
13
11
|
super()
|
@@ -17,64 +15,85 @@ module RubyRoutes
|
|
17
15
|
|
18
16
|
def initialize
|
19
17
|
@root = Node.new
|
20
|
-
@
|
21
|
-
@split_cache_order = []
|
22
|
-
@split_cache_max =
|
18
|
+
@split_cache = {}
|
19
|
+
@split_cache_order = []
|
20
|
+
@split_cache_max = 2048 # larger cache for better hit rates
|
21
|
+
@empty_segments = [].freeze # reuse for root path
|
23
22
|
end
|
24
23
|
|
25
24
|
def add(path, methods, handler)
|
26
|
-
segments = split_path(path)
|
27
25
|
current = @root
|
26
|
+
segments = split_path_raw(path)
|
28
27
|
|
29
|
-
segments.each do |
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
current.param_name = segment[1..-1] || 'splat'
|
34
|
-
break
|
35
|
-
elsif segment.start_with?(':')
|
36
|
-
current.dynamic_child ||= Node.new
|
37
|
-
current = current.dynamic_child
|
38
|
-
current.param_name = segment[1..-1]
|
39
|
-
else
|
40
|
-
current.static_children[segment] ||= Node.new
|
41
|
-
current = current.static_children[segment]
|
42
|
-
end
|
28
|
+
segments.each do |raw_seg|
|
29
|
+
seg = RubyRoutes::Segment.for(raw_seg)
|
30
|
+
current = seg.ensure_child(current)
|
31
|
+
break if seg.wildcard?
|
43
32
|
end
|
44
33
|
|
45
|
-
|
34
|
+
# Normalize methods once during registration
|
35
|
+
Array(methods).each { |method| current.add_handler(method.to_s.upcase, handler) }
|
46
36
|
end
|
47
37
|
|
48
|
-
def find(path, method)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
if current.static_children.key?(segment)
|
55
|
-
current = current.static_children[segment]
|
56
|
-
elsif current.dynamic_child
|
57
|
-
current = current.dynamic_child
|
58
|
-
# keep string keys to avoid symbol allocations and extra conversions later
|
59
|
-
params[current.param_name.to_s] = segment
|
60
|
-
elsif current.wildcard_child
|
61
|
-
current = current.wildcard_child
|
62
|
-
params[current.param_name.to_s] = segments[index..-1].join('/')
|
63
|
-
break
|
38
|
+
def find(path, method, params_out = nil)
|
39
|
+
# Fast path: root route
|
40
|
+
if path == '/' || path.empty?
|
41
|
+
handler = @root.get_handler(method)
|
42
|
+
if @root.is_endpoint && handler
|
43
|
+
return [handler, params_out || {}]
|
64
44
|
else
|
65
45
|
return [nil, {}]
|
66
46
|
end
|
67
47
|
end
|
68
48
|
|
49
|
+
segments = split_path_cached(path)
|
50
|
+
current = @root
|
51
|
+
params = params_out || {}
|
52
|
+
params.clear if params_out
|
53
|
+
|
54
|
+
# Unrolled traversal for common case (1-3 segments)
|
55
|
+
case segments.size
|
56
|
+
when 1
|
57
|
+
next_node, _ = current.traverse_for(segments[0], 0, segments, params)
|
58
|
+
current = next_node
|
59
|
+
when 2
|
60
|
+
next_node, should_break = current.traverse_for(segments[0], 0, segments, params)
|
61
|
+
return [nil, {}] unless next_node
|
62
|
+
current = next_node
|
63
|
+
unless should_break
|
64
|
+
next_node, _ = current.traverse_for(segments[1], 1, segments, params)
|
65
|
+
current = next_node
|
66
|
+
end
|
67
|
+
when 3
|
68
|
+
next_node, should_break = current.traverse_for(segments[0], 0, segments, params)
|
69
|
+
return [nil, {}] unless next_node
|
70
|
+
current = next_node
|
71
|
+
unless should_break
|
72
|
+
next_node, should_break = current.traverse_for(segments[1], 1, segments, params)
|
73
|
+
return [nil, {}] unless next_node
|
74
|
+
current = next_node
|
75
|
+
unless should_break
|
76
|
+
next_node, _ = current.traverse_for(segments[2], 2, segments, params)
|
77
|
+
current = next_node
|
78
|
+
end
|
79
|
+
end
|
80
|
+
else
|
81
|
+
# General case for longer paths
|
82
|
+
segments.each_with_index do |text, idx|
|
83
|
+
next_node, should_break = current.traverse_for(text, idx, segments, params)
|
84
|
+
return [nil, {}] unless next_node
|
85
|
+
current = next_node
|
86
|
+
break if should_break
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
return [nil, {}] unless current
|
69
91
|
handler = current.get_handler(method)
|
70
92
|
return [nil, {}] unless current.is_endpoint && handler
|
71
93
|
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
unless constraints_match?(route.constraints, params)
|
76
|
-
return [nil, {}]
|
77
|
-
end
|
94
|
+
# Fast constraint check
|
95
|
+
if handler.respond_to?(:constraints) && !handler.constraints.empty?
|
96
|
+
return [nil, {}] unless constraints_match_fast(handler.constraints, params)
|
78
97
|
end
|
79
98
|
|
80
99
|
[handler, params]
|
@@ -82,34 +101,48 @@ module RubyRoutes
|
|
82
101
|
|
83
102
|
private
|
84
103
|
|
85
|
-
#
|
86
|
-
def
|
87
|
-
@
|
88
|
-
|
104
|
+
# Cached path splitting with optimized common cases
|
105
|
+
def split_path_cached(path)
|
106
|
+
return @empty_segments if path == '/' || path.empty?
|
107
|
+
|
89
108
|
if (cached = @split_cache[path])
|
90
109
|
return cached
|
91
110
|
end
|
92
111
|
|
93
|
-
|
94
|
-
p = p[1..-1] if p.start_with?('/')
|
95
|
-
p = p[0...-1] if p.end_with?('/')
|
96
|
-
segs = p.split('/')
|
112
|
+
result = split_path_raw(path)
|
97
113
|
|
98
|
-
# simple LRU
|
99
|
-
@split_cache[path] =
|
114
|
+
# Cache with simple LRU eviction
|
115
|
+
@split_cache[path] = result
|
100
116
|
@split_cache_order << path
|
101
117
|
if @split_cache_order.size > @split_cache_max
|
102
118
|
oldest = @split_cache_order.shift
|
103
119
|
@split_cache.delete(oldest)
|
104
120
|
end
|
105
121
|
|
106
|
-
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
# Raw path splitting without caching (for registration)
|
126
|
+
def split_path_raw(path)
|
127
|
+
return [] if path == '/' || path.empty?
|
128
|
+
|
129
|
+
# Optimized trimming: avoid string allocations when possible
|
130
|
+
start_idx = path.start_with?('/') ? 1 : 0
|
131
|
+
end_idx = path.end_with?('/') ? -2 : -1
|
132
|
+
|
133
|
+
if start_idx == 0 && end_idx == -1
|
134
|
+
path.split('/')
|
135
|
+
else
|
136
|
+
path[start_idx..end_idx].split('/')
|
137
|
+
end
|
107
138
|
end
|
108
139
|
|
109
|
-
#
|
110
|
-
def
|
140
|
+
# Optimized constraint matching with fast paths
|
141
|
+
def constraints_match_fast(constraints, params)
|
111
142
|
constraints.each do |param, constraint|
|
112
|
-
|
143
|
+
# Try both string and symbol keys (common pattern)
|
144
|
+
value = params[param.to_s]
|
145
|
+
value ||= params[param] if param.respond_to?(:to_s)
|
113
146
|
next unless value
|
114
147
|
|
115
148
|
case constraint
|
@@ -117,15 +150,16 @@ module RubyRoutes
|
|
117
150
|
return false unless constraint.match?(value)
|
118
151
|
when Proc
|
119
152
|
return false unless constraint.call(value)
|
153
|
+
when :int
|
154
|
+
# Fast integer check without regex
|
155
|
+
return false unless value.is_a?(String) && value.match?(/\A\d+\z/)
|
156
|
+
when :uuid
|
157
|
+
# Fast UUID check
|
158
|
+
return false unless value.is_a?(String) && value.length == 36 &&
|
159
|
+
value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
120
160
|
when Symbol
|
121
|
-
|
122
|
-
|
123
|
-
when :uuid then return false unless value.match?(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i)
|
124
|
-
else
|
125
|
-
# unknown symbol constraint — be conservative and allow
|
126
|
-
end
|
127
|
-
else
|
128
|
-
# unknown constraint type — allow (Route will validate later if needed)
|
161
|
+
# Handle other symbolic constraints
|
162
|
+
next # unknown symbol constraint — allow
|
129
163
|
end
|
130
164
|
end
|
131
165
|
true
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
class Route
|
3
|
+
# small LRU used for path generation cache
|
4
|
+
class SmallLru
|
5
|
+
attr_reader :hits, :misses, :evictions
|
6
|
+
|
7
|
+
# larger default to reduce eviction likelihood in benchmarks
|
8
|
+
def initialize(max_size = 1024)
|
9
|
+
@max_size = max_size
|
10
|
+
@h = {}
|
11
|
+
@hits = 0
|
12
|
+
@misses = 0
|
13
|
+
@evictions = 0
|
14
|
+
|
15
|
+
@hit_strategy = RubyRoutes::Constant::LRU_HIT_STRATEGY
|
16
|
+
@miss_strategy = RubyRoutes::Constant::LRU_MISS_STRATEGY
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(key)
|
20
|
+
strategy = @h.key?(key) ? @hit_strategy : @miss_strategy
|
21
|
+
strategy.call(self, key)
|
22
|
+
end
|
23
|
+
|
24
|
+
def set(key, val)
|
25
|
+
@h.delete(key) if @h.key?(key)
|
26
|
+
@h[key] = val
|
27
|
+
if @h.size > @max_size
|
28
|
+
@h.shift
|
29
|
+
@evictions += 1
|
30
|
+
end
|
31
|
+
val
|
32
|
+
end
|
33
|
+
|
34
|
+
def increment_hits
|
35
|
+
@hits += 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def increment_misses
|
39
|
+
@misses += 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/ruby_routes/route.rb
CHANGED
@@ -1,217 +1,223 @@
|
|
1
1
|
require 'uri'
|
2
|
+
require_relative 'route/small_lru'
|
2
3
|
|
3
4
|
module RubyRoutes
|
4
5
|
class Route
|
5
|
-
# small LRU used for path generation cache
|
6
|
-
class SmallLru
|
7
|
-
attr_reader :hits, :misses, :evictions
|
8
|
-
|
9
|
-
# larger default to reduce eviction likelihood in benchmarks
|
10
|
-
def initialize(max_size = 1024)
|
11
|
-
@max_size = max_size
|
12
|
-
@h = {}
|
13
|
-
@hits = 0
|
14
|
-
@misses = 0
|
15
|
-
@evictions = 0
|
16
|
-
end
|
17
|
-
|
18
|
-
def get(key)
|
19
|
-
if @h.key?(key)
|
20
|
-
@hits += 1
|
21
|
-
val = @h.delete(key)
|
22
|
-
@h[key] = val
|
23
|
-
val
|
24
|
-
else
|
25
|
-
@misses += 1
|
26
|
-
nil
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def set(key, val)
|
31
|
-
@h.delete(key) if @h.key?(key)
|
32
|
-
@h[key] = val
|
33
|
-
if @h.size > @max_size
|
34
|
-
@h.shift
|
35
|
-
@evictions += 1
|
36
|
-
end
|
37
|
-
val
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
6
|
attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
|
42
7
|
|
43
8
|
def initialize(path, options = {})
|
44
9
|
@path = normalize_path(path)
|
45
|
-
|
10
|
+
# Pre-normalize and freeze methods at creation time
|
11
|
+
raw_methods = Array(options[:via] || :get)
|
12
|
+
@methods = raw_methods.map { |m| normalize_method(m) }.freeze
|
13
|
+
@methods_set = @methods.to_set.freeze
|
46
14
|
@controller = extract_controller(options)
|
47
15
|
@action = options[:action] || extract_action(options[:to])
|
48
16
|
@name = options[:as]
|
49
17
|
@constraints = options[:constraints] || {}
|
50
|
-
#
|
51
|
-
@defaults = (options[:defaults] || {}).transform_keys(&:to_s)
|
18
|
+
# Pre-normalize defaults to string keys and freeze
|
19
|
+
@defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
|
52
20
|
|
21
|
+
# Pre-compile everything at initialization
|
22
|
+
precompile_route_data
|
53
23
|
validate_route!
|
54
24
|
end
|
55
25
|
|
56
26
|
def match?(request_method, request_path)
|
57
|
-
|
58
|
-
|
27
|
+
# Fast method check: use frozen Set for O(1) lookup
|
28
|
+
return false unless @methods_set.include?(request_method.to_s.upcase)
|
29
|
+
!!extract_path_params_fast(request_path)
|
59
30
|
end
|
60
31
|
|
61
|
-
def extract_params(request_path)
|
62
|
-
path_params =
|
63
|
-
return
|
32
|
+
def extract_params(request_path, parsed_qp = nil)
|
33
|
+
path_params = extract_path_params_fast(request_path)
|
34
|
+
return EMPTY_HASH unless path_params
|
64
35
|
|
65
|
-
|
66
|
-
|
67
|
-
# defaults already string keys, only merge keys that aren't present
|
68
|
-
defaults.each { |k, v| params[k] = v unless params.key?(k) }
|
69
|
-
|
70
|
-
validate_constraints!(params)
|
71
|
-
params
|
36
|
+
# Use optimized param building
|
37
|
+
build_params_hash(path_params, request_path, parsed_qp)
|
72
38
|
end
|
73
39
|
|
74
40
|
def named?
|
75
|
-
|
41
|
+
!@name.nil?
|
76
42
|
end
|
77
43
|
|
78
44
|
def resource?
|
79
|
-
|
45
|
+
@is_resource
|
80
46
|
end
|
81
47
|
|
82
48
|
def collection?
|
83
|
-
|
49
|
+
!@is_resource
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_query_params(path)
|
53
|
+
query_params_fast(path)
|
84
54
|
end
|
85
55
|
|
86
|
-
#
|
87
|
-
# Avoids unbounded cache growth and skips URI-encoding for safe values.
|
56
|
+
# Optimized path generation with better caching and fewer allocations
|
88
57
|
def generate_path(params = {})
|
89
|
-
return
|
90
|
-
|
91
|
-
#
|
92
|
-
|
93
|
-
|
94
|
-
params.each do |k, v|
|
95
|
-
ks = k.to_s
|
96
|
-
merged[ks] = v
|
58
|
+
return ROOT_PATH if @path == ROOT_PATH
|
59
|
+
|
60
|
+
# Fast path: empty params and no required params
|
61
|
+
if params.empty? && @required_params.empty?
|
62
|
+
return @static_path if @static_path
|
97
63
|
end
|
98
64
|
|
99
|
-
|
100
|
-
|
65
|
+
# Build merged params efficiently
|
66
|
+
merged = build_merged_params(params)
|
101
67
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
68
|
+
# Check required params (fast Set operation)
|
69
|
+
missing_params = @required_params_set - merged.keys
|
70
|
+
unless missing_params.empty?
|
71
|
+
raise RubyRoutes::RouteNotFound, "Missing params: #{missing_params.to_a.join(', ')}"
|
106
72
|
end
|
107
73
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
when :param
|
113
|
-
v = merged.fetch(seg[:name]).to_s
|
114
|
-
safe_encode_segment(v)
|
115
|
-
when :splat
|
116
|
-
v = merged.fetch(seg[:name], '')
|
117
|
-
arr = v.is_a?(Array) ? v : v.to_s.split('/')
|
118
|
-
arr.map { |p| safe_encode_segment(p.to_s) }.join('/')
|
119
|
-
end
|
74
|
+
# Cache lookup
|
75
|
+
cache_key = build_cache_key_fast(merged)
|
76
|
+
if (cached = @gen_cache.get(cache_key))
|
77
|
+
return cached
|
120
78
|
end
|
121
79
|
|
122
|
-
|
123
|
-
|
80
|
+
# Generate path using string buffer (avoid array allocations)
|
81
|
+
path_str = generate_path_string(merged)
|
82
|
+
@gen_cache.set(cache_key, path_str)
|
83
|
+
path_str
|
84
|
+
end
|
124
85
|
|
125
|
-
|
126
|
-
|
86
|
+
# Fast query params method (cached and optimized)
|
87
|
+
def query_params(request_path)
|
88
|
+
query_params_fast(request_path)
|
127
89
|
end
|
128
90
|
|
129
91
|
private
|
130
92
|
|
131
|
-
#
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
93
|
+
# Constants for performance
|
94
|
+
EMPTY_HASH = {}.freeze
|
95
|
+
ROOT_PATH = '/'.freeze
|
96
|
+
UNRESERVED_RE = /\A[a-zA-Z0-9\-._~]+\z/.freeze
|
97
|
+
QUERY_CACHE_SIZE = 128
|
98
|
+
|
99
|
+
# Fast method normalization
|
100
|
+
def normalize_method(method)
|
101
|
+
case method
|
102
|
+
when :get then 'GET'
|
103
|
+
when :post then 'POST'
|
104
|
+
when :put then 'PUT'
|
105
|
+
when :patch then 'PATCH'
|
106
|
+
when :delete then 'DELETE'
|
107
|
+
when :head then 'HEAD'
|
108
|
+
when :options then 'OPTIONS'
|
109
|
+
else method.to_s.upcase
|
110
|
+
end.freeze
|
148
111
|
end
|
149
112
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
113
|
+
# Pre-compile all route data at initialization
|
114
|
+
def precompile_route_data
|
115
|
+
@is_resource = @path.match?(/\/:id(?:$|\.)/)
|
116
|
+
@gen_cache = SmallLru.new(512) # larger cache
|
117
|
+
@query_cache = SmallLru.new(QUERY_CACHE_SIZE)
|
118
|
+
|
119
|
+
compile_segments
|
120
|
+
compile_required_params
|
121
|
+
check_static_path
|
154
122
|
end
|
155
123
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
124
|
+
def compile_segments
|
125
|
+
@compiled_segments = if @path == ROOT_PATH
|
126
|
+
EMPTY_ARRAY
|
127
|
+
else
|
128
|
+
@path.split('/').reject(&:empty?).map do |seg|
|
129
|
+
RubyRoutes::Constant.segment_descriptor(seg)
|
130
|
+
end.freeze
|
131
|
+
end
|
161
132
|
end
|
162
133
|
|
163
|
-
def
|
164
|
-
|
134
|
+
def compile_required_params
|
135
|
+
param_names = @compiled_segments.filter_map { |s| s[:name] if s[:type] != :static }
|
136
|
+
@param_names = param_names.freeze
|
137
|
+
@required_params = param_names.reject { |n| @defaults.key?(n) }.freeze
|
138
|
+
@required_params_set = @required_params.to_set.freeze
|
165
139
|
end
|
166
140
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
URI.encode_www_form_component(str)
|
141
|
+
def check_static_path
|
142
|
+
# Pre-generate static paths (no params)
|
143
|
+
if @required_params.empty?
|
144
|
+
@static_path = generate_static_path
|
145
|
+
end
|
173
146
|
end
|
174
147
|
|
175
|
-
def
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
148
|
+
def generate_static_path
|
149
|
+
return ROOT_PATH if @compiled_segments.empty?
|
150
|
+
|
151
|
+
parts = @compiled_segments.map { |seg| seg[:value] }
|
152
|
+
"/#{parts.join('/')}"
|
180
153
|
end
|
181
154
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
155
|
+
# Optimized param building
|
156
|
+
def build_params_hash(path_params, request_path, parsed_qp)
|
157
|
+
# Use pre-allocated hash when possible
|
158
|
+
result = get_thread_local_hash
|
159
|
+
|
160
|
+
# Path params first (highest priority)
|
161
|
+
result.update(path_params)
|
162
|
+
|
163
|
+
# Query params (if needed)
|
164
|
+
if parsed_qp
|
165
|
+
result.merge!(parsed_qp)
|
166
|
+
elsif request_path.include?('?')
|
167
|
+
qp = query_params_fast(request_path)
|
168
|
+
result.merge!(qp) unless qp.empty?
|
187
169
|
end
|
170
|
+
|
171
|
+
# Defaults (lowest priority)
|
172
|
+
merge_defaults_fast(result) unless @defaults.empty?
|
173
|
+
|
174
|
+
# Validate constraints efficiently
|
175
|
+
validate_constraints_fast!(result) unless @constraints.empty?
|
176
|
+
|
177
|
+
result.dup
|
188
178
|
end
|
189
179
|
|
190
|
-
def
|
191
|
-
|
192
|
-
|
180
|
+
def get_thread_local_hash
|
181
|
+
hash = Thread.current[:ruby_routes_params] ||= {}
|
182
|
+
hash.clear
|
183
|
+
hash
|
184
|
+
end
|
185
|
+
|
186
|
+
def merge_defaults_fast(result)
|
187
|
+
@defaults.each { |k, v| result[k] = v unless result.key?(k) }
|
193
188
|
end
|
194
189
|
|
195
|
-
|
196
|
-
|
197
|
-
return
|
190
|
+
# Fast path parameter extraction
|
191
|
+
def extract_path_params_fast(request_path)
|
192
|
+
return EMPTY_HASH if @compiled_segments.empty? && request_path == ROOT_PATH
|
193
|
+
return nil if @compiled_segments.empty?
|
194
|
+
|
195
|
+
# Fast path normalization
|
196
|
+
path_parts = split_path_fast(request_path)
|
197
|
+
return nil if @compiled_segments.size != path_parts.size
|
198
198
|
|
199
|
-
|
200
|
-
|
201
|
-
req = req[0...-1] if req.end_with?('/') && req != '/'
|
202
|
-
request_parts = req == '' ? [] : req.split('/')
|
199
|
+
extract_params_from_parts(path_parts)
|
200
|
+
end
|
203
201
|
|
204
|
-
|
202
|
+
def split_path_fast(request_path)
|
203
|
+
# Optimized path splitting
|
204
|
+
path = request_path
|
205
|
+
path = path[1..-1] if path.start_with?('/')
|
206
|
+
path = path[0...-1] if path.end_with?('/') && path != ROOT_PATH
|
207
|
+
path.empty? ? [] : path.split('/')
|
208
|
+
end
|
205
209
|
|
210
|
+
def extract_params_from_parts(path_parts)
|
206
211
|
params = {}
|
207
|
-
|
212
|
+
|
213
|
+
@compiled_segments.each_with_index do |seg, idx|
|
208
214
|
case seg[:type]
|
209
215
|
when :static
|
210
|
-
return nil unless seg[:value] ==
|
216
|
+
return nil unless seg[:value] == path_parts[idx]
|
211
217
|
when :param
|
212
|
-
params[seg[:name]] =
|
218
|
+
params[seg[:name]] = path_parts[idx]
|
213
219
|
when :splat
|
214
|
-
params[seg[:name]] =
|
220
|
+
params[seg[:name]] = path_parts[idx..-1].join('/')
|
215
221
|
break
|
216
222
|
end
|
217
223
|
end
|
@@ -219,15 +225,128 @@ module RubyRoutes
|
|
219
225
|
params
|
220
226
|
end
|
221
227
|
|
222
|
-
|
223
|
-
|
224
|
-
return
|
225
|
-
|
226
|
-
|
228
|
+
# Optimized merged params building
|
229
|
+
def build_merged_params(params)
|
230
|
+
return @defaults if params.empty?
|
231
|
+
|
232
|
+
merged = get_thread_local_merged_hash
|
233
|
+
merged.update(@defaults) unless @defaults.empty?
|
234
|
+
|
235
|
+
# Convert param keys to strings efficiently
|
236
|
+
params.each { |k, v| merged[k.to_s] = v }
|
237
|
+
merged
|
238
|
+
end
|
239
|
+
|
240
|
+
def get_thread_local_merged_hash
|
241
|
+
hash = Thread.current[:ruby_routes_merged] ||= {}
|
242
|
+
hash.clear
|
243
|
+
hash
|
244
|
+
end
|
245
|
+
|
246
|
+
# Fast cache key building with minimal allocations
|
247
|
+
def build_cache_key_fast(merged)
|
248
|
+
# Use instance variable buffer to avoid repeated allocations
|
249
|
+
@cache_key_buffer ||= String.new(capacity: 128)
|
250
|
+
@cache_key_buffer.clear
|
251
|
+
|
252
|
+
return @cache_key_buffer.dup if @required_params.empty?
|
253
|
+
|
254
|
+
@required_params.each_with_index do |name, idx|
|
255
|
+
@cache_key_buffer << '|' unless idx.zero?
|
256
|
+
value = merged[name]
|
257
|
+
@cache_key_buffer << (value.is_a?(Array) ? value.join('/') : value.to_s) if value
|
258
|
+
end
|
259
|
+
@cache_key_buffer.dup
|
260
|
+
end
|
261
|
+
|
262
|
+
# Optimized path generation
|
263
|
+
def generate_path_string(merged)
|
264
|
+
return ROOT_PATH if @compiled_segments.empty?
|
265
|
+
|
266
|
+
buffer = String.new(capacity: 128)
|
267
|
+
buffer << '/'
|
268
|
+
|
269
|
+
@compiled_segments.each_with_index do |seg, idx|
|
270
|
+
buffer << '/' unless idx.zero?
|
271
|
+
|
272
|
+
case seg[:type]
|
273
|
+
when :static
|
274
|
+
buffer << seg[:value]
|
275
|
+
when :param
|
276
|
+
value = merged.fetch(seg[:name]).to_s
|
277
|
+
buffer << encode_segment_fast(value)
|
278
|
+
when :splat
|
279
|
+
value = merged.fetch(seg[:name], '')
|
280
|
+
append_splat_value(buffer, value)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
buffer == '/' ? ROOT_PATH : buffer
|
285
|
+
end
|
286
|
+
|
287
|
+
def append_splat_value(buffer, value)
|
288
|
+
case value
|
289
|
+
when Array
|
290
|
+
value.each_with_index do |part, idx|
|
291
|
+
buffer << '/' unless idx.zero?
|
292
|
+
buffer << encode_segment_fast(part.to_s)
|
293
|
+
end
|
294
|
+
when String
|
295
|
+
parts = value.split('/')
|
296
|
+
parts.each_with_index do |part, idx|
|
297
|
+
buffer << '/' unless idx.zero?
|
298
|
+
buffer << encode_segment_fast(part)
|
299
|
+
end
|
300
|
+
else
|
301
|
+
buffer << encode_segment_fast(value.to_s)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Fast segment encoding
|
306
|
+
def encode_segment_fast(str)
|
307
|
+
return str if UNRESERVED_RE.match?(str)
|
308
|
+
URI.encode_www_form_component(str)
|
309
|
+
end
|
310
|
+
|
311
|
+
# Optimized query params with caching
|
312
|
+
def query_params_fast(path)
|
313
|
+
query_start = path.index('?')
|
314
|
+
return EMPTY_HASH unless query_start
|
315
|
+
|
316
|
+
query_string = path[(query_start + 1)..-1]
|
317
|
+
return EMPTY_HASH if query_string.empty?
|
318
|
+
|
319
|
+
# Cache query param parsing
|
320
|
+
if (cached = @query_cache.get(query_string))
|
321
|
+
return cached
|
322
|
+
end
|
323
|
+
|
324
|
+
result = Rack::Utils.parse_query(query_string)
|
325
|
+
@query_cache.set(query_string, result)
|
326
|
+
result
|
327
|
+
end
|
328
|
+
|
329
|
+
def normalize_path(path)
|
330
|
+
path_str = path.to_s
|
331
|
+
path_str = "/#{path_str}" unless path_str.start_with?('/')
|
332
|
+
path_str = path_str.chomp('/') unless path_str == ROOT_PATH
|
333
|
+
path_str
|
334
|
+
end
|
335
|
+
|
336
|
+
def extract_controller(options)
|
337
|
+
to = options[:to]
|
338
|
+
return options[:controller] unless to
|
339
|
+
to.to_s.split('#', 2).first
|
340
|
+
end
|
341
|
+
|
342
|
+
def extract_action(to)
|
343
|
+
return nil unless to
|
344
|
+
to.to_s.split('#', 2).last
|
227
345
|
end
|
228
346
|
|
229
|
-
|
230
|
-
|
347
|
+
# Optimized constraint validation
|
348
|
+
def validate_constraints_fast!(params)
|
349
|
+
@constraints.each do |param, constraint|
|
231
350
|
value = params[param.to_s]
|
232
351
|
next unless value
|
233
352
|
|
@@ -236,19 +355,23 @@ module RubyRoutes
|
|
236
355
|
raise ConstraintViolation unless constraint.match?(value)
|
237
356
|
when Proc
|
238
357
|
raise ConstraintViolation unless constraint.call(value)
|
239
|
-
when
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
358
|
+
when :int
|
359
|
+
raise ConstraintViolation unless value.match?(/\A\d+\z/)
|
360
|
+
when :uuid
|
361
|
+
raise ConstraintViolation unless value.length == 36 &&
|
362
|
+
value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
244
363
|
end
|
245
364
|
end
|
246
365
|
end
|
247
366
|
|
248
367
|
def validate_route!
|
249
|
-
raise InvalidRoute, "Controller is required" if controller.nil?
|
250
|
-
raise InvalidRoute, "Action is required" if action.nil?
|
251
|
-
raise InvalidRoute, "Invalid HTTP method: #{methods}" if methods.empty?
|
368
|
+
raise InvalidRoute, "Controller is required" if @controller.nil?
|
369
|
+
raise InvalidRoute, "Action is required" if @action.nil?
|
370
|
+
raise InvalidRoute, "Invalid HTTP method: #{@methods}" if @methods.empty?
|
252
371
|
end
|
372
|
+
|
373
|
+
# Additional constants
|
374
|
+
EMPTY_ARRAY = [].freeze
|
375
|
+
EMPTY_STRING = ''.freeze
|
253
376
|
end
|
254
|
-
end
|
377
|
+
end
|
@@ -5,44 +5,80 @@ module RubyRoutes
|
|
5
5
|
def initialize
|
6
6
|
@tree = RubyRoutes::RadixTree.new
|
7
7
|
@named_routes = {}
|
8
|
-
@routes = []
|
8
|
+
@routes = []
|
9
|
+
# Optimized recognition cache with better data structures
|
10
|
+
@recognition_cache = {}
|
11
|
+
@cache_hits = 0
|
12
|
+
@cache_misses = 0
|
13
|
+
@recognition_cache_max = 8192 # larger for better hit rates
|
9
14
|
end
|
10
15
|
|
11
16
|
def add_route(route)
|
12
17
|
@routes << route
|
13
18
|
@tree.add(route.path, route.methods, route)
|
14
19
|
@named_routes[route.name] = route if route.named?
|
20
|
+
# Clear recognition cache when routes change
|
21
|
+
@recognition_cache.clear if @recognition_cache.size > 100
|
15
22
|
route
|
16
23
|
end
|
17
24
|
|
18
25
|
def find_route(request_method, request_path)
|
19
|
-
#
|
20
|
-
|
26
|
+
# Optimized: avoid repeated string allocation
|
27
|
+
method_up = request_method.to_s.upcase
|
28
|
+
handler, _params = @tree.find(request_path, method_up)
|
21
29
|
handler
|
22
30
|
end
|
23
31
|
|
24
32
|
def find_named_route(name)
|
25
|
-
@named_routes[name]
|
33
|
+
route = @named_routes[name]
|
34
|
+
return route if route
|
35
|
+
raise RouteNotFound, "No route named '#{name}'"
|
26
36
|
end
|
27
37
|
|
28
38
|
def match(request_method, request_path)
|
29
|
-
#
|
30
|
-
|
39
|
+
# Fast path: normalize method once
|
40
|
+
method_up = method_lookup(request_method)
|
41
|
+
|
42
|
+
# Optimized cache key: avoid string interpolation when possible
|
43
|
+
cache_key = build_cache_key(method_up, request_path)
|
44
|
+
|
45
|
+
# Cache hit: return immediately
|
46
|
+
if (cached = @recognition_cache[cache_key])
|
47
|
+
@cache_hits += 1
|
48
|
+
cached_route, cached_params = cached
|
49
|
+
return {
|
50
|
+
route: cached_route,
|
51
|
+
params: cached_params,
|
52
|
+
controller: cached_route.controller,
|
53
|
+
action: cached_route.action
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
@cache_misses += 1
|
58
|
+
|
59
|
+
# Use thread-local params to avoid allocations
|
60
|
+
params = get_thread_local_params
|
61
|
+
handler, _ = @tree.find(request_path, method_up, params)
|
31
62
|
return nil unless handler
|
32
63
|
|
33
64
|
route = handler
|
34
65
|
|
35
|
-
#
|
36
|
-
params
|
37
|
-
params = route.defaults.transform_keys(&:to_s).merge(params)
|
38
|
-
params.merge!(route.send(:query_params, request_path))
|
66
|
+
# Fast path: merge defaults only if they exist
|
67
|
+
merge_defaults(route, params) if route.defaults && !route.defaults.empty?
|
39
68
|
|
40
|
-
#
|
41
|
-
|
69
|
+
# Fast path: parse query params only if needed
|
70
|
+
if request_path.include?('?')
|
71
|
+
merge_query_params(route, request_path, params)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Create return hash and cache entry
|
75
|
+
result_params = params.dup
|
76
|
+
cache_entry = [route, result_params.freeze]
|
77
|
+
insert_cache_entry(cache_key, cache_entry)
|
42
78
|
|
43
79
|
{
|
44
80
|
route: route,
|
45
|
-
params:
|
81
|
+
params: result_params,
|
46
82
|
controller: route.controller,
|
47
83
|
action: route.action
|
48
84
|
}
|
@@ -53,35 +89,119 @@ module RubyRoutes
|
|
53
89
|
end
|
54
90
|
|
55
91
|
def generate_path(name, params = {})
|
56
|
-
route =
|
57
|
-
|
92
|
+
route = @named_routes[name]
|
93
|
+
if route
|
94
|
+
route.generate_path(params)
|
95
|
+
else
|
96
|
+
raise RouteNotFound, "No route named '#{name}'"
|
97
|
+
end
|
58
98
|
end
|
59
99
|
|
60
100
|
def generate_path_from_route(route, params = {})
|
61
|
-
|
62
|
-
route.generate_path(params)
|
101
|
+
route.generate_path(params)
|
63
102
|
end
|
64
103
|
|
65
104
|
def clear!
|
66
105
|
@routes.clear
|
67
106
|
@named_routes.clear
|
107
|
+
@recognition_cache.clear
|
68
108
|
@tree = RadixTree.new
|
109
|
+
@cache_hits = @cache_misses = 0
|
69
110
|
end
|
70
111
|
|
71
112
|
def size
|
72
113
|
@routes.size
|
73
114
|
end
|
115
|
+
alias_method :length, :size
|
74
116
|
|
75
117
|
def empty?
|
76
118
|
@routes.empty?
|
77
119
|
end
|
78
120
|
|
79
121
|
def each(&block)
|
122
|
+
return enum_for(:each) unless block_given?
|
80
123
|
@routes.each(&block)
|
81
124
|
end
|
82
125
|
|
83
126
|
def include?(route)
|
84
127
|
@routes.include?(route)
|
85
128
|
end
|
129
|
+
|
130
|
+
# Performance monitoring
|
131
|
+
def cache_stats
|
132
|
+
total = @cache_hits + @cache_misses
|
133
|
+
hit_rate = total > 0 ? (@cache_hits.to_f / total * 100).round(2) : 0
|
134
|
+
{
|
135
|
+
hits: @cache_hits,
|
136
|
+
misses: @cache_misses,
|
137
|
+
hit_rate: "#{hit_rate}%",
|
138
|
+
size: @recognition_cache.size
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Method lookup table to avoid repeated upcasing
|
145
|
+
def method_lookup(method)
|
146
|
+
@method_cache ||= Hash.new { |h, k| h[k] = k.to_s.upcase.freeze }
|
147
|
+
@method_cache[method]
|
148
|
+
end
|
149
|
+
|
150
|
+
# Optimized cache key building - avoid string interpolation
|
151
|
+
def build_cache_key(method, path)
|
152
|
+
# Use frozen string concatenation to avoid allocations
|
153
|
+
@cache_key_buffer ||= String.new(capacity: 256)
|
154
|
+
@cache_key_buffer.clear
|
155
|
+
@cache_key_buffer << method << ':' << path
|
156
|
+
@cache_key_buffer.dup.freeze
|
157
|
+
end
|
158
|
+
|
159
|
+
# Get thread-local params hash, reusing when possible
|
160
|
+
def get_thread_local_params
|
161
|
+
# Use object pool to reduce GC pressure
|
162
|
+
@params_pool ||= []
|
163
|
+
if @params_pool.empty?
|
164
|
+
{}
|
165
|
+
else
|
166
|
+
hash = @params_pool.pop
|
167
|
+
hash.clear
|
168
|
+
hash
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def return_params_to_pool(params)
|
173
|
+
@params_pool ||= []
|
174
|
+
@params_pool.push(params) if @params_pool.size < 10
|
175
|
+
end
|
176
|
+
|
177
|
+
# Fast defaults merging
|
178
|
+
def merge_defaults(route, params)
|
179
|
+
route.defaults.each do |key, value|
|
180
|
+
params[key] = value unless params.key?(key)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Fast query params merging
|
185
|
+
def merge_query_params(route, request_path, params)
|
186
|
+
if route.respond_to?(:parse_query_params)
|
187
|
+
qp = route.parse_query_params(request_path)
|
188
|
+
params.merge!(qp) unless qp.empty?
|
189
|
+
elsif route.respond_to?(:query_params)
|
190
|
+
qp = route.query_params(request_path)
|
191
|
+
params.merge!(qp) unless qp.empty?
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Efficient cache insertion with LRU eviction
|
196
|
+
def insert_cache_entry(cache_key, cache_entry)
|
197
|
+
@recognition_cache[cache_key] = cache_entry
|
198
|
+
|
199
|
+
# Simple eviction: clear cache when it gets too large
|
200
|
+
if @recognition_cache.size > @recognition_cache_max
|
201
|
+
# Keep most recently used half
|
202
|
+
keys_to_delete = @recognition_cache.keys[0...(@recognition_cache_max / 2)]
|
203
|
+
keys_to_delete.each { |k| @recognition_cache.delete(k) }
|
204
|
+
end
|
205
|
+
end
|
86
206
|
end
|
87
207
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'segments/base_segment'
|
2
|
+
require_relative 'segments/dynamic_segment'
|
3
|
+
require_relative 'segments/static_segment'
|
4
|
+
require_relative 'segments/wildcard_segment'
|
5
|
+
require_relative 'constant'
|
6
|
+
|
7
|
+
module RubyRoutes
|
8
|
+
class Segment
|
9
|
+
def self.for(text)
|
10
|
+
t = text.to_s
|
11
|
+
key = t.empty? ? :default : t.getbyte(0)
|
12
|
+
segment = RubyRoutes::Constant::SEGMENTS[key] || RubyRoutes::Constant::SEGMENTS[:default]
|
13
|
+
segment.new(t)
|
14
|
+
end
|
15
|
+
|
16
|
+
def wildcard?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
module Segments
|
3
|
+
class BaseSegment
|
4
|
+
def initialize(text = nil)
|
5
|
+
@text = text.to_s if text
|
6
|
+
end
|
7
|
+
|
8
|
+
def wildcard?
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
def ensure_child(current)
|
13
|
+
raise NotImplementedError, "#{self.class}#ensure_child must be implemented"
|
14
|
+
end
|
15
|
+
|
16
|
+
def match(_node, _text, _idx, _segments, _params)
|
17
|
+
raise NotImplementedError, "#{self.class}#match must be implemented"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
module Segments
|
3
|
+
class DynamicSegment < BaseSegment
|
4
|
+
def initialize(text)
|
5
|
+
@name = text[1..-1]
|
6
|
+
end
|
7
|
+
|
8
|
+
def ensure_child(current)
|
9
|
+
current.dynamic_child ||= Node.new
|
10
|
+
current = current.dynamic_child
|
11
|
+
current.param_name = @name
|
12
|
+
current
|
13
|
+
end
|
14
|
+
|
15
|
+
def match(node, text, _idx, _segments, params)
|
16
|
+
return [nil, false] unless node.dynamic_child
|
17
|
+
nxt = node.dynamic_child
|
18
|
+
params[nxt.param_name.to_s] = text if params
|
19
|
+
[nxt, false]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
module Segments
|
3
|
+
class StaticSegment < BaseSegment
|
4
|
+
def initialize(text)
|
5
|
+
@text = text
|
6
|
+
end
|
7
|
+
|
8
|
+
def ensure_child(current)
|
9
|
+
current.static_children[@text] ||= Node.new
|
10
|
+
current.static_children[@text]
|
11
|
+
end
|
12
|
+
|
13
|
+
def match(node, text, _idx, _segments, _params)
|
14
|
+
[node.static_children[text], false]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
module Segments
|
3
|
+
class WildcardSegment < BaseSegment
|
4
|
+
def initialize(text)
|
5
|
+
@name = (text[1..-1] || 'splat')
|
6
|
+
end
|
7
|
+
|
8
|
+
def ensure_child(current)
|
9
|
+
current.wildcard_child ||= Node.new
|
10
|
+
current = current.wildcard_child
|
11
|
+
current.param_name = @name
|
12
|
+
current
|
13
|
+
end
|
14
|
+
|
15
|
+
def wildcard?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def match(node, _text, idx, segments, params)
|
20
|
+
return [nil, false] unless node.wildcard_child
|
21
|
+
nxt = node.wildcard_child
|
22
|
+
params[nxt.param_name.to_s] = segments[idx..-1].join('/') if params
|
23
|
+
[nxt, true]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/ruby_routes/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_routes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yosef Benny Widyokarsono
|
@@ -62,11 +62,20 @@ files:
|
|
62
62
|
- LICENSE
|
63
63
|
- README.md
|
64
64
|
- lib/ruby_routes.rb
|
65
|
+
- lib/ruby_routes/constant.rb
|
66
|
+
- lib/ruby_routes/lru_strategies/hit_strategy.rb
|
67
|
+
- lib/ruby_routes/lru_strategies/miss_strategy.rb
|
65
68
|
- lib/ruby_routes/node.rb
|
66
69
|
- lib/ruby_routes/radix_tree.rb
|
67
70
|
- lib/ruby_routes/route.rb
|
71
|
+
- lib/ruby_routes/route/small_lru.rb
|
68
72
|
- lib/ruby_routes/route_set.rb
|
69
73
|
- lib/ruby_routes/router.rb
|
74
|
+
- lib/ruby_routes/segment.rb
|
75
|
+
- lib/ruby_routes/segments/base_segment.rb
|
76
|
+
- lib/ruby_routes/segments/dynamic_segment.rb
|
77
|
+
- lib/ruby_routes/segments/static_segment.rb
|
78
|
+
- lib/ruby_routes/segments/wildcard_segment.rb
|
70
79
|
- lib/ruby_routes/string_extensions.rb
|
71
80
|
- lib/ruby_routes/url_helpers.rb
|
72
81
|
- lib/ruby_routes/version.rb
|