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 +4 -4
- data/README.md +110 -6
- data/lib/ruby_routes/constant.rb +3 -3
- data/lib/ruby_routes/route.rb +181 -55
- data/lib/ruby_routes/route_set.rb +18 -32
- data/lib/ruby_routes/router.rb +11 -11
- data/lib/ruby_routes/url_helpers.rb +25 -6
- data/lib/ruby_routes/version.rb +1 -1
- data/lib/ruby_routes.rb +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4017abab11dd0945bcfe1659b2ed1456c2cf82a8993db55d094d283c48c8ebd0
|
4
|
+
data.tar.gz: 07f0aba728bf81cdfdc245e20a212f1775b2314a826c88f0e25b8ff2b608c195
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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**:
|
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
|
-
###
|
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
|
-
|
124
|
-
|
125
|
-
|
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
|
-
##
|
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
|
data/lib/ruby_routes/constant.rb
CHANGED
@@ -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)
|
data/lib/ruby_routes/route.rb
CHANGED
@@ -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
|
-
#
|
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
|
103
|
-
when :post then
|
104
|
-
when :put then
|
105
|
-
when :patch then
|
106
|
-
when :delete then
|
107
|
-
when :head then
|
108
|
-
when :options then
|
109
|
-
else method.to_s.upcase
|
110
|
-
end
|
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
|
-
|
182
|
-
|
183
|
-
|
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
|
-
|
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
|
-
#
|
236
|
-
params.
|
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
|
-
|
249
|
-
@cache_key_buffer ||= String.new(capacity: 128)
|
250
|
-
@cache_key_buffer.clear
|
294
|
+
return '' if @required_params.empty?
|
251
295
|
|
252
|
-
|
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
|
-
|
299
|
+
value.is_a?(Array) ? value.join('/') : value.to_s
|
258
300
|
end
|
259
|
-
|
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
|
-
|
267
|
-
|
268
|
-
|
269
|
-
@compiled_segments.
|
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
|
-
|
314
|
+
parts << seg[:value]
|
275
315
|
when :param
|
276
316
|
value = merged.fetch(seg[:name]).to_s
|
277
|
-
|
317
|
+
parts << encode_segment_fast(value)
|
278
318
|
when :splat
|
279
319
|
value = merged.fetch(seg[:name], '')
|
280
|
-
|
320
|
+
parts << format_splat_value(value)
|
281
321
|
end
|
282
322
|
end
|
283
323
|
|
284
|
-
|
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
|
329
|
+
def format_splat_value(value)
|
288
330
|
case value
|
289
331
|
when Array
|
290
|
-
value.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
418
|
+
value_str = value.to_s
|
419
|
+
raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A\d+\z/)
|
360
420
|
when :uuid
|
361
|
-
|
362
|
-
|
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 (
|
45
|
+
# Cache hit: return immediately (cached result includes full structure)
|
46
|
+
if (cached_result = @recognition_cache[cache_key])
|
47
47
|
@cache_hits += 1
|
48
|
-
|
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
|
68
|
+
# Create return hash and cache the complete result
|
75
69
|
result_params = params.dup
|
76
|
-
|
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
|
153
|
-
|
154
|
-
|
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
|
162
|
-
|
163
|
-
|
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
|
-
|
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
|
data/lib/ruby_routes/router.rb
CHANGED
@@ -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: "#{
|
43
|
-
get "/#{plural}/new", options.merge(to: "#{
|
44
|
-
post "/#{plural}", options.merge(to: "#{
|
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: "#{
|
48
|
-
get "/#{plural}/:id/edit", options.merge(to: "#{
|
49
|
-
put "/#{plural}/:id", options.merge(to: "#{
|
50
|
-
patch "/#{plural}/:id", options.merge(to: "#{
|
51
|
-
delete "/#{plural}/:id", options.merge(to: "#{
|
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
|
-
|
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
|
-
|
41
|
-
method =
|
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
|
-
|
44
|
-
html += "<
|
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
|
data/lib/ruby_routes/version.rb
CHANGED
data/lib/ruby_routes.rb
CHANGED