ruby_routes 2.2.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 +75 -33
- 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 +79 -227
- 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 +93 -18
- 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 +124 -501
- 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 +120 -133
- 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 +193 -181
- 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 +161 -84
- data/lib/ruby_routes/utility/method_utility.rb +137 -0
- data/lib/ruby_routes/utility/path_utility.rb +75 -28
- data/lib/ruby_routes/utility/route_utility.rb +30 -2
- data/lib/ruby_routes/version.rb +3 -1
- data/lib/ruby_routes.rb +68 -11
- metadata +27 -7
data/lib/ruby_routes/route.rb
CHANGED
@@ -1,552 +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'
|
6
|
-
require_relative 'utility/path_utility'
|
7
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'
|
8
21
|
|
9
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
|
10
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
|
11
59
|
include RubyRoutes::Utility::PathUtility
|
12
60
|
include RubyRoutes::Utility::KeyBuilderUtility
|
13
61
|
|
14
62
|
attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
|
15
63
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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.
|
20
77
|
def initialize(path, options = {})
|
21
78
|
@path = normalize_path(path)
|
22
|
-
# Pre-normalize and freeze methods at creation time
|
23
|
-
raw_methods = Array(options[:via] || :get)
|
24
|
-
@methods = raw_methods.map { |m| normalize_method(m) }.freeze
|
25
|
-
@methods_set = @methods.to_set.freeze
|
26
|
-
@controller = extract_controller(options)
|
27
|
-
@action = options[:action] || extract_action(options[:to])
|
28
|
-
@name = options[:as]
|
29
|
-
@constraints = options[:constraints] || {}
|
30
|
-
# Pre-normalize defaults to string keys and freeze
|
31
|
-
@defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
|
32
79
|
|
33
|
-
|
80
|
+
setup_methods(options)
|
81
|
+
setup_controller_and_action(options)
|
82
|
+
|
83
|
+
@name = options[:as]
|
84
|
+
@constraints = options[:constraints] || {}
|
85
|
+
@defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
|
86
|
+
@param_key_slots = [[nil, nil], [nil, nil]]
|
87
|
+
@required_validated_once = false
|
88
|
+
|
34
89
|
precompile_route_data
|
35
90
|
validate_route!
|
36
91
|
end
|
37
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.
|
38
98
|
def match?(request_method, request_path)
|
39
|
-
|
40
|
-
return false unless @methods_set.include?(
|
41
|
-
!!extract_path_params_fast(request_path)
|
42
|
-
end
|
99
|
+
normalized_method = normalize_http_method(request_method)
|
100
|
+
return false unless @methods_set.include?(normalized_method)
|
43
101
|
|
44
|
-
|
45
|
-
path_params = extract_path_params_fast(request_path)
|
46
|
-
|
47
|
-
return EMPTY_HASH unless path_params
|
48
|
-
|
49
|
-
# Use optimized param building
|
50
|
-
build_params_hash(path_params, request_path, parsed_qp)
|
102
|
+
!!extract_path_params_fast(request_path)
|
51
103
|
end
|
52
104
|
|
105
|
+
# @return [Boolean] Whether this route has a name.
|
53
106
|
def named?
|
54
107
|
!@name.nil?
|
55
108
|
end
|
56
109
|
|
110
|
+
# @return [Boolean] Heuristic: path contains `:id` implying a resource member.
|
57
111
|
def resource?
|
58
112
|
@is_resource
|
59
113
|
end
|
60
114
|
|
115
|
+
# @return [Boolean] Inverse of `#resource?`.
|
61
116
|
def collection?
|
62
117
|
!@is_resource
|
63
118
|
end
|
64
119
|
|
65
|
-
def parse_query_params(path)
|
66
|
-
query_params_fast(path)
|
67
|
-
end
|
68
|
-
|
69
|
-
# Optimized path generation with better caching and fewer allocations
|
70
|
-
def generate_path(params = {})
|
71
|
-
return @static_path if @static_path && (params.nil? || params.empty?)
|
72
|
-
params ||= {}
|
73
|
-
missing, nils = validate_required_params(params)
|
74
|
-
raise RouteNotFound, "Missing params: #{missing.join(', ')}" unless missing.empty?
|
75
|
-
raise RouteNotFound, "Missing or nil params: #{nils.join(', ')}" unless nils.empty?
|
76
|
-
|
77
|
-
merged = build_merged_params(params)
|
78
|
-
cache_key = cache_key_for_params(@required_params, merged)
|
79
|
-
if (cached = @gen_cache.get(cache_key))
|
80
|
-
return cached
|
81
|
-
end
|
82
|
-
path_str = generate_path_string(merged)
|
83
|
-
@gen_cache.set(cache_key, path_str)
|
84
|
-
path_str
|
85
|
-
end
|
86
|
-
|
87
|
-
# Fast query params method (cached and optimized)
|
88
|
-
def query_params(request_path)
|
89
|
-
query_params_fast(request_path)
|
90
|
-
end
|
91
|
-
|
92
120
|
private
|
93
121
|
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
when :head then HTTP_HEAD
|
118
|
-
when :options then HTTP_OPTIONS
|
119
|
-
else method.to_s.upcase.freeze
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
# Pre-compile all route data at initialization
|
124
|
-
def precompile_route_data
|
125
|
-
@is_resource = @path.match?(/\/:id(?:$|\.)/)
|
126
|
-
@gen_cache = SmallLru.new(512) # larger cache
|
127
|
-
@query_cache = SmallLru.new(QUERY_CACHE_SIZE)
|
128
|
-
initialize_validation_cache
|
129
|
-
|
130
|
-
compile_segments
|
131
|
-
compile_required_params
|
132
|
-
check_static_path
|
133
|
-
end
|
134
|
-
|
135
|
-
def compile_segments
|
136
|
-
@compiled_segments = if @path == ROOT_PATH
|
137
|
-
EMPTY_ARRAY
|
138
|
-
else
|
139
|
-
@path.split('/').reject(&:empty?).map do |seg|
|
140
|
-
RubyRoutes::Constant.segment_descriptor(seg)
|
141
|
-
end.freeze
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
def compile_required_params
|
146
|
-
param_names = @compiled_segments.filter_map { |s| s[:name] if s[:type] != :static }
|
147
|
-
@param_names = param_names.freeze
|
148
|
-
@required_params = param_names.reject { |n| @defaults.key?(n) }.freeze
|
149
|
-
@required_params_set = @required_params.to_set.freeze
|
150
|
-
end
|
151
|
-
|
152
|
-
def check_static_path
|
153
|
-
# Pre-generate static paths (no params)
|
154
|
-
if @required_params.empty?
|
155
|
-
@static_path = generate_static_path
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def generate_static_path
|
160
|
-
return ROOT_PATH if @compiled_segments.empty?
|
161
|
-
|
162
|
-
parts = @compiled_segments.map { |seg| seg[:value] }
|
163
|
-
"/#{parts.join('/')}"
|
164
|
-
end
|
165
|
-
|
166
|
-
# Optimized param building
|
167
|
-
def build_params_hash(path_params, request_path, parsed_qp)
|
168
|
-
# Use pre-allocated hash when possible
|
169
|
-
result = get_thread_local_hash
|
170
|
-
|
171
|
-
# Path params first (highest priority)
|
172
|
-
result.update(path_params)
|
173
|
-
|
174
|
-
# Query params (if needed)
|
175
|
-
if parsed_qp
|
176
|
-
result.merge!(parsed_qp)
|
177
|
-
elsif request_path.include?('?')
|
178
|
-
qp = query_params_fast(request_path)
|
179
|
-
result.merge!(qp) unless qp.empty?
|
180
|
-
end
|
181
|
-
|
182
|
-
# Defaults (lowest priority)
|
183
|
-
merge_defaults_fast(result) unless @defaults.empty?
|
184
|
-
|
185
|
-
# Validate constraints efficiently
|
186
|
-
validate_constraints_fast!(result) unless @constraints.empty?
|
187
|
-
|
188
|
-
result
|
189
|
-
end
|
190
|
-
|
191
|
-
def get_thread_local_hash
|
192
|
-
# Use a pool of hashes to reduce allocations
|
193
|
-
pool = Thread.current[:ruby_routes_hash_pool] ||= []
|
194
|
-
if pool.empty?
|
195
|
-
{}
|
196
|
-
else
|
197
|
-
hash = pool.pop
|
198
|
-
hash.clear
|
199
|
-
hash
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
def return_hash_to_pool(hash)
|
204
|
-
pool = Thread.current[:ruby_routes_hash_pool] ||= []
|
205
|
-
pool.push(hash) if pool.size < 5 # Keep pool small to avoid memory bloat
|
206
|
-
end
|
207
|
-
|
208
|
-
def merge_defaults_fast(result)
|
209
|
-
@defaults.each { |k, v| result[k] = v unless result.key?(k) }
|
210
|
-
end
|
211
|
-
|
212
|
-
# Fast path parameter extraction
|
213
|
-
def extract_path_params_fast(request_path)
|
214
|
-
return EMPTY_HASH if @compiled_segments.empty? && request_path == ROOT_PATH
|
215
|
-
return nil if @compiled_segments.empty?
|
216
|
-
|
217
|
-
path_parts = split_path(request_path)
|
218
|
-
|
219
|
-
# Check for wildcard/splat segment
|
220
|
-
has_splat = @compiled_segments.any? { |seg| seg[:type] == :splat }
|
221
|
-
|
222
|
-
if has_splat
|
223
|
-
return nil if path_parts.size < @compiled_segments.size - 1
|
224
|
-
else
|
225
|
-
return nil if @compiled_segments.size != path_parts.size
|
226
|
-
end
|
227
|
-
|
228
|
-
extract_params_from_parts(path_parts)
|
229
|
-
end
|
230
|
-
|
231
|
-
def extract_params_from_parts(path_parts)
|
232
|
-
params = {}
|
233
|
-
|
234
|
-
@compiled_segments.each_with_index do |seg, idx|
|
235
|
-
case seg[:type]
|
236
|
-
when :static
|
237
|
-
return nil unless seg[:value] == path_parts[idx]
|
238
|
-
when :param
|
239
|
-
params[seg[:name]] = path_parts[idx]
|
240
|
-
when :splat
|
241
|
-
params[seg[:name]] = path_parts[idx..-1].join('/')
|
242
|
-
break
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
params
|
247
|
-
end
|
248
|
-
|
249
|
-
# Optimized merged params building
|
250
|
-
def build_merged_params(params)
|
251
|
-
return @defaults if params.nil? || params.empty?
|
252
|
-
h = Thread.current[:ruby_routes_merge_hash] ||= {}
|
253
|
-
h.clear
|
254
|
-
@defaults.each { |k,v| h[k] = v }
|
255
|
-
params.each do |k,v|
|
256
|
-
next if v.nil?
|
257
|
-
ks = k.is_a?(String) ? k : k.to_s
|
258
|
-
h[ks] = v
|
259
|
-
end
|
260
|
-
h
|
261
|
-
end
|
262
|
-
|
263
|
-
def get_thread_local_merged_hash
|
264
|
-
hash = Thread.current[:ruby_routes_merged] ||= {}
|
265
|
-
hash.clear
|
266
|
-
hash
|
267
|
-
end
|
268
|
-
|
269
|
-
# Optimized path generation
|
270
|
-
def generate_path_string(merged)
|
271
|
-
return ROOT_PATH if @compiled_segments.empty?
|
272
|
-
|
273
|
-
# Estimate final path length to avoid resizing
|
274
|
-
estimated_size = 1 # For leading slash
|
275
|
-
@compiled_segments.each do |seg|
|
276
|
-
case seg[:type]
|
277
|
-
when :static
|
278
|
-
estimated_size += seg[:value].length + 1 # +1 for slash
|
279
|
-
when :param, :splat
|
280
|
-
estimated_size += 20 # Average param length estimate
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
# Use string buffer with pre-allocated capacity
|
285
|
-
path = String.new(capacity: estimated_size)
|
286
|
-
path << '/'
|
287
|
-
|
288
|
-
# Generate path directly into buffer
|
289
|
-
last_idx = @compiled_segments.size - 1
|
290
|
-
@compiled_segments.each_with_index do |seg, i|
|
291
|
-
case seg[:type]
|
292
|
-
when :static
|
293
|
-
path << seg[:value]
|
294
|
-
when :param
|
295
|
-
value = merged.fetch(seg[:name]).to_s
|
296
|
-
path << encode_segment_fast(value)
|
297
|
-
when :splat
|
298
|
-
value = merged.fetch(seg[:name], '')
|
299
|
-
path << format_splat_value(value)
|
300
|
-
end
|
301
|
-
|
302
|
-
path << '/' unless i == last_idx
|
303
|
-
end
|
304
|
-
|
305
|
-
path
|
306
|
-
end
|
307
|
-
|
308
|
-
def format_splat_value(value)
|
309
|
-
case value
|
310
|
-
when Array
|
311
|
-
value.map { |part| encode_segment_fast(part.to_s) }.join('/')
|
312
|
-
when String
|
313
|
-
value.split('/').map { |part| encode_segment_fast(part) }.join('/')
|
314
|
-
else
|
315
|
-
encode_segment_fast(value.to_s)
|
316
|
-
end
|
317
|
-
end
|
318
|
-
|
319
|
-
# Fast segment encoding with caching for common values
|
320
|
-
def encode_segment_fast(str)
|
321
|
-
return str if UNRESERVED_RE.match?(str)
|
322
|
-
|
323
|
-
# Cache encoded segments to avoid repeated encoding
|
324
|
-
@encoding_cache ||= {}
|
325
|
-
@encoding_cache[str] ||= begin
|
326
|
-
# Use URI.encode_www_form_component but replace + with %20 for path segments
|
327
|
-
URI.encode_www_form_component(str).gsub('+', '%20')
|
328
|
-
end
|
329
|
-
end
|
330
|
-
|
331
|
-
# Optimized query params with caching
|
332
|
-
def query_params_fast(path)
|
333
|
-
query_start = path.index('?')
|
334
|
-
return EMPTY_HASH unless query_start
|
335
|
-
|
336
|
-
query_string = path[(query_start + 1)..-1]
|
337
|
-
return EMPTY_HASH if query_string.empty? || query_string.match?(/^\?+$/)
|
338
|
-
|
339
|
-
# Cache query param parsing
|
340
|
-
if (cached = @query_cache.get(query_string))
|
341
|
-
return cached
|
342
|
-
end
|
343
|
-
|
344
|
-
result = Rack::Utils.parse_query(query_string)
|
345
|
-
@query_cache.set(query_string, result)
|
346
|
-
result
|
347
|
-
end
|
348
|
-
|
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
|
130
|
+
end
|
131
|
+
|
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])
|
139
|
+
end
|
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.
|
349
145
|
def extract_controller(options)
|
350
146
|
to = options[:to]
|
351
147
|
return options[:controller] unless to
|
148
|
+
|
352
149
|
to.to_s.split('#', 2).first
|
353
150
|
end
|
354
151
|
|
152
|
+
# Infer action from `:to` string.
|
153
|
+
#
|
154
|
+
# @param to [String, nil] The `:to` string.
|
155
|
+
# @return [String, nil] The inferred action name.
|
355
156
|
def extract_action(to)
|
356
157
|
return nil unless to
|
357
|
-
to.to_s.split('#', 2).last
|
358
|
-
end
|
359
|
-
|
360
|
-
|
361
|
-
def validate_required_params(params)
|
362
|
-
return EMPTY_PAIR if @required_params.empty?
|
363
|
-
missing = nil
|
364
|
-
nils = nil
|
365
|
-
@required_params.each do |rk|
|
366
|
-
if params.key?(rk)
|
367
|
-
(nils ||= []) << rk if params[rk].nil?
|
368
|
-
elsif params.key?(sym = rk.to_sym)
|
369
|
-
(nils ||= []) << rk if params[sym].nil?
|
370
|
-
else
|
371
|
-
(missing ||= []) << rk
|
372
|
-
end
|
373
|
-
end
|
374
|
-
[missing || EMPTY_ARRAY, nils || EMPTY_ARRAY]
|
375
|
-
end
|
376
|
-
|
377
|
-
# Add this validation cache management
|
378
|
-
def initialize_validation_cache
|
379
|
-
@validation_cache = SmallLru.new(64)
|
380
|
-
end
|
381
|
-
|
382
|
-
def cache_validation_result(params, result)
|
383
|
-
# Only cache immutable params to prevent subtle bugs
|
384
|
-
if params.frozen? && @validation_cache && @validation_cache.size < 64
|
385
|
-
@validation_cache.set(params.hash, result)
|
386
|
-
end
|
387
|
-
end
|
388
|
-
|
389
|
-
def get_cached_validation(params)
|
390
|
-
return nil unless @validation_cache
|
391
|
-
@validation_cache.get(params.hash)
|
392
|
-
end
|
393
|
-
|
394
|
-
# Fast parameter type detection with result caching
|
395
|
-
def params_type(params)
|
396
|
-
# Cache parameter type detection results
|
397
|
-
@params_type_cache ||= {}
|
398
|
-
param_obj_id = params.object_id
|
399
158
|
|
400
|
-
#
|
401
|
-
return @params_type_cache[param_obj_id] if @params_type_cache.key?(param_obj_id)
|
402
|
-
|
403
|
-
# Type detection with explicit checks
|
404
|
-
type = if params.is_a?(Hash)
|
405
|
-
# Further refine hash type for optimization
|
406
|
-
refine_hash_type(params)
|
407
|
-
elsif params.respond_to?(:each) && params.respond_to?(:[])
|
408
|
-
# Hash-like enumerable
|
409
|
-
:enumerable
|
410
|
-
else
|
411
|
-
# Last resort - may
|
412
|
-
:mehod_missing
|
413
|
-
end
|
414
|
-
|
415
|
-
# Keep cache small
|
416
|
-
if @params_type_cache.size > 100
|
417
|
-
@params_type_cache.clear
|
418
|
-
end
|
419
|
-
|
420
|
-
# Cache result
|
421
|
-
@params_type_cache[param_obj_id] = type
|
422
|
-
end
|
423
|
-
|
424
|
-
# Further refine hash type for potential optimization
|
425
|
-
def refine_hash_type(params)
|
426
|
-
# Only sample a few keys to determine type tendency
|
427
|
-
key_samples = params.keys.take(3)
|
428
|
-
|
429
|
-
if key_samples.all? { |k| k.is_a?(String) }
|
430
|
-
:string_keyed_hash
|
431
|
-
elsif key_samples.all? { |k| k.is_a?(Symbol) }
|
432
|
-
:symbol_keyed_hash
|
433
|
-
else
|
434
|
-
:hash # Mixed keys
|
435
|
-
end
|
436
|
-
end
|
437
|
-
|
438
|
-
# Optimized constraint validation
|
439
|
-
def validate_constraints_fast!(params)
|
440
|
-
@constraints.each do |param, constraint|
|
441
|
-
value = params[param.to_s]
|
442
|
-
# Only skip validation if the parameter is completely missing from params
|
443
|
-
# Empty strings and nil values should still be validated
|
444
|
-
next unless params.key?(param.to_s)
|
445
|
-
|
446
|
-
case constraint
|
447
|
-
when Regexp
|
448
|
-
# Protect against ReDoS attacks with timeout
|
449
|
-
begin
|
450
|
-
Timeout.timeout(0.1) do
|
451
|
-
raise RubyRoutes::ConstraintViolation unless constraint.match?(value.to_s)
|
452
|
-
end
|
453
|
-
rescue Timeout::Error
|
454
|
-
raise RubyRoutes::ConstraintViolation, "Regex constraint timed out (potential ReDoS attack)"
|
455
|
-
end
|
456
|
-
when Proc
|
457
|
-
# DEPRECATED: Proc constraints are deprecated due to security risks
|
458
|
-
warn_proc_constraint_deprecation(param)
|
459
|
-
|
460
|
-
# For backward compatibility, still execute but with strict timeout
|
461
|
-
begin
|
462
|
-
Timeout.timeout(0.05) do # Reduced timeout for security
|
463
|
-
raise RubyRoutes::ConstraintViolation unless constraint.call(value.to_s)
|
464
|
-
end
|
465
|
-
rescue Timeout::Error
|
466
|
-
raise RubyRoutes::ConstraintViolation, "Proc constraint timed out (consider using secure alternatives)"
|
467
|
-
rescue => e
|
468
|
-
raise RubyRoutes::ConstraintViolation, "Proc constraint failed: #{e.message}"
|
469
|
-
end
|
470
|
-
when :int
|
471
|
-
value_str = value.to_s
|
472
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A\d+\z/)
|
473
|
-
when :uuid
|
474
|
-
value_str = value.to_s
|
475
|
-
raise RubyRoutes::ConstraintViolation unless value_str.length == 36 &&
|
476
|
-
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)
|
477
|
-
when :email
|
478
|
-
value_str = value.to_s
|
479
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
|
480
|
-
when :slug
|
481
|
-
value_str = value.to_s
|
482
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
|
483
|
-
when :alpha
|
484
|
-
value_str = value.to_s
|
485
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z]+\z/)
|
486
|
-
when :alphanumeric
|
487
|
-
value_str = value.to_s
|
488
|
-
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z0-9]+\z/)
|
489
|
-
when Hash
|
490
|
-
# Secure hash-based constraints for common patterns
|
491
|
-
validate_hash_constraint!(constraint, value_str = value.to_s)
|
492
|
-
end
|
493
|
-
end
|
494
|
-
end
|
495
|
-
|
496
|
-
def warn_proc_constraint_deprecation(param)
|
497
|
-
return if @proc_warnings_shown&.include?(param)
|
498
|
-
|
499
|
-
@proc_warnings_shown ||= Set.new
|
500
|
-
@proc_warnings_shown << param
|
501
|
-
|
502
|
-
warn <<~WARNING
|
503
|
-
[DEPRECATION] Proc constraints are deprecated due to security risks.
|
504
|
-
|
505
|
-
Parameter: #{param}
|
506
|
-
Route: #{@path}
|
507
|
-
|
508
|
-
Secure alternatives:
|
509
|
-
- Use regex: constraints: { #{param}: /\\A\\d+\\z/ }
|
510
|
-
- Use built-in types: constraints: { #{param}: :int }
|
511
|
-
- Use hash constraints: constraints: { #{param}: { min_length: 3, format: /\\A[a-z]+\\z/ } }
|
512
|
-
|
513
|
-
Available built-in types: :int, :uuid, :email, :slug, :alpha, :alphanumeric
|
514
|
-
|
515
|
-
This warning will become an error in a future version.
|
516
|
-
WARNING
|
517
|
-
end
|
518
|
-
|
519
|
-
def validate_hash_constraint!(constraint, value)
|
520
|
-
# Secure hash-based constraints
|
521
|
-
if constraint[:min_length] && value.length < constraint[:min_length]
|
522
|
-
raise RubyRoutes::ConstraintViolation, "Value too short (minimum #{constraint[:min_length]} characters)"
|
523
|
-
end
|
524
|
-
|
525
|
-
if constraint[:max_length] && value.length > constraint[:max_length]
|
526
|
-
raise RubyRoutes::ConstraintViolation, "Value too long (maximum #{constraint[:max_length]} characters)"
|
527
|
-
end
|
528
|
-
|
529
|
-
if constraint[:format] && !value.match?(constraint[:format])
|
530
|
-
raise RubyRoutes::ConstraintViolation, "Value does not match required format"
|
531
|
-
end
|
532
|
-
|
533
|
-
if constraint[:in] && !constraint[:in].include?(value)
|
534
|
-
raise RubyRoutes::ConstraintViolation, "Value not in allowed list"
|
535
|
-
end
|
536
|
-
|
537
|
-
if constraint[:not_in] && constraint[:not_in].include?(value)
|
538
|
-
raise RubyRoutes::ConstraintViolation, "Value in forbidden list"
|
539
|
-
end
|
540
|
-
|
541
|
-
if constraint[:range] && !constraint[:range].cover?(value.to_i)
|
542
|
-
raise RubyRoutes::ConstraintViolation, "Value not in allowed range"
|
543
|
-
end
|
159
|
+
to.to_s.split('#', 2).last
|
544
160
|
end
|
545
161
|
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
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
|
550
173
|
end
|
551
174
|
end
|
552
|
-
|
175
|
+
end
|