ruby_routes 0.1.0 → 1.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/README.md +1 -0
- 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 +56 -0
- data/lib/ruby_routes/radix_tree.rb +119 -0
- data/lib/ruby_routes/route/small_lru.rb +43 -0
- data/lib/ruby_routes/route.rb +171 -25
- data/lib/ruby_routes/route_set.rb +47 -16
- 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
- data/lib/ruby_routes.rb +2 -0
- metadata +12 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02d39aae113654256dae0905438ac98a540854a5d131dea6c4b2b606b51bf4ba
|
4
|
+
data.tar.gz: ba01a85fc21713387e76c183a41e786106f1a053e2485d294895ac36027d94a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3b230ef35186d4f96714ba708b933cf7897166a74b7162949a3144aa21dc79aab2446a0b14d1e0504ea30f941146a7e0f1841d5908e6a6175d771a9b4a658ad
|
7
|
+
data.tar.gz: 9d476bdbaf46cef516cd51adb218097137863fb12976e98d87a7c8d7f77e3609421d81ba14ed77d67cd59054d4356690d088b619e8e089964cd4fdd6055225ac
|
data/README.md
CHANGED
@@ -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
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require_relative 'segment'
|
2
|
+
|
3
|
+
module RubyRoutes
|
4
|
+
class Node
|
5
|
+
attr_accessor :static_children, :dynamic_child, :wildcard_child,
|
6
|
+
:handlers, :param_name, :is_endpoint
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@static_children = {}
|
10
|
+
@dynamic_child = nil
|
11
|
+
@wildcard_child = nil
|
12
|
+
@handlers = {}
|
13
|
+
@param_name = nil
|
14
|
+
@is_endpoint = false
|
15
|
+
end
|
16
|
+
|
17
|
+
# Traverse for a single segment using the matcher registry.
|
18
|
+
# Returns [next_node_or_nil, should_break_bool] or [nil, false] if no match.
|
19
|
+
def traverse_for(segment, index, segments, params)
|
20
|
+
# Prefer static children first (exact match).
|
21
|
+
if @static_children.key?(segment)
|
22
|
+
return [@static_children[segment], false]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Then dynamic param child (single segment)
|
26
|
+
if @dynamic_child
|
27
|
+
next_node = @dynamic_child
|
28
|
+
if params
|
29
|
+
params[next_node.param_name.to_s] = segment
|
30
|
+
end
|
31
|
+
return [next_node, false]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Then wildcard child (consume remainder)
|
35
|
+
if @wildcard_child
|
36
|
+
next_node = @wildcard_child
|
37
|
+
if params
|
38
|
+
params[next_node.param_name.to_s] = segments[index..-1].join('/')
|
39
|
+
end
|
40
|
+
return [next_node, true]
|
41
|
+
end
|
42
|
+
|
43
|
+
# No match at this node
|
44
|
+
[nil, false]
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_handler(method, handler)
|
48
|
+
@handlers[method.to_s] = handler
|
49
|
+
@is_endpoint = true
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_handler(method)
|
53
|
+
@handlers[method.to_s]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require_relative 'segment'
|
2
|
+
|
3
|
+
module RubyRoutes
|
4
|
+
class RadixTree
|
5
|
+
class << self
|
6
|
+
# Allow RadixTree.new(path, options...) to act as a convenience factory
|
7
|
+
# returning a Route (this matches test usage where specs call
|
8
|
+
# RadixTree.new('/path', to: 'controller#action')).
|
9
|
+
# Calling RadixTree.new with no arguments returns an actual RadixTree instance.
|
10
|
+
def new(*args, &block)
|
11
|
+
if args.any?
|
12
|
+
# Delegate to Route initializer when args are provided
|
13
|
+
RubyRoutes::Route.new(*args, &block)
|
14
|
+
else
|
15
|
+
super()
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@root = Node.new
|
22
|
+
@_split_cache = {} # simple LRU: key -> [value, age]
|
23
|
+
@split_cache_order = [] # track order for eviction
|
24
|
+
@split_cache_max = 1024
|
25
|
+
end
|
26
|
+
|
27
|
+
def add(path, methods, handler)
|
28
|
+
current = @root
|
29
|
+
parse_segments(path).each do |seg|
|
30
|
+
current = seg.ensure_child(current)
|
31
|
+
break if seg.wildcard?
|
32
|
+
end
|
33
|
+
|
34
|
+
methods.each { |method| current.add_handler(method, handler) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_segments(path)
|
38
|
+
split_path(path).map { |s| RubyRoutes::Segment.for(s) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def find(path, method, params_out = nil)
|
42
|
+
segments = split_path(path)
|
43
|
+
current = @root
|
44
|
+
params = params_out || {}
|
45
|
+
params.clear if params_out
|
46
|
+
|
47
|
+
segments.each_with_index do |text, idx|
|
48
|
+
next_node, should_break = current.traverse_for(text, idx, segments, params)
|
49
|
+
return [nil, {}] unless next_node
|
50
|
+
current = next_node
|
51
|
+
break if should_break
|
52
|
+
end
|
53
|
+
|
54
|
+
handler = current.get_handler(method)
|
55
|
+
return [nil, {}] unless current.is_endpoint && handler
|
56
|
+
|
57
|
+
# lightweight constraint checks: reject early if route constraints don't match
|
58
|
+
route = handler
|
59
|
+
if route.respond_to?(:constraints) && route.constraints.any?
|
60
|
+
unless constraints_match?(route.constraints, params)
|
61
|
+
return [nil, {}]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
[handler, params]
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# faster, lower-allocation trim + split
|
71
|
+
def split_path(path)
|
72
|
+
@split_cache ||= {}
|
73
|
+
return [''] if path == '/'
|
74
|
+
if (cached = @split_cache[path])
|
75
|
+
return cached
|
76
|
+
end
|
77
|
+
|
78
|
+
p = path
|
79
|
+
p = p[1..-1] if p.start_with?('/')
|
80
|
+
p = p[0...-1] if p.end_with?('/')
|
81
|
+
segs = p.split('/')
|
82
|
+
|
83
|
+
# simple LRU insert
|
84
|
+
@split_cache[path] = segs
|
85
|
+
@split_cache_order << path
|
86
|
+
if @split_cache_order.size > @split_cache_max
|
87
|
+
oldest = @split_cache_order.shift
|
88
|
+
@split_cache.delete(oldest)
|
89
|
+
end
|
90
|
+
|
91
|
+
segs
|
92
|
+
end
|
93
|
+
|
94
|
+
# constraints match helper (non-raising, lightweight)
|
95
|
+
def constraints_match?(constraints, params)
|
96
|
+
constraints.each do |param, constraint|
|
97
|
+
value = params[param.to_s] || params[param]
|
98
|
+
next unless value
|
99
|
+
|
100
|
+
case constraint
|
101
|
+
when Regexp
|
102
|
+
return false unless constraint.match?(value)
|
103
|
+
when Proc
|
104
|
+
return false unless constraint.call(value)
|
105
|
+
when Symbol
|
106
|
+
case constraint
|
107
|
+
when :int then return false unless value.match?(/^\d+$/)
|
108
|
+
when :uuid then return false unless value.match?(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i)
|
109
|
+
else
|
110
|
+
# unknown symbol constraint — be conservative and allow
|
111
|
+
end
|
112
|
+
else
|
113
|
+
# unknown constraint type — allow (Route will validate later if needed)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
true
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -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,3 +1,6 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require_relative 'route/small_lru'
|
3
|
+
|
1
4
|
module RubyRoutes
|
2
5
|
class Route
|
3
6
|
attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
|
@@ -9,27 +12,40 @@ module RubyRoutes
|
|
9
12
|
@action = options[:action] || extract_action(options[:to])
|
10
13
|
@name = options[:as]
|
11
14
|
@constraints = options[:constraints] || {}
|
12
|
-
|
15
|
+
# pre-normalize defaults to string keys to avoid per-request transform_keys
|
16
|
+
@defaults = (options[:defaults] || {}).transform_keys(&:to_s)
|
13
17
|
|
14
18
|
validate_route!
|
15
19
|
end
|
16
20
|
|
17
21
|
def match?(request_method, request_path)
|
18
22
|
return false unless methods.include?(request_method.to_s.upcase)
|
19
|
-
|
20
|
-
path_params = extract_path_params(request_path)
|
21
|
-
path_params != nil
|
23
|
+
!!extract_path_params(request_path)
|
22
24
|
end
|
23
25
|
|
24
|
-
def extract_params(request_path)
|
26
|
+
def extract_params(request_path, parsed_qp = nil)
|
25
27
|
path_params = extract_path_params(request_path)
|
26
28
|
return {} unless path_params
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
params
|
30
|
+
# Reuse a thread-local hash to reduce allocations; return a dup to callers.
|
31
|
+
tmp = Thread.current[:ruby_routes_params] ||= {}
|
32
|
+
tmp.clear
|
33
|
+
|
34
|
+
# start with path params (they take precedence)
|
35
|
+
path_params.each { |k, v| tmp[k] = v }
|
36
|
+
|
37
|
+
# use provided parsed_qp if available, otherwise parse lazily only if needed
|
38
|
+
qp = parsed_qp
|
39
|
+
if qp.nil? && request_path.include?('?')
|
40
|
+
qp = query_params(request_path)
|
41
|
+
end
|
42
|
+
qp.each { |k, v| tmp[k] = v } if qp && !qp.empty?
|
43
|
+
|
44
|
+
# only set defaults for keys not already present
|
45
|
+
defaults.each { |k, v| tmp[k] = v unless tmp.key?(k) } if defaults
|
46
|
+
|
47
|
+
validate_constraints!(tmp)
|
48
|
+
tmp.dup
|
33
49
|
end
|
34
50
|
|
35
51
|
def named?
|
@@ -44,13 +60,111 @@ module RubyRoutes
|
|
44
60
|
!resource?
|
45
61
|
end
|
46
62
|
|
63
|
+
def parse_query_params(path)
|
64
|
+
query_params(path)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Fast path generator: uses precompiled token list and a small LRU.
|
68
|
+
# Avoids unbounded cache growth and skips URI-encoding for safe values.
|
69
|
+
def generate_path(params = {})
|
70
|
+
return '/' if path == '/'
|
71
|
+
|
72
|
+
# build merged for only relevant param names, reusing a thread-local hash
|
73
|
+
tmp = Thread.current[:ruby_routes_merged] ||= {}
|
74
|
+
tmp.clear
|
75
|
+
defaults.each { |k, v| tmp[k] = v } if defaults
|
76
|
+
params.each { |k, v| tmp[k.to_s] = v } if params
|
77
|
+
merged = tmp
|
78
|
+
|
79
|
+
missing = compiled_required_params - merged.keys
|
80
|
+
raise RubyRoutes::RouteNotFound, "Missing params: #{missing.join(', ')}" unless missing.empty?
|
81
|
+
|
82
|
+
@gen_cache ||= SmallLru.new(256)
|
83
|
+
cache_key = cache_key_for(merged)
|
84
|
+
if (cached = @gen_cache.get(cache_key))
|
85
|
+
return cached
|
86
|
+
end
|
87
|
+
|
88
|
+
parts = compiled_segments.map do |seg|
|
89
|
+
case seg[:type]
|
90
|
+
when :static
|
91
|
+
seg[:value]
|
92
|
+
when :param
|
93
|
+
v = merged.fetch(seg[:name]).to_s
|
94
|
+
safe_encode_segment(v)
|
95
|
+
when :splat
|
96
|
+
v = merged.fetch(seg[:name], '')
|
97
|
+
arr = v.is_a?(Array) ? v : v.to_s.split('/')
|
98
|
+
arr.map { |p| safe_encode_segment(p.to_s) }.join('/')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
out = '/' + parts.join('/')
|
103
|
+
out = '/' if out == ''
|
104
|
+
|
105
|
+
@gen_cache.set(cache_key, out)
|
106
|
+
out
|
107
|
+
end
|
108
|
+
|
47
109
|
private
|
48
110
|
|
111
|
+
# compile helpers (memoize)
|
112
|
+
def compiled_segments
|
113
|
+
@compiled_segments ||= begin
|
114
|
+
if path == '/'
|
115
|
+
[]
|
116
|
+
else
|
117
|
+
path.split('/').reject(&:empty?).map do |seg|
|
118
|
+
RubyRoutes::Constant.segment_descriptor(seg)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def compiled_required_params
|
125
|
+
@compiled_required_params ||= compiled_segments.select { |s| s[:type] != :static }
|
126
|
+
.map { |s| s[:name] }.uniq
|
127
|
+
.reject { |n| defaults.to_s.include?(n) }
|
128
|
+
end
|
129
|
+
|
130
|
+
# Cache key: deterministic param-order key (fast, stable)
|
131
|
+
def cache_key_for(merged)
|
132
|
+
# build key in route token order (parameters & splat) to avoid sorting/inspect
|
133
|
+
names = compiled_param_names
|
134
|
+
# build with single string buffer to avoid temporary arrays
|
135
|
+
buf = +""
|
136
|
+
names.each_with_index do |n, i|
|
137
|
+
val = merged[n]
|
138
|
+
part = if val.nil?
|
139
|
+
''
|
140
|
+
elsif val.is_a?(Array)
|
141
|
+
val.map!(&:to_s) && val.join('/')
|
142
|
+
else
|
143
|
+
val.to_s
|
144
|
+
end
|
145
|
+
buf << '|' unless i.zero?
|
146
|
+
buf << part
|
147
|
+
end
|
148
|
+
buf
|
149
|
+
end
|
150
|
+
|
151
|
+
def compiled_param_names
|
152
|
+
@compiled_param_names ||= compiled_segments.map { |s| s[:name] if s[:type] != :static }.compact
|
153
|
+
end
|
154
|
+
|
155
|
+
# Only URI-encode a segment when it contains unsafe chars.
|
156
|
+
UNRESERVED_RE = /\A[a-zA-Z0-9\-._~]+\z/
|
157
|
+
def safe_encode_segment(str)
|
158
|
+
# leave slash handling to splat logic (splats already split)
|
159
|
+
return str if UNRESERVED_RE.match?(str)
|
160
|
+
URI.encode_www_form_component(str)
|
161
|
+
end
|
162
|
+
|
49
163
|
def normalize_path(path)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
164
|
+
p = path.to_s
|
165
|
+
p = "/#{p}" unless p.start_with?('/')
|
166
|
+
p = p.chomp('/') unless p == '/'
|
167
|
+
p
|
54
168
|
end
|
55
169
|
|
56
170
|
def extract_controller(options)
|
@@ -67,26 +181,58 @@ module RubyRoutes
|
|
67
181
|
end
|
68
182
|
|
69
183
|
def extract_path_params(request_path)
|
70
|
-
|
71
|
-
|
184
|
+
segs = compiled_segments # memoized compiled route tokens
|
185
|
+
return nil if segs.empty? && request_path != '/'
|
186
|
+
|
187
|
+
req = request_path
|
188
|
+
req = req[1..-1] if req.start_with?('/')
|
189
|
+
req = req[0...-1] if req.end_with?('/') && req != '/'
|
190
|
+
request_parts = req == '' ? [] : req.split('/')
|
72
191
|
|
73
|
-
return nil if
|
192
|
+
return nil if segs.size != request_parts.size
|
74
193
|
|
75
194
|
params = {}
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
params[
|
82
|
-
|
83
|
-
|
195
|
+
segs.each_with_index do |seg, idx|
|
196
|
+
case seg[:type]
|
197
|
+
when :static
|
198
|
+
return nil unless seg[:value] == request_parts[idx]
|
199
|
+
when :param
|
200
|
+
params[seg[:name]] = request_parts[idx]
|
201
|
+
when :splat
|
202
|
+
params[seg[:name]] = request_parts[idx..-1].join('/')
|
203
|
+
break
|
84
204
|
end
|
85
205
|
end
|
86
206
|
|
87
207
|
params
|
88
208
|
end
|
89
209
|
|
210
|
+
def query_params(path)
|
211
|
+
qidx = path.index('?')
|
212
|
+
return {} unless qidx
|
213
|
+
qs = path[(qidx + 1)..-1] || ''
|
214
|
+
Rack::Utils.parse_query(qs)
|
215
|
+
end
|
216
|
+
|
217
|
+
def validate_constraints!(params)
|
218
|
+
constraints.each do |param, constraint|
|
219
|
+
value = params[param.to_s]
|
220
|
+
next unless value
|
221
|
+
|
222
|
+
case constraint
|
223
|
+
when Regexp
|
224
|
+
raise ConstraintViolation unless constraint.match?(value)
|
225
|
+
when Proc
|
226
|
+
raise ConstraintViolation unless constraint.call(value)
|
227
|
+
when Symbol
|
228
|
+
case constraint
|
229
|
+
when :int then raise ConstraintViolation unless value.match?(/^\d+$/)
|
230
|
+
when :uuid then raise ConstraintViolation unless value.match?(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
90
236
|
def validate_route!
|
91
237
|
raise InvalidRoute, "Controller is required" if controller.nil?
|
92
238
|
raise InvalidRoute, "Action is required" if action.nil?
|
@@ -3,18 +3,25 @@ module RubyRoutes
|
|
3
3
|
attr_reader :routes
|
4
4
|
|
5
5
|
def initialize
|
6
|
-
@
|
6
|
+
@tree = RubyRoutes::RadixTree.new
|
7
7
|
@named_routes = {}
|
8
|
+
@routes = [] # keep list for specs / iteration / size
|
9
|
+
@recognition_cache = {} # simple bounded cache: key -> [route, params]
|
10
|
+
@recognition_cache_order = []
|
11
|
+
@recognition_cache_max = 4096
|
8
12
|
end
|
9
13
|
|
10
14
|
def add_route(route)
|
11
15
|
@routes << route
|
16
|
+
@tree.add(route.path, route.methods, route)
|
12
17
|
@named_routes[route.name] = route if route.named?
|
13
18
|
route
|
14
19
|
end
|
15
20
|
|
16
21
|
def find_route(request_method, request_path)
|
17
|
-
|
22
|
+
# Return the Route object (or nil) to match spec expectations.
|
23
|
+
handler, _params = @tree.find(request_path, request_method.to_s.upcase)
|
24
|
+
handler
|
18
25
|
end
|
19
26
|
|
20
27
|
def find_named_route(name)
|
@@ -22,12 +29,44 @@ module RubyRoutes
|
|
22
29
|
end
|
23
30
|
|
24
31
|
def match(request_method, request_path)
|
25
|
-
|
26
|
-
|
32
|
+
# Normalize method once and attempt recognition cache hit
|
33
|
+
method_up = request_method.to_s.upcase
|
34
|
+
cache_key = "#{method_up}:#{request_path}"
|
35
|
+
if (cached = @recognition_cache[cache_key])
|
36
|
+
# Return cached params (frozen) directly to avoid heavy dup allocations.
|
37
|
+
cached_route, cached_params = cached
|
38
|
+
return { route: cached_route, params: cached_params, controller: cached_route.controller, action: cached_route.action }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Use a thread-local hash as output for RadixTree to avoid allocating a params Hash
|
42
|
+
tmp = Thread.current[:ruby_routes_params] ||= {}
|
43
|
+
handler, _ = @tree.find(request_path, method_up, tmp)
|
44
|
+
return nil unless handler
|
45
|
+
route = handler
|
46
|
+
|
47
|
+
# tmp now contains path params (filled by RadixTree). Merge defaults and query params in-place.
|
48
|
+
# defaults first (only set missing keys)
|
49
|
+
if route.defaults
|
50
|
+
route.defaults.each { |k, v| tmp[k] = v unless tmp.key?(k) }
|
51
|
+
end
|
52
|
+
if request_path.include?('?')
|
53
|
+
qp = route.parse_query_params(request_path)
|
54
|
+
qp.each { |k, v| tmp[k] = v } unless qp.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
params = tmp.dup
|
58
|
+
|
59
|
+
# insert into bounded recognition cache (store frozen params to reduce accidental mutation)
|
60
|
+
@recognition_cache[cache_key] = [route, params.freeze]
|
61
|
+
@recognition_cache_order << cache_key
|
62
|
+
if @recognition_cache_order.size > @recognition_cache_max
|
63
|
+
oldest = @recognition_cache_order.shift
|
64
|
+
@recognition_cache.delete(oldest)
|
65
|
+
end
|
27
66
|
|
28
67
|
{
|
29
68
|
route: route,
|
30
|
-
params:
|
69
|
+
params: params,
|
31
70
|
controller: route.controller,
|
32
71
|
action: route.action
|
33
72
|
}
|
@@ -43,22 +82,14 @@ module RubyRoutes
|
|
43
82
|
end
|
44
83
|
|
45
84
|
def generate_path_from_route(route, params = {})
|
46
|
-
|
47
|
-
|
48
|
-
params.each do |key, value|
|
49
|
-
path.gsub!(":#{key}", value.to_s)
|
50
|
-
end
|
51
|
-
|
52
|
-
# Remove any remaining :param placeholders
|
53
|
-
path.gsub!(/\/:[^\/]+/, '')
|
54
|
-
path.gsub!(/\/$/, '') if path != '/'
|
55
|
-
|
56
|
-
path
|
85
|
+
# Delegate to Route#generate_path which uses precompiled segments + cache
|
86
|
+
route.generate_path(params)
|
57
87
|
end
|
58
88
|
|
59
89
|
def clear!
|
60
90
|
@routes.clear
|
61
91
|
@named_routes.clear
|
92
|
+
@tree = RadixTree.new
|
62
93
|
end
|
63
94
|
|
64
95
|
def size
|
@@ -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
data/lib/ruby_routes.rb
CHANGED
@@ -4,6 +4,8 @@ require_relative "ruby_routes/route"
|
|
4
4
|
require_relative "ruby_routes/route_set"
|
5
5
|
require_relative "ruby_routes/url_helpers"
|
6
6
|
require_relative "ruby_routes/router"
|
7
|
+
require_relative "ruby_routes/radix_tree"
|
8
|
+
require_relative "ruby_routes/node"
|
7
9
|
|
8
10
|
module RubyRoutes
|
9
11
|
class Error < StandardError; end
|
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.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yosef Benny Widyokarsono
|
@@ -62,9 +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
|
68
|
+
- lib/ruby_routes/node.rb
|
69
|
+
- lib/ruby_routes/radix_tree.rb
|
65
70
|
- lib/ruby_routes/route.rb
|
71
|
+
- lib/ruby_routes/route/small_lru.rb
|
66
72
|
- lib/ruby_routes/route_set.rb
|
67
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
|
68
79
|
- lib/ruby_routes/string_extensions.rb
|
69
80
|
- lib/ruby_routes/url_helpers.rb
|
70
81
|
- lib/ruby_routes/version.rb
|