ruby_routes 2.1.0 → 2.3.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 +232 -162
- data/lib/ruby_routes/constant.rb +137 -18
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +31 -4
- data/lib/ruby_routes/lru_strategies/miss_strategy.rb +21 -0
- data/lib/ruby_routes/node.rb +82 -41
- data/lib/ruby_routes/radix_tree/finder.rb +164 -0
- data/lib/ruby_routes/radix_tree/inserter.rb +98 -0
- data/lib/ruby_routes/radix_tree.rb +83 -142
- data/lib/ruby_routes/route/check_helpers.rb +109 -0
- data/lib/ruby_routes/route/constraint_validator.rb +159 -0
- data/lib/ruby_routes/route/param_support.rb +202 -0
- data/lib/ruby_routes/route/path_builder.rb +86 -0
- data/lib/ruby_routes/route/path_generation.rb +102 -0
- data/lib/ruby_routes/route/query_helpers.rb +56 -0
- data/lib/ruby_routes/route/segment_compiler.rb +163 -0
- data/lib/ruby_routes/route/small_lru.rb +96 -17
- data/lib/ruby_routes/route/validation_helpers.rb +151 -0
- data/lib/ruby_routes/route/warning_helpers.rb +54 -0
- data/lib/ruby_routes/route.rb +121 -451
- data/lib/ruby_routes/route_set/cache_helpers.rb +174 -0
- data/lib/ruby_routes/route_set/collection_helpers.rb +127 -0
- data/lib/ruby_routes/route_set.rb +126 -148
- data/lib/ruby_routes/router/build_helpers.rb +100 -0
- data/lib/ruby_routes/router/builder.rb +96 -0
- data/lib/ruby_routes/router/http_helpers.rb +135 -0
- data/lib/ruby_routes/router/resource_helpers.rb +137 -0
- data/lib/ruby_routes/router/scope_helpers.rb +109 -0
- data/lib/ruby_routes/router.rb +196 -179
- data/lib/ruby_routes/segment.rb +28 -8
- data/lib/ruby_routes/segments/base_segment.rb +40 -4
- data/lib/ruby_routes/segments/dynamic_segment.rb +48 -12
- data/lib/ruby_routes/segments/static_segment.rb +43 -7
- data/lib/ruby_routes/segments/wildcard_segment.rb +56 -12
- data/lib/ruby_routes/string_extensions.rb +52 -15
- data/lib/ruby_routes/url_helpers.rb +106 -24
- data/lib/ruby_routes/utility/inflector_utility.rb +35 -0
- data/lib/ruby_routes/utility/key_builder_utility.rb +179 -0
- data/lib/ruby_routes/utility/method_utility.rb +137 -0
- data/lib/ruby_routes/utility/path_utility.rb +89 -0
- data/lib/ruby_routes/utility/route_utility.rb +49 -0
- data/lib/ruby_routes/version.rb +3 -1
- data/lib/ruby_routes.rb +68 -11
- metadata +30 -7
data/lib/ruby_routes/route.rb
CHANGED
@@ -1,505 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'uri'
|
2
4
|
require 'timeout'
|
3
|
-
require 'set'
|
4
5
|
require 'rack'
|
6
|
+
require_relative 'constant'
|
7
|
+
require_relative 'node'
|
5
8
|
require_relative 'route/small_lru'
|
9
|
+
require_relative 'utility/key_builder_utility'
|
10
|
+
require_relative 'utility/method_utility'
|
11
|
+
require_relative 'utility/path_utility'
|
12
|
+
require_relative 'utility/route_utility'
|
13
|
+
require_relative 'route/param_support'
|
14
|
+
require_relative 'route/segment_compiler'
|
15
|
+
require_relative 'route/path_builder'
|
16
|
+
require_relative 'route/constraint_validator'
|
17
|
+
require_relative 'route/check_helpers'
|
18
|
+
require_relative 'route/query_helpers'
|
19
|
+
require_relative 'route/validation_helpers'
|
20
|
+
require_relative 'route/path_generation'
|
6
21
|
|
7
22
|
module RubyRoutes
|
23
|
+
# Route
|
24
|
+
#
|
25
|
+
# Immutable-ish representation of a single HTTP route plus optimized
|
26
|
+
# helpers for:
|
27
|
+
# - Path recognition (segment compilation + fast param extraction)
|
28
|
+
# - Path generation (low‑allocation caching + param merging)
|
29
|
+
# - Constraint validation (regexp / typed / hash rules)
|
30
|
+
#
|
31
|
+
# Performance Techniques:
|
32
|
+
# - Precompiled segment descriptors (static / param / splat)
|
33
|
+
# - Small LRU caches (path generation + query parsing)
|
34
|
+
# - Thread‑local reusable Hashes / String buffers
|
35
|
+
# - Minimal object allocation in hot paths
|
36
|
+
#
|
37
|
+
# Thread Safety:
|
38
|
+
# - Instance is effectively read‑only after initialization aside from
|
39
|
+
# internal caches which are not synchronized but safe for typical
|
40
|
+
# single writer (boot) + many reader (request) usage.
|
41
|
+
#
|
42
|
+
# Public API Surface (stable):
|
43
|
+
# - #match?
|
44
|
+
# - #extract_params
|
45
|
+
# - #generate_path
|
46
|
+
# - #named?
|
47
|
+
# - #resource? / #collection?
|
48
|
+
#
|
49
|
+
# @api public
|
8
50
|
class Route
|
51
|
+
include ParamSupport
|
52
|
+
include SegmentCompiler
|
53
|
+
include PathBuilder
|
54
|
+
include RubyRoutes::Route::ConstraintValidator
|
55
|
+
include RubyRoutes::Route::ValidationHelpers
|
56
|
+
include RubyRoutes::Route::QueryHelpers
|
57
|
+
include RubyRoutes::Route::PathGeneration
|
58
|
+
include RubyRoutes::Utility::MethodUtility
|
59
|
+
include RubyRoutes::Utility::PathUtility
|
60
|
+
include RubyRoutes::Utility::KeyBuilderUtility
|
61
|
+
|
9
62
|
attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
|
10
63
|
|
64
|
+
public :extract_params, :parse_query_params, :query_params, :generate_path
|
65
|
+
|
66
|
+
# Create a new Route.
|
67
|
+
#
|
68
|
+
# @param path [String] The raw route path (may include `:params` or `*splat`).
|
69
|
+
# @param options [Hash] The options for the route.
|
70
|
+
# @option options [Symbol, String, Array<Symbol, String>] :via (:get) HTTP method(s).
|
71
|
+
# @option options [String] :to ("controller#action") The controller and action.
|
72
|
+
# @option options [String] :controller Explicit controller (overrides `:to`).
|
73
|
+
# @option options [String, Symbol] :action Explicit action (overrides part after `#`).
|
74
|
+
# @option options [Hash] :constraints Parameter constraints (Regexp / Symbol / Hash).
|
75
|
+
# @option options [Hash] :defaults Default parameter values.
|
76
|
+
# @option options [Symbol, String] :as The route name.
|
11
77
|
def initialize(path, options = {})
|
12
78
|
@path = normalize_path(path)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@controller = extract_controller(options)
|
18
|
-
@action = options[:action] || extract_action(options[:to])
|
79
|
+
|
80
|
+
setup_methods(options)
|
81
|
+
setup_controller_and_action(options)
|
82
|
+
|
19
83
|
@name = options[:as]
|
20
84
|
@constraints = options[:constraints] || {}
|
21
|
-
# Pre-normalize defaults to string keys and freeze
|
22
85
|
@defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
|
86
|
+
@param_key_slots = [[nil, nil], [nil, nil]]
|
87
|
+
@required_validated_once = false
|
23
88
|
|
24
|
-
# Pre-compile everything at initialization
|
25
89
|
precompile_route_data
|
26
90
|
validate_route!
|
27
91
|
end
|
28
92
|
|
93
|
+
# Test if this route matches an HTTP method + path string.
|
94
|
+
#
|
95
|
+
# @param request_method [String, Symbol] The HTTP method.
|
96
|
+
# @param request_path [String] The request path.
|
97
|
+
# @return [Boolean] `true` if the route matches, `false` otherwise.
|
29
98
|
def match?(request_method, request_path)
|
30
|
-
|
31
|
-
return false unless @methods_set.include?(
|
32
|
-
!!extract_path_params_fast(request_path)
|
33
|
-
end
|
34
|
-
|
35
|
-
def extract_params(request_path, parsed_qp = nil)
|
36
|
-
path_params = extract_path_params_fast(request_path)
|
99
|
+
normalized_method = normalize_http_method(request_method)
|
100
|
+
return false unless @methods_set.include?(normalized_method)
|
37
101
|
|
38
|
-
|
39
|
-
|
40
|
-
# Use optimized param building
|
41
|
-
build_params_hash(path_params, request_path, parsed_qp)
|
102
|
+
!!extract_path_params_fast(request_path)
|
42
103
|
end
|
43
104
|
|
105
|
+
# @return [Boolean] Whether this route has a name.
|
44
106
|
def named?
|
45
107
|
!@name.nil?
|
46
108
|
end
|
47
109
|
|
110
|
+
# @return [Boolean] Heuristic: path contains `:id` implying a resource member.
|
48
111
|
def resource?
|
49
112
|
@is_resource
|
50
113
|
end
|
51
114
|
|
115
|
+
# @return [Boolean] Inverse of `#resource?`.
|
52
116
|
def collection?
|
53
117
|
!@is_resource
|
54
118
|
end
|
55
119
|
|
56
|
-
def parse_query_params(path)
|
57
|
-
query_params_fast(path)
|
58
|
-
end
|
59
|
-
|
60
|
-
# Optimized path generation with better caching and fewer allocations
|
61
|
-
def generate_path(params = {})
|
62
|
-
return ROOT_PATH if @path == ROOT_PATH
|
63
|
-
|
64
|
-
# Fast path: empty params and no required params
|
65
|
-
if params.empty? && @required_params.empty?
|
66
|
-
return @static_path if @static_path
|
67
|
-
end
|
68
|
-
|
69
|
-
# Build merged params efficiently
|
70
|
-
merged = build_merged_params(params)
|
71
|
-
|
72
|
-
# Check required params (fast Set operation)
|
73
|
-
missing_params = @required_params_set - merged.keys
|
74
|
-
unless missing_params.empty?
|
75
|
-
raise RubyRoutes::RouteNotFound, "Missing params: #{missing_params.to_a.join(', ')}"
|
76
|
-
end
|
77
|
-
|
78
|
-
# Check for nil values in required params
|
79
|
-
nil_params = @required_params_set.select { |param| merged[param].nil? }
|
80
|
-
unless nil_params.empty?
|
81
|
-
raise RubyRoutes::RouteNotFound, "Missing or nil params: #{nil_params.to_a.join(', ')}"
|
82
|
-
end
|
83
|
-
|
84
|
-
# Cache lookup
|
85
|
-
cache_key = build_cache_key_fast(merged)
|
86
|
-
if (cached = @gen_cache.get(cache_key))
|
87
|
-
return cached
|
88
|
-
end
|
89
|
-
|
90
|
-
# Generate path using string buffer (avoid array allocations)
|
91
|
-
path_str = generate_path_string(merged)
|
92
|
-
@gen_cache.set(cache_key, path_str)
|
93
|
-
path_str
|
94
|
-
end
|
95
|
-
|
96
|
-
# Fast query params method (cached and optimized)
|
97
|
-
def query_params(request_path)
|
98
|
-
query_params_fast(request_path)
|
99
|
-
end
|
100
|
-
|
101
120
|
private
|
102
121
|
|
103
|
-
#
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
HTTP_POST = 'POST'.freeze
|
112
|
-
HTTP_PUT = 'PUT'.freeze
|
113
|
-
HTTP_PATCH = 'PATCH'.freeze
|
114
|
-
HTTP_DELETE = 'DELETE'.freeze
|
115
|
-
HTTP_HEAD = 'HEAD'.freeze
|
116
|
-
HTTP_OPTIONS = 'OPTIONS'.freeze
|
117
|
-
|
118
|
-
# Fast method normalization using interned constants
|
119
|
-
def normalize_method(method)
|
120
|
-
case method
|
121
|
-
when :get then HTTP_GET
|
122
|
-
when :post then HTTP_POST
|
123
|
-
when :put then HTTP_PUT
|
124
|
-
when :patch then HTTP_PATCH
|
125
|
-
when :delete then HTTP_DELETE
|
126
|
-
when :head then HTTP_HEAD
|
127
|
-
when :options then HTTP_OPTIONS
|
128
|
-
else method.to_s.upcase.freeze
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
# Pre-compile all route data at initialization
|
133
|
-
def precompile_route_data
|
134
|
-
@is_resource = @path.match?(/\/:id(?:$|\.)/)
|
135
|
-
@gen_cache = SmallLru.new(512) # larger cache
|
136
|
-
@query_cache = SmallLru.new(QUERY_CACHE_SIZE)
|
137
|
-
|
138
|
-
compile_segments
|
139
|
-
compile_required_params
|
140
|
-
check_static_path
|
141
|
-
end
|
142
|
-
|
143
|
-
def compile_segments
|
144
|
-
@compiled_segments = if @path == ROOT_PATH
|
145
|
-
EMPTY_ARRAY
|
146
|
-
else
|
147
|
-
@path.split('/').reject(&:empty?).map do |seg|
|
148
|
-
RubyRoutes::Constant.segment_descriptor(seg)
|
149
|
-
end.freeze
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def compile_required_params
|
154
|
-
param_names = @compiled_segments.filter_map { |s| s[:name] if s[:type] != :static }
|
155
|
-
@param_names = param_names.freeze
|
156
|
-
@required_params = param_names.reject { |n| @defaults.key?(n) }.freeze
|
157
|
-
@required_params_set = @required_params.to_set.freeze
|
158
|
-
end
|
159
|
-
|
160
|
-
def check_static_path
|
161
|
-
# Pre-generate static paths (no params)
|
162
|
-
if @required_params.empty?
|
163
|
-
@static_path = generate_static_path
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
def generate_static_path
|
168
|
-
return ROOT_PATH if @compiled_segments.empty?
|
169
|
-
|
170
|
-
parts = @compiled_segments.map { |seg| seg[:value] }
|
171
|
-
"/#{parts.join('/')}"
|
172
|
-
end
|
173
|
-
|
174
|
-
# Optimized param building
|
175
|
-
def build_params_hash(path_params, request_path, parsed_qp)
|
176
|
-
# Use pre-allocated hash when possible
|
177
|
-
result = get_thread_local_hash
|
178
|
-
|
179
|
-
# Path params first (highest priority)
|
180
|
-
result.update(path_params)
|
181
|
-
|
182
|
-
# Query params (if needed)
|
183
|
-
if parsed_qp
|
184
|
-
result.merge!(parsed_qp)
|
185
|
-
elsif request_path.include?('?')
|
186
|
-
qp = query_params_fast(request_path)
|
187
|
-
result.merge!(qp) unless qp.empty?
|
188
|
-
end
|
189
|
-
|
190
|
-
# Defaults (lowest priority)
|
191
|
-
merge_defaults_fast(result) unless @defaults.empty?
|
192
|
-
|
193
|
-
# Validate constraints efficiently
|
194
|
-
validate_constraints_fast!(result) unless @constraints.empty?
|
195
|
-
|
196
|
-
result.dup
|
197
|
-
end
|
198
|
-
|
199
|
-
def get_thread_local_hash
|
200
|
-
# Use a pool of hashes to reduce allocations
|
201
|
-
pool = Thread.current[:ruby_routes_hash_pool] ||= []
|
202
|
-
if pool.empty?
|
203
|
-
{}
|
204
|
-
else
|
205
|
-
hash = pool.pop
|
206
|
-
hash.clear
|
207
|
-
hash
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def return_hash_to_pool(hash)
|
212
|
-
pool = Thread.current[:ruby_routes_hash_pool] ||= []
|
213
|
-
pool.push(hash) if pool.size < 5 # Keep pool small to avoid memory bloat
|
214
|
-
end
|
215
|
-
|
216
|
-
def merge_defaults_fast(result)
|
217
|
-
@defaults.each { |k, v| result[k] = v unless result.key?(k) }
|
218
|
-
end
|
219
|
-
|
220
|
-
# Fast path parameter extraction
|
221
|
-
def extract_path_params_fast(request_path)
|
222
|
-
return EMPTY_HASH if @compiled_segments.empty? && request_path == ROOT_PATH
|
223
|
-
return nil if @compiled_segments.empty?
|
224
|
-
|
225
|
-
path_parts = split_path_fast(request_path)
|
226
|
-
|
227
|
-
# Check for wildcard/splat segment
|
228
|
-
has_splat = @compiled_segments.any? { |seg| seg[:type] == :splat }
|
229
|
-
|
230
|
-
if has_splat
|
231
|
-
return nil if path_parts.size < @compiled_segments.size - 1
|
232
|
-
else
|
233
|
-
return nil if @compiled_segments.size != path_parts.size
|
234
|
-
end
|
235
|
-
|
236
|
-
extract_params_from_parts(path_parts)
|
237
|
-
end
|
238
|
-
|
239
|
-
def split_path_fast(request_path)
|
240
|
-
# Remove query string before splitting
|
241
|
-
path = request_path.split('?', 2).first
|
242
|
-
path = path[1..-1] if path.start_with?('/')
|
243
|
-
path = path[0...-1] if path.end_with?('/') && path != ROOT_PATH
|
244
|
-
path.empty? ? [] : path.split('/')
|
245
|
-
end
|
246
|
-
|
247
|
-
def extract_params_from_parts(path_parts)
|
248
|
-
params = {}
|
249
|
-
|
250
|
-
@compiled_segments.each_with_index do |seg, idx|
|
251
|
-
case seg[:type]
|
252
|
-
when :static
|
253
|
-
return nil unless seg[:value] == path_parts[idx]
|
254
|
-
when :param
|
255
|
-
params[seg[:name]] = path_parts[idx]
|
256
|
-
when :splat
|
257
|
-
params[seg[:name]] = path_parts[idx..-1].join('/')
|
258
|
-
break
|
259
|
-
end
|
260
|
-
end
|
261
|
-
|
262
|
-
params
|
263
|
-
end
|
264
|
-
|
265
|
-
# Optimized merged params building
|
266
|
-
def build_merged_params(params)
|
267
|
-
return @defaults if params.empty?
|
268
|
-
|
269
|
-
merged = get_thread_local_merged_hash
|
270
|
-
|
271
|
-
# Merge defaults first if they exist
|
272
|
-
merged.update(@defaults) unless @defaults.empty?
|
273
|
-
|
274
|
-
# Use merge! with transform_keys for better performance
|
275
|
-
if params.respond_to?(:transform_keys)
|
276
|
-
merged.merge!(params.transform_keys(&:to_s))
|
277
|
-
else
|
278
|
-
# Fallback for older Ruby versions
|
279
|
-
params.each { |k, v| merged[k.to_s] = v }
|
280
|
-
end
|
281
|
-
|
282
|
-
merged
|
283
|
-
end
|
284
|
-
|
285
|
-
def get_thread_local_merged_hash
|
286
|
-
hash = Thread.current[:ruby_routes_merged] ||= {}
|
287
|
-
hash.clear
|
288
|
-
hash
|
289
|
-
end
|
290
|
-
|
291
|
-
# Fast cache key building with minimal allocations
|
292
|
-
def build_cache_key_fast(merged)
|
293
|
-
return '' if @required_params.empty?
|
294
|
-
|
295
|
-
# Use array join which is faster than string concatenation
|
296
|
-
parts = @required_params.map do |name|
|
297
|
-
value = merged[name]
|
298
|
-
value.is_a?(Array) ? value.join('/') : value.to_s
|
299
|
-
end
|
300
|
-
parts.join('|')
|
301
|
-
end
|
302
|
-
|
303
|
-
# Optimized path generation
|
304
|
-
def generate_path_string(merged)
|
305
|
-
return ROOT_PATH if @compiled_segments.empty?
|
306
|
-
|
307
|
-
# Pre-allocate array for parts to avoid string buffer operations
|
308
|
-
parts = []
|
309
|
-
|
310
|
-
@compiled_segments.each do |seg|
|
311
|
-
case seg[:type]
|
312
|
-
when :static
|
313
|
-
parts << seg[:value]
|
314
|
-
when :param
|
315
|
-
value = merged.fetch(seg[:name]).to_s
|
316
|
-
parts << encode_segment_fast(value)
|
317
|
-
when :splat
|
318
|
-
value = merged.fetch(seg[:name], '')
|
319
|
-
parts << format_splat_value(value)
|
320
|
-
end
|
321
|
-
end
|
322
|
-
|
323
|
-
# Single join operation is faster than multiple string concatenations
|
324
|
-
path = "/#{parts.join('/')}"
|
325
|
-
path == '/' ? ROOT_PATH : path
|
326
|
-
end
|
327
|
-
|
328
|
-
def format_splat_value(value)
|
329
|
-
case value
|
330
|
-
when Array
|
331
|
-
value.map { |part| encode_segment_fast(part.to_s) }.join('/')
|
332
|
-
when String
|
333
|
-
value.split('/').map { |part| encode_segment_fast(part) }.join('/')
|
334
|
-
else
|
335
|
-
encode_segment_fast(value.to_s)
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
|
-
# Fast segment encoding with caching for common values
|
340
|
-
def encode_segment_fast(str)
|
341
|
-
return str if UNRESERVED_RE.match?(str)
|
342
|
-
|
343
|
-
# Cache encoded segments to avoid repeated encoding
|
344
|
-
@encoding_cache ||= {}
|
345
|
-
@encoding_cache[str] ||= begin
|
346
|
-
# Use URI.encode_www_form_component but replace + with %20 for path segments
|
347
|
-
URI.encode_www_form_component(str).gsub('+', '%20')
|
348
|
-
end
|
349
|
-
end
|
350
|
-
|
351
|
-
# Optimized query params with caching
|
352
|
-
def query_params_fast(path)
|
353
|
-
query_start = path.index('?')
|
354
|
-
return EMPTY_HASH unless query_start
|
355
|
-
|
356
|
-
query_string = path[(query_start + 1)..-1]
|
357
|
-
return EMPTY_HASH if query_string.empty? || query_string.match?(/^\?+$/)
|
358
|
-
|
359
|
-
# Cache query param parsing
|
360
|
-
if (cached = @query_cache.get(query_string))
|
361
|
-
return cached
|
362
|
-
end
|
363
|
-
|
364
|
-
result = Rack::Utils.parse_query(query_string)
|
365
|
-
@query_cache.set(query_string, result)
|
366
|
-
result
|
122
|
+
# Set up HTTP methods from options.
|
123
|
+
#
|
124
|
+
# @param options [Hash] The options for the route.
|
125
|
+
# @return [void]
|
126
|
+
def setup_methods(options)
|
127
|
+
raw_http_methods = Array(options[:via] || :get)
|
128
|
+
@methods = raw_http_methods.map { |method| normalize_http_method(method) }.freeze
|
129
|
+
@methods_set = @methods.to_set.freeze
|
367
130
|
end
|
368
131
|
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
132
|
+
# Set up controller and action from options.
|
133
|
+
#
|
134
|
+
# @param options [Hash] The options for the route.
|
135
|
+
# @return [void]
|
136
|
+
def setup_controller_and_action(options)
|
137
|
+
@controller = extract_controller(options)
|
138
|
+
@action = options[:action] || extract_action(options[:to])
|
374
139
|
end
|
375
140
|
|
141
|
+
# Infer controller name from options or `:to`.
|
142
|
+
#
|
143
|
+
# @param options [Hash] The options for the route.
|
144
|
+
# @return [String, nil] The inferred controller name.
|
376
145
|
def extract_controller(options)
|
377
146
|
to = options[:to]
|
378
147
|
return options[:controller] unless to
|
148
|
+
|
379
149
|
to.to_s.split('#', 2).first
|
380
150
|
end
|
381
151
|
|
152
|
+
# Infer action from `:to` string.
|
153
|
+
#
|
154
|
+
# @param to [String, nil] The `:to` string.
|
155
|
+
# @return [String, nil] The inferred action name.
|
382
156
|
def extract_action(to)
|
383
157
|
return nil unless to
|
384
|
-
to.to_s.split('#', 2).last
|
385
|
-
end
|
386
|
-
|
387
|
-
# Optimized constraint validation
|
388
|
-
def validate_constraints_fast!(params)
|
389
|
-
@constraints.each do |param, constraint|
|
390
|
-
value = params[param.to_s]
|
391
|
-
# Only skip validation if the parameter is completely missing from params
|
392
|
-
# Empty strings and nil values should still be validated
|
393
|
-
next unless params.key?(param.to_s)
|
394
|
-
|
395
|
-
case constraint
|
396
|
-
when Regexp
|
397
|
-
# Protect against ReDoS attacks with timeout
|
398
|
-
begin
|
399
|
-
Timeout.timeout(0.1) do
|
400
|
-
raise RubyRoutes::ConstraintViolation unless constraint.match?(value.to_s)
|
401
|
-
end
|
402
|
-
rescue Timeout::Error
|
403
|
-
raise RubyRoutes::ConstraintViolation, "Regex constraint timed out (potential ReDoS attack)"
|
404
|
-
end
|
405
|
-
when Proc
|
406
|
-
# DEPRECATED: Proc constraints are deprecated due to security risks
|
407
|
-
warn_proc_constraint_deprecation(param)
|
408
|
-
|
409
|
-
# For backward compatibility, still execute but with strict timeout
|
410
|
-
begin
|
411
|
-
Timeout.timeout(0.05) do # Reduced timeout for security
|
412
|
-
raise RubyRoutes::ConstraintViolation unless constraint.call(value.to_s)
|
413
|
-
end
|
414
|
-
rescue Timeout::Error
|
415
|
-
raise RubyRoutes::ConstraintViolation, "Proc constraint timed out (consider using secure alternatives)"
|
416
|
-
rescue => e
|
417
|
-
raise RubyRoutes::ConstraintViolation, "Proc constraint failed: #{e.message}"
|
418
|
-
end
|
419
|
-
when :int
|
420
|
-
value_str = value.to_s
|
421
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A\d+\z/)
|
422
|
-
when :uuid
|
423
|
-
value_str = value.to_s
|
424
|
-
raise RubyRoutes::ConstraintViolation unless value_str.length == 36 &&
|
425
|
-
value_str.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
426
|
-
when :email
|
427
|
-
value_str = value.to_s
|
428
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
|
429
|
-
when :slug
|
430
|
-
value_str = value.to_s
|
431
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
|
432
|
-
when :alpha
|
433
|
-
value_str = value.to_s
|
434
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z]+\z/)
|
435
|
-
when :alphanumeric
|
436
|
-
value_str = value.to_s
|
437
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z0-9]+\z/)
|
438
|
-
when Hash
|
439
|
-
# Secure hash-based constraints for common patterns
|
440
|
-
validate_hash_constraint!(constraint, value_str = value.to_s)
|
441
|
-
end
|
442
|
-
end
|
443
|
-
end
|
444
|
-
|
445
|
-
def warn_proc_constraint_deprecation(param)
|
446
|
-
return if @proc_warnings_shown&.include?(param)
|
447
|
-
|
448
|
-
@proc_warnings_shown ||= Set.new
|
449
|
-
@proc_warnings_shown << param
|
450
|
-
|
451
|
-
warn <<~WARNING
|
452
|
-
[DEPRECATION] Proc constraints are deprecated due to security risks.
|
453
|
-
|
454
|
-
Parameter: #{param}
|
455
|
-
Route: #{@path}
|
456
158
|
|
457
|
-
|
458
|
-
- Use regex: constraints: { #{param}: /\\A\\d+\\z/ }
|
459
|
-
- Use built-in types: constraints: { #{param}: :int }
|
460
|
-
- Use hash constraints: constraints: { #{param}: { min_length: 3, format: /\\A[a-z]+\\z/ } }
|
461
|
-
|
462
|
-
Available built-in types: :int, :uuid, :email, :slug, :alpha, :alphanumeric
|
463
|
-
|
464
|
-
This warning will become an error in a future version.
|
465
|
-
WARNING
|
466
|
-
end
|
467
|
-
|
468
|
-
def validate_hash_constraint!(constraint, value)
|
469
|
-
# Secure hash-based constraints
|
470
|
-
if constraint[:min_length] && value.length < constraint[:min_length]
|
471
|
-
raise RubyRoutes::ConstraintViolation, "Value too short (minimum #{constraint[:min_length]} characters)"
|
472
|
-
end
|
473
|
-
|
474
|
-
if constraint[:max_length] && value.length > constraint[:max_length]
|
475
|
-
raise RubyRoutes::ConstraintViolation, "Value too long (maximum #{constraint[:max_length]} characters)"
|
476
|
-
end
|
477
|
-
|
478
|
-
if constraint[:format] && !value.match?(constraint[:format])
|
479
|
-
raise RubyRoutes::ConstraintViolation, "Value does not match required format"
|
480
|
-
end
|
481
|
-
|
482
|
-
if constraint[:in] && !constraint[:in].include?(value)
|
483
|
-
raise RubyRoutes::ConstraintViolation, "Value not in allowed list"
|
484
|
-
end
|
485
|
-
|
486
|
-
if constraint[:not_in] && constraint[:not_in].include?(value)
|
487
|
-
raise RubyRoutes::ConstraintViolation, "Value in forbidden list"
|
488
|
-
end
|
489
|
-
|
490
|
-
if constraint[:range] && !constraint[:range].cover?(value.to_i)
|
491
|
-
raise RubyRoutes::ConstraintViolation, "Value not in allowed range"
|
492
|
-
end
|
159
|
+
to.to_s.split('#', 2).last
|
493
160
|
end
|
494
161
|
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
162
|
+
# Precompile route data for performance.
|
163
|
+
#
|
164
|
+
# @return [void]
|
165
|
+
def precompile_route_data
|
166
|
+
@is_resource = @path.match?(%r{/:id(?:$|\.)})
|
167
|
+
@gen_cache = SmallLru.new(512)
|
168
|
+
@query_cache = SmallLru.new(RubyRoutes::Constant::QUERY_CACHE_SIZE)
|
169
|
+
initialize_validation_cache
|
170
|
+
compile_segments
|
171
|
+
compile_required_params
|
172
|
+
check_static_path
|
499
173
|
end
|
500
|
-
|
501
|
-
# Additional constants
|
502
|
-
EMPTY_ARRAY = [].freeze
|
503
|
-
EMPTY_STRING = ''.freeze
|
504
174
|
end
|
505
|
-
|
175
|
+
end
|