ruby_routes 1.1.0 → 2.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: 15debcef313430cfc799afcb9ba0b6f9bd8292226d023d7e854afc608d5ede64
4
- data.tar.gz: 1d7c971980a984738c6239cc1727376b74ab61ae824ee054a92f254451574bcf
3
+ metadata.gz: 4017abab11dd0945bcfe1659b2ed1456c2cf82a8993db55d094d283c48c8ebd0
4
+ data.tar.gz: 07f0aba728bf81cdfdc245e20a212f1775b2314a826c88f0e25b8ff2b608c195
5
5
  SHA512:
6
- metadata.gz: 8f272672f91127b65fab8ed4ae2eacfb95fe55310fc4bcd017b5656a0eb475e05255884f19416520ce8b36ec6ff51602561b6f9519befc1f7cf7448bca4ab3f9
7
- data.tar.gz: c3cc386c128531ef9bb268bb904c806e1632c3d88609fd3de5077d8143f0fa56a1089ea0915de3bf2e1116c0d0d812402877480d6edc702f8923d3ee3252ef9a
6
+ metadata.gz: a4aa77ba441b9e87cdcb31cae5b5f12babaf93396e400723d5f845392ac132520c4398e19d49d0755e05fc305791b54a3676a460ba8c73cd4b5ee55647fb2589
7
+ data.tar.gz: a3b883c253731dfad5eaf34a28fb89684d0f9fa921127f6e428225743a93d87934508e49f59c3710964cc8a444b729f9278872aa8d991c4302abbb7ff8645b66
data/README.md CHANGED
@@ -8,7 +8,7 @@ A lightweight, flexible routing system for Ruby that provides a Rails-like DSL f
8
8
  - **HTTP Method Support**: GET, POST, PUT, PATCH, DELETE, and custom methods
9
9
  - **RESTful Resources**: Automatic generation of RESTful routes
10
10
  - **Nested Routes**: Support for nested resources and namespaces
11
- - **Route Constraints**: Add constraints to routes (regex, etc.)
11
+ - **Secure Route Constraints**: Powerful constraint system with built-in security ([see CONSTRAINTS.md](CONSTRAINTS.md))
12
12
  - **Named Routes**: Generate URLs from route names
13
13
  - **Path Generation**: Build URLs with parameters
14
14
  - **Scope Support**: Group routes with common options
@@ -116,14 +116,91 @@ end
116
116
  # etc.
117
117
  ```
118
118
 
119
- ### Scopes and Constraints
119
+ ### Route Constraints
120
+
121
+ Ruby Routes provides a powerful and secure constraint system to validate route parameters. **For security reasons, Proc constraints are deprecated** - use the secure alternatives below.
122
+
123
+ #### Built-in Constraint Types
120
124
 
121
125
  ```ruby
122
126
  router = RubyRoutes.draw do
123
- scope constraints: { id: /\d+/ } do
124
- get '/users/:id', to: 'users#show'
125
- end
127
+ # Integer validation
128
+ get '/users/:id', to: 'users#show', constraints: { id: :int }
129
+
130
+ # UUID validation
131
+ get '/resources/:uuid', to: 'resources#show', constraints: { uuid: :uuid }
132
+
133
+ # Email validation
134
+ get '/users/:email', to: 'users#show', constraints: { email: :email }
126
135
 
