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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9f05718781115fc2cc1d8851ddea52d0af3fa46f7a937335398b518ea0116e9
4
- data.tar.gz: 12b51d5138dbbdec4b46d66f20489e167a721f67f58ed80652add4a2e9aa606b
3
+ metadata.gz: 02d39aae113654256dae0905438ac98a540854a5d131dea6c4b2b606b51bf4ba
4
+ data.tar.gz: ba01a85fc21713387e76c183a41e786106f1a053e2485d294895ac36027d94a5
5
5
  SHA512:
6
- metadata.gz: 32c7fab4ab209b6c2c1a5d50531d9f248f8413fb1409c6ebec9173eb538149c2a5f358e6bad85c0ed6993ed6b91a62a46f8d3551881ffaba4551b80df79c13a2
7
- data.tar.gz: dd96665fc956cf1a76444e75287c2661b3629c0d0cc5546f44ed464533a50b8ff830e354d1caa83782c53855446f6e0043039ae09489fef8b5f33eeaa73329c0
6
+ metadata.gz: c3b230ef35186d4f96714ba708b933cf7897166a74b7162949a3144aa21dc79aab2446a0b14d1e0504ea30f941146a7e0f1841d5908e6a6175d771a9b4a658ad
7
+ data.tar.gz: 9d476bdbaf46cef516cd51adb218097137863fb12976e98d87a7c8d7f77e3609421d81ba14ed77d67cd59054d4356690d088b619e8e089964cd4fdd6055225ac
data/README.md CHANGED
@@ -62,6 +62,7 @@ end
62
62
  ```
63
63
 
64
64
  This creates the following routes:
65
+
65
66
  - `GET /users` → `users#index`
66
67
  - `GET /users/new` → `users#new`
67
68
  - `POST /users` → `users#create`
@@ -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,13 @@
1
+ module RubyRoutes
2
+ module LruStrategies
3
+ class HitStrategy
4
+ def call(lru, key)
5
+ lru.increment_hits
6
+ h = lru.instance_variable_get(:@h)
7
+ val = h.delete(key)
8
+ h[key] = val
9
+ val
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ module RubyRoutes
2
+ module LruStrategies
3
+ class MissStrategy
4
+ def call(lru, _key)
5
+ lru.increment_misses
6
+ nil
7
+ end
8
+ end
9
+ end
10
+ 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
@@ -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
- @defaults = options[:defaults] || {}
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
- params = path_params.dup
29
- # Convert symbol keys to string keys for consistency
30
- string_defaults = defaults.transform_keys(&:to_s)
31
- params.merge!(string_defaults)
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
- path = "/#{path}" unless path.start_with?('/')
51
- # Remove trailing slash unless it's the root path
52
- path = path.chomp('/') unless path == '/'
53
- path
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
- route_parts = path.split('/')
71
- request_parts = request_path.split('/')
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 route_parts.length != request_parts.length
192
+ return nil if segs.size != request_parts.size
74
193
 
75
194
  params = {}
76
- route_parts.each_with_index do |route_part, index|
77
- request_part = request_parts[index]
78
-
79
- if route_part.start_with?(':')
80
- param_name = route_part[1..-1]
81
- params[param_name] = request_part
82
- elsif route_part != request_part
83
- return nil
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
- @routes = []
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
- @routes.find { |route| route.match?(request_method, request_path) }
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
- route = find_route(request_method, request_path)
26
- return nil unless route
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: route.extract_params(request_path),
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
- path = route.path.dup
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
@@ -1,3 +1,3 @@
1
1
  module RubyRoutes
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0.0"
3
3
  end
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: 0.1.0
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