136
+ # URL-friendly slug validation
137
+ get '/posts/:slug', to: 'posts#show', constraints: { slug: :slug }
138
+
139
+ # Alphabetic characters only
140
+ get '/categories/:name', to: 'categories#show', constraints: { name: :alpha }
141
+
142
+ # Alphanumeric characters only
143
+ get '/codes/:code', to: 'codes#show', constraints: { code: :alphanumeric }
144
+ end
145
+ ```
146
+
147
+ #### Hash-based Constraints (Recommended)
148
+
149
+ ```ruby
150
+ router = RubyRoutes.draw do
151
+ # Length constraints
152
+ get '/users/:username', to: 'users#show',
153
+ constraints: {
154
+ username: {
155
+ min_length: 3,
156
+ max_length: 20,
157
+ format: /\A[a-zA-Z0-9_]+\z/
158
+ }
159
+ }
160
+
161
+ # Allowed values (whitelist)
162
+ get '/posts/:status', to: 'posts#show',
163
+ constraints: {
164
+ status: { in: %w[draft published archived] }
165
+ }
166
+
167
+ # Numeric ranges
168
+ get '/products/:price', to: 'products#show',
169
+ constraints: {
170
+ price: { range: 1..10000 }
171
+ }
172
+ end
173
+ ```
174
+
175
+ #### Regular Expression Constraints
176
+
177
+ ```ruby
178
+ router = RubyRoutes.draw do
179
+ # Custom regex pattern (with ReDoS protection)
180
+ get '/products/:sku', to: 'products#show',
181
+ constraints: { sku: /\A[A-Z]{2}\d{4}\z/ }
182
+ end
183
+ ```
184
+
185
+ #### ⚠️ Security Notice: Proc Constraints Deprecated
186
+
187
+ ```ruby
188
+ # ❌ DEPRECATED - Security risk!
189
+ get '/users/:id', to: 'users#show',
190
+ constraints: { id: ->(value) { value.to_i > 0 } }
191
+
192
+ # ✅ Use secure alternatives instead:
193
+ get '/users/:id', to: 'users#show',
194
+ constraints: { id: { range: 1..Float::INFINITY } }
195
+ ```
196
+
197
+ **📚 For complete constraint documentation, see [CONSTRAINTS.md](CONSTRAINTS.md)**
198
+ **🔄 For migration help, see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md)**
199
+
200
+ ### Scopes
201
+
202
+ ```ruby
203
+ router = RubyRoutes.draw do
127
204
  scope defaults: { format: 'html' } do
128
205
  get '/posts', to: 'posts#index'
129
206
  end
@@ -273,13 +350,38 @@ Creates a new router instance and yields to the block for route definition.
273
350
  - `find_route(method, path)` - Finds a specific route
274
351
  - `find_named_route(name)` - Finds a named route
275
352
 
276
- ## Examples
353
+ ## Documentation
354
+
355
+ ### Core Documentation
356
+ - **[CONSTRAINTS.md](CONSTRAINTS.md)** - Complete guide to route constraints and security best practices
357
+ - **[MIGRATION_GUIDE.md](MIGRATION_GUIDE.md)** - Step-by-step guide for migrating from deprecated Proc constraints
358
+
359
+ ### Examples
277
360
 
278
361
  See the `examples/` directory for more detailed examples:
279
362
 
280
363
  - `examples/basic_usage.rb` - Basic routing examples
281
364
  - `examples/rack_integration.rb` - Full Rack application example
282
365
 
366
+ ## Security
367
+
368
+ Ruby Routes prioritizes security and has implemented several protections:
369
+
370
+ ### 🔒 Security Features
371
+ - **XSS Protection**: All HTML output is properly escaped
372
+ - **ReDoS Protection**: Regular expression constraints have timeout protection
373
+ - **Secure Constraints**: Deprecated dangerous Proc constraints in favor of secure alternatives
374
+ - **Thread Safety**: All caching and shared resources are thread-safe
375
+ - **Input Validation**: Comprehensive parameter validation before reaching application code
376
+
377
+ ### ⚠️ Important Security Notice
378
+ **Proc constraints are deprecated due to security risks** and will be removed in a future version. They allow arbitrary code execution which can be exploited for:
379
+ - Code injection attacks
380
+ - Denial of service attacks
381
+ - System compromise
382
+
383
+ **Migration Required**: If you're using Proc constraints, please migrate to secure alternatives using our [Migration Guide](MIGRATION_GUIDE.md).
384
+
283
385
  ## Testing
284
386
 
285
387
  Run the test suite:
@@ -288,6 +390,8 @@ Run the test suite:
288
390
  bundle exec rspec
289
391
  ```
290
392
 
393
+ The test suite includes comprehensive security tests to ensure all protections are working correctly.
394
+
291
395
  ## Contributing
292
396
 
293
397
  1. Fork the repository
@@ -42,9 +42,9 @@ module RubyRoutes
42
42
 
43
43
  # Descriptor factories for segment classification (O(1) dispatch by first byte).
44
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 } }
45
+ 42 => ->(s) { { type: :splat, name: (s[1..-1] || 'splat').freeze } }, # '*'
46
+ 58 => ->(s) { { type: :param, name: s[1..-1].freeze } }, # ':'
47
+ :default => ->(s) { { type: :static, value: s.freeze } } # Intern static values
48
48
  }.freeze
49
49
 
50
50
  def self.segment_descriptor(raw)
@@ -1,4 +1,6 @@
1
1
  require 'uri'
2
+ require 'timeout'
3
+ require 'set'
2
4
  require_relative 'route/small_lru'
3
5
 
4
6
  module RubyRoutes
@@ -71,6 +73,12 @@ module RubyRoutes
71
73
  raise RubyRoutes::RouteNotFound, "Missing params: #{missing_params.to_a.join(', ')}"
72
74
  end
73
75
 
76
+ # Check for nil values in required params
77
+ nil_params = @required_params_set.select { |param| merged[param].nil? }
78
+ unless nil_params.empty?
79
+ raise RubyRoutes::RouteNotFound, "Missing or nil params: #{nil_params.to_a.join(', ')}"
80
+ end
81
+
74
82
  # Cache lookup
75
83
  cache_key = build_cache_key_fast(merged)
76
84
  if (cached = @gen_cache.get(cache_key))
@@ -95,19 +103,28 @@ module RubyRoutes
95
103
  ROOT_PATH = '/'.freeze
96
104
  UNRESERVED_RE = /\A[a-zA-Z0-9\-._~]+\z/.freeze
97
105
  QUERY_CACHE_SIZE = 128
98
-
99
- # Fast method normalization
106
+
107
+ # Common HTTP methods - interned for performance
108
+ HTTP_GET = 'GET'.freeze
109
+ HTTP_POST = 'POST'.freeze
110
+ HTTP_PUT = 'PUT'.freeze
111
+ HTTP_PATCH = 'PATCH'.freeze
112
+ HTTP_DELETE = 'DELETE'.freeze
113
+ HTTP_HEAD = 'HEAD'.freeze
114
+ HTTP_OPTIONS = 'OPTIONS'.freeze
115
+
116
+ # Fast method normalization using interned constants
100
117
  def normalize_method(method)
101
118
  case method
102
- when :get then 'GET'
103
- when :post then 'POST'
104
- when :put then 'PUT'
105
- when :patch then 'PATCH'
106
- when :delete then 'DELETE'
107
- when :head then 'HEAD'
108
- when :options then 'OPTIONS'
109
- else method.to_s.upcase
110
- end.freeze
119
+ when :get then HTTP_GET
120
+ when :post then HTTP_POST
121
+ when :put then HTTP_PUT
122
+ when :patch then HTTP_PATCH
123
+ when :delete then HTTP_DELETE
124
+ when :head then HTTP_HEAD
125
+ when :options then HTTP_OPTIONS
126
+ else method.to_s.upcase.freeze
127
+ end
111
128
  end
112
129
 
113
130
  # Pre-compile all route data at initialization
@@ -178,9 +195,20 @@ module RubyRoutes
178
195
  end
179
196
 
180
197
  def get_thread_local_hash
181
- hash = Thread.current[:ruby_routes_params] ||= {}
182
- hash.clear
183
- hash
198
+ # Use a pool of hashes to reduce allocations
199
+ pool = Thread.current[:ruby_routes_hash_pool] ||= []
200
+ if pool.empty?
201
+ {}
202
+ else
203
+ hash = pool.pop
204
+ hash.clear
205
+ hash
206
+ end
207
+ end
208
+
209
+ def return_hash_to_pool(hash)
210
+ pool = Thread.current[:ruby_routes_hash_pool] ||= []
211
+ pool.push(hash) if pool.size < 5 # Keep pool small to avoid memory bloat
184
212
  end
185
213
 
186
214
  def merge_defaults_fast(result)
@@ -194,7 +222,17 @@ module RubyRoutes
194
222
 
195
223
  # Fast path normalization
196
224
  path_parts = split_path_fast(request_path)
197
- return nil if @compiled_segments.size != path_parts.size
225
+
226
+ # Check if we have a wildcard/splat segment
227
+ has_splat = @compiled_segments.any? { |seg| seg[:type] == :splat }
228
+
229
+ if has_splat
230
+ # For wildcard routes, path can have more parts than segments
231
+ return nil if path_parts.size < @compiled_segments.size - 1
232
+ else
233
+ # For non-wildcard routes, size must match exactly
234
+ return nil if @compiled_segments.size != path_parts.size
235
+ end
198
236
 
199
237
  extract_params_from_parts(path_parts)
200
238
  end
@@ -230,10 +268,18 @@ module RubyRoutes
230
268
  return @defaults if params.empty?
231
269
 
232
270
  merged = get_thread_local_merged_hash
271
+
272
+ # Merge defaults first if they exist
233
273
  merged.update(@defaults) unless @defaults.empty?
234
274
 
235
- # Convert param keys to strings efficiently
236
- params.each { |k, v| merged[k.to_s] = v }
275
+ # Use merge! with transform_keys for better performance
276
+ if params.respond_to?(:transform_keys)
277
+ merged.merge!(params.transform_keys(&:to_s))
278
+ else
279
+ # Fallback for older Ruby versions
280
+ params.each { |k, v| merged[k.to_s] = v }
281
+ end
282
+
237
283
  merged
238
284
  end
239
285
 
@@ -245,67 +291,59 @@ module RubyRoutes
245
291
 
246
292
  # Fast cache key building with minimal allocations
247
293
  def build_cache_key_fast(merged)
248
- # Use instance variable buffer to avoid repeated allocations
249
- @cache_key_buffer ||= String.new(capacity: 128)
250
- @cache_key_buffer.clear
294
+ return '' if @required_params.empty?
251
295
 
252
- return @cache_key_buffer.dup if @required_params.empty?
253
-
254
- @required_params.each_with_index do |name, idx|
255
- @cache_key_buffer << '|' unless idx.zero?
296
+ # Use array join which is faster than string concatenation
297
+ parts = @required_params.map do |name|
256
298
  value = merged[name]
257
- @cache_key_buffer << (value.is_a?(Array) ? value.join('/') : value.to_s) if value
299
+ value.is_a?(Array) ? value.join('/') : value.to_s
258
300
  end
259
- @cache_key_buffer.dup
301
+ parts.join('|')
260
302
  end
261
303
 
262
304
  # Optimized path generation
263
305
  def generate_path_string(merged)
264
306
  return ROOT_PATH if @compiled_segments.empty?
265
307
 
266
- buffer = String.new(capacity: 128)
267
- buffer << '/'
268
-
269
- @compiled_segments.each_with_index do |seg, idx|
270
- buffer << '/' unless idx.zero?
271
-
308
+ # Pre-allocate array for parts to avoid string buffer operations
309
+ parts = []
310
+
311
+ @compiled_segments.each do |seg|
272
312
  case seg[:type]
273
313
  when :static
274
- buffer << seg[:value]
314
+ parts << seg[:value]
275
315
  when :param
276
316
  value = merged.fetch(seg[:name]).to_s
277
- buffer << encode_segment_fast(value)
317
+ parts << encode_segment_fast(value)
278
318
  when :splat
279
319
  value = merged.fetch(seg[:name], '')
280
- append_splat_value(buffer, value)
320
+ parts << format_splat_value(value)
281
321
  end
282
322
  end
283
323
 
284
- buffer == '/' ? ROOT_PATH : buffer
324
+ # Single join operation is faster than multiple string concatenations
325
+ path = "/#{parts.join('/')}"
326
+ path == '/' ? ROOT_PATH : path
285
327
  end
286
328
 
287
- def append_splat_value(buffer, value)
329
+ def format_splat_value(value)
288
330
  case value
289
331
  when Array
290
- value.each_with_index do |part, idx|
291
- buffer << '/' unless idx.zero?
292
- buffer << encode_segment_fast(part.to_s)
293
- end
332
+ value.map { |part| encode_segment_fast(part.to_s) }.join('/')
294
333
  when String
295
- parts = value.split('/')
296
- parts.each_with_index do |part, idx|
297
- buffer << '/' unless idx.zero?
298
- buffer << encode_segment_fast(part)
299
- end
334
+ value.split('/').map { |part| encode_segment_fast(part) }.join('/')
300
335
  else
301
- buffer << encode_segment_fast(value.to_s)
336
+ encode_segment_fast(value.to_s)
302
337
  end
303
338
  end
304
339
 
305
- # Fast segment encoding
340
+ # Fast segment encoding with caching for common values
306
341
  def encode_segment_fast(str)
307
342
  return str if UNRESERVED_RE.match?(str)
308
- URI.encode_www_form_component(str)
343
+
344
+ # Cache encoded segments to avoid repeated encoding
345
+ @encoding_cache ||= {}
346
+ @encoding_cache[str] ||= URI.encode_www_form_component(str)
309
347
  end
310
348
 
311
349
  # Optimized query params with caching
@@ -348,22 +386,110 @@ module RubyRoutes
348
386
  def validate_constraints_fast!(params)
349
387
  @constraints.each do |param, constraint|
350
388
  value = params[param.to_s]
351
- next unless value
389
+ # Only skip validation if the parameter is completely missing from params
390
+ # Empty strings and nil values should still be validated
391
+ next unless params.key?(param.to_s)
352
392
 
353
393
  case constraint
354
394
  when Regexp
355
- raise ConstraintViolation unless constraint.match?(value)
395
+ # Protect against ReDoS attacks with timeout
396
+ begin
397
+ Timeout.timeout(0.1) do
398
+ raise RubyRoutes::ConstraintViolation unless constraint.match?(value.to_s)
399
+ end
400
+ rescue Timeout::Error
401
+ raise RubyRoutes::ConstraintViolation, "Regex constraint timed out (potential ReDoS attack)"
402
+ end
356
403
  when Proc
357
- raise ConstraintViolation unless constraint.call(value)
404
+ # DEPRECATED: Proc constraints are deprecated due to security risks
405
+ warn_proc_constraint_deprecation(param)
406
+
407
+ # For backward compatibility, still execute but with strict timeout
408
+ begin
409
+ Timeout.timeout(0.05) do # Reduced timeout for security
410
+ raise RubyRoutes::ConstraintViolation unless constraint.call(value.to_s)
411
+ end
412
+ rescue Timeout::Error
413
+ raise RubyRoutes::ConstraintViolation, "Proc constraint timed out (consider using secure alternatives)"
414
+ rescue => e
415
+ raise RubyRoutes::ConstraintViolation, "Proc constraint failed: #{e.message}"
416
+ end
358
417
  when :int
359
- raise ConstraintViolation unless value.match?(/\A\d+\z/)
418
+ value_str = value.to_s
419
+ raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A\d+\z/)
360
420
  when :uuid
361
- raise ConstraintViolation unless value.length == 36 &&
362
- value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
421
+ value_str = value.to_s
422
+ raise RubyRoutes::ConstraintViolation unless value_str.length == 36 &&
423
+ 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)
424
+ when :email
425
+ value_str = value.to_s
426
+ raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
427
+ when :slug
428
+ value_str = value.to_s
429
+ raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
430
+ when :alpha
431
+ value_str = value.to_s
432
+ raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z]+\z/)
433
+ when :alphanumeric
434
+ value_str = value.to_s
435
+ raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z0-9]+\z/)
436
+ when Hash
437
+ # Secure hash-based constraints for common patterns
438
+ validate_hash_constraint!(constraint, value_str = value.to_s)
363
439
  end
364
440
  end
365
441
  end
366
442
 
443
+ def warn_proc_constraint_deprecation(param)
444
+ return if @proc_warnings_shown&.include?(param)
445
+
446
+ @proc_warnings_shown ||= Set.new
447
+ @proc_warnings_shown << param
448
+
449
+ warn <<~WARNING
450
+ [DEPRECATION] Proc constraints are deprecated due to security risks.
451
+
452
+ Parameter: #{param}
453
+ Route: #{@path}
454
+
455
+ Secure alternatives:
456
+ - Use regex: constraints: { #{param}: /\\A\\d+\\z/ }
457
+ - Use built-in types: constraints: { #{param}: :int }
458
+ - Use hash constraints: constraints: { #{param}: { min_length: 3, format: /\\A[a-z]+\\z/ } }
459
+
460
+ Available built-in types: :int, :uuid, :email, :slug, :alpha, :alphanumeric
461
+
462
+ This warning will become an error in a future version.
463
+ WARNING
464
+ end
465
+
466
+ def validate_hash_constraint!(constraint, value)
467
+ # Secure hash-based constraints
468
+ if constraint[:min_length] && value.length < constraint[:min_length]
469
+ raise RubyRoutes::ConstraintViolation, "Value too short (minimum #{constraint[:min_length]} characters)"
470
+ end
471
+
472
+ if constraint[:max_length] && value.length > constraint[:max_length]
473
+ raise RubyRoutes::ConstraintViolation, "Value too long (maximum #{constraint[:max_length]} characters)"
474
+ end
475
+
476
+ if constraint[:format] && !value.match?(constraint[:format])
477
+ raise RubyRoutes::ConstraintViolation, "Value does not match required format"
478
+ end
479
+
480
+ if constraint[:in] && !constraint[:in].include?(value)
481
+ raise RubyRoutes::ConstraintViolation, "Value not in allowed list"
482
+ end
483
+
484
+ if constraint[:not_in] && constraint[:not_in].include?(value)
485
+ raise RubyRoutes::ConstraintViolation, "Value in forbidden list"
486
+ end
487
+
488
+ if constraint[:range] && !constraint[:range].cover?(value.to_i)
489
+ raise RubyRoutes::ConstraintViolation, "Value not in allowed range"
490
+ end
491
+ end
492
+
367
493
  def validate_route!
368
494
  raise InvalidRoute, "Controller is required" if @controller.nil?
369
495
  raise InvalidRoute, "Action is required" if @action.nil?
@@ -42,16 +42,10 @@ module RubyRoutes
42
42
  # Optimized cache key: avoid string interpolation when possible
43
43
  cache_key = build_cache_key(method_up, request_path)
44
44
 
45
- # Cache hit: return immediately
46
- if (cached = @recognition_cache[cache_key])
45
+ # Cache hit: return immediately (cached result includes full structure)
46
+ if (cached_result = @recognition_cache[cache_key])
47
47
  @cache_hits += 1
48
- cached_route, cached_params = cached
49
- return {
50
- route: cached_route,
51
- params: cached_params,
52
- controller: cached_route.controller,
53
- action: cached_route.action
54
- }
48
+ return cached_result
55
49
  end
56
50
 
57
51
  @cache_misses += 1
@@ -71,17 +65,17 @@ module RubyRoutes
71
65
  merge_query_params(route, request_path, params)
72
66
  end
73
67
 
74
- # Create return hash and cache entry
68
+ # Create return hash and cache the complete result
75
69
  result_params = params.dup
76
- cache_entry = [route, result_params.freeze]
77
- insert_cache_entry(cache_key, cache_entry)
78
-
79
- {
70
+ result = {
80
71
  route: route,
81
72
  params: result_params,
82
73
  controller: route.controller,
83
74
  action: route.action
84
- }
75
+ }.freeze
76
+
77
+ insert_cache_entry(cache_key, result)
78
+ result
85
79
  end
86
80
 
87
81
  def recognize_path(path, method = :get)
@@ -141,7 +135,7 @@ module RubyRoutes
141
135
 
142
136
  private
143
137
 
144
- # Method lookup table to avoid repeated upcasing
138
+ # Method lookup table to avoid repeated upcasing with interned strings
145
139
  def method_lookup(method)
146
140
  @method_cache ||= Hash.new { |h, k| h[k] = k.to_s.upcase.freeze }
147
141
  @method_cache[method]
@@ -149,29 +143,21 @@ module RubyRoutes
149
143
 
150
144
  # Optimized cache key building - avoid string interpolation
151
145
  def build_cache_key(method, path)
152
- # Use frozen string concatenation to avoid allocations
153
- @cache_key_buffer ||= String.new(capacity: 256)
154
- @cache_key_buffer.clear
155
- @cache_key_buffer << method << ':' << path
156
- @cache_key_buffer.dup.freeze
146
+ # Use string interpolation which is faster than buffer + dup + freeze
147
+ # String interpolation creates a new string directly without intermediate allocations
148
+ "#{method}:#{path}".freeze
157
149
  end
158
150
 
159
151
  # Get thread-local params hash, reusing when possible
160
152
  def get_thread_local_params
161
- # Use object pool to reduce GC pressure
162
- @params_pool ||= []
163
- if @params_pool.empty?
164
- {}
165
- else
166
- hash = @params_pool.pop
167
- hash.clear
168
- hash
169
- end
153
+ # Use single thread-local hash that gets cleared, avoiding pool management overhead
154
+ hash = Thread.current[:ruby_routes_params_hash] ||= {}
155
+ hash.clear
156
+ hash
170
157
  end
171
158
 
172
159
  def return_params_to_pool(params)
173
- @params_pool ||= []
174
- @params_pool.push(params) if @params_pool.size < 10
160
+ # No-op since we're using a single reusable hash per thread
175
161
  end
176
162
 
177
163
  # Fast defaults merging
@@ -5,6 +5,7 @@ module RubyRoutes
5
5
  def initialize(&block)
6
6
  @route_set = RouteSet.new
7
7
  @scope_stack = []
8
+ @concerns = {}
8
9
  instance_eval(&block) if block_given?
9
10
  end
10
11
 
@@ -36,19 +37,20 @@ module RubyRoutes
36
37
  # Resources routing (Rails-like)
37
38
  def resources(name, options = {}, &block)
38
39
  singular = name.to_s.singularize
39
- plural = name.to_s.pluralize
40
+ plural = (options[:path] || name.to_s.pluralize)
41
+ controller = options[:controller] || plural
40
42
 
41
43
  # Collection routes
42
- get "/#{plural}", options.merge(to: "#{plural}#index")
43
- get "/#{plural}/new", options.merge(to: "#{plural}#new")
44
- post "/#{plural}", options.merge(to: "#{plural}#create")
44
+ get "/#{plural}", options.merge(to: "#{controller}#index")
45
+ get "/#{plural}/new", options.merge(to: "#{controller}#new")
46
+ post "/#{plural}", options.merge(to: "#{controller}#create")
45
47
 
46
48
  # Member routes
47
- get "/#{plural}/:id", options.merge(to: "#{plural}#show")
48
- get "/#{plural}/:id/edit", options.merge(to: "#{plural}#edit")
49
- put "/#{plural}/:id", options.merge(to: "#{plural}#update")
50
- patch "/#{plural}/:id", options.merge(to: "#{plural}#update")
51
- delete "/#{plural}/:id", options.merge(to: "#{plural}#destroy")
49
+ get "/#{plural}/:id", options.merge(to: "#{controller}#show")
50
+ get "/#{plural}/:id/edit", options.merge(to: "#{controller}#edit")
51
+ put "/#{plural}/:id", options.merge(to: "#{controller}#update")
52
+ patch "/#{plural}/:id", options.merge(to: "#{controller}#update")
53
+ delete "/#{plural}/:id", options.merge(to: "#{controller}#destroy")
52
54
 
53
55
  # Nested resources if specified
54
56
  if options[:nested]
@@ -63,7 +65,6 @@ module RubyRoutes
63
65
  get "/#{plural}/:id/#{nested_plural}/:nested_id/edit", options.merge(to: "#{nested_plural}#edit")
64
66
  put "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
65
67
  patch "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
66
- delete "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
67
68
  delete "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#destroy")
68
69
  end
69
70
 
@@ -131,7 +132,6 @@ module RubyRoutes
131
132
  end
132
133
 
133
134
  def concern(name, &block)
134
- @concerns ||= {}
135
135
  @concerns[name] = block
136
136
  end
137
137
 
@@ -1,3 +1,5 @@
1
+ require 'cgi'
2
+
1
3
  module RubyRoutes
2
4
  module UrlHelpers
3
5
  def self.included(base)
@@ -33,16 +35,33 @@ module RubyRoutes
33
35
 
34
36
  def link_to(name, text, params = {})
35
37
  path = path_to(name, params)
36
- "<a href=\"#{path}\">#{text}</a>"
38
+ safe_path = CGI.escapeHTML(path.to_s)
39
+ safe_text = CGI.escapeHTML(text.to_s)
40
+ "<a href=\"#{safe_path}\">#{safe_text}</a>"
37
41
  end
38
42
 
39
43
  def button_to(name, text, params = {})
40
- path = path_to(name, params)
41
- method = params.delete(:method) || :post
44
+ local_params = params ? params.dup : {}
45
+ method = local_params.delete(:method) || :post
46
+ method = method.to_s.downcase
47
+ path = path_to(name, local_params)
48
+
49
+ # HTML forms only support GET and POST
50
+ # For other methods, use POST with _method hidden field
51
+ form_method = (method == 'get') ? 'get' : 'post'
52
+
53
+ safe_path = CGI.escapeHTML(path.to_s)
54
+ safe_form_method = CGI.escapeHTML(form_method)
55
+ html = "<form action=\"#{safe_path}\" method=\"#{safe_form_method}\">"
56
+
57
+ # Add _method hidden field for non-GET/POST methods
58
+ if method != 'get' && method != 'post'
59
+ safe_method = CGI.escapeHTML(method)
60
+ html += "<input type=\"hidden\" name=\"_method\" value=\"#{safe_method}\">"
61
+ end
42
62
 
43
- html = "<form action=\"#{path}\" method=\"#{method}\">"
44
- html += "<input type=\"hidden\" name=\"_method\" value=\"#{method}\">" if method != :get
45
- html += "<button type=\"submit\">#{text}</button>"
63
+ safe_text = CGI.escapeHTML(text.to_s)
64
+ html += "<button type=\"submit\">#{safe_text}</button>"
46
65
  html += "</form>"
47
66
  html
48
67
  end
@@ -1,3 +1,3 @@
1
1
  module RubyRoutes
2
- VERSION = "1.1.0"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/ruby_routes.rb CHANGED
@@ -11,6 +11,7 @@ module RubyRoutes
11
11
  class Error < StandardError; end
12
12
  class RouteNotFound < Error; end
13
13
  class InvalidRoute < Error; end
14
+ class ConstraintViolation < Error; end
14
15
 
15
16
  # Create a new router instance
16
17
  def self.new(&block)
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: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yosef Benny Widyokarsono