ruby_routes 2.6.0 → 2.7.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 +24 -1
- data/lib/ruby_routes/cache_setup.rb +32 -0
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -6
- data/lib/ruby_routes/radix_tree/finder.rb +21 -7
- data/lib/ruby_routes/radix_tree/traversal_strategy/unrolled.rb +16 -7
- data/lib/ruby_routes/route/check_helpers.rb +1 -1
- data/lib/ruby_routes/route/constraint_validator.rb +2 -0
- data/lib/ruby_routes/route/small_lru.rb +8 -3
- data/lib/ruby_routes/route/validation_helpers.rb +0 -10
- data/lib/ruby_routes/route.rb +4 -10
- data/lib/ruby_routes/route_set/cache_helpers.rb +2 -19
- data/lib/ruby_routes/route_set/collection_helpers.rb +1 -8
- data/lib/ruby_routes/route_set.rb +2 -0
- data/lib/ruby_routes/segments/dynamic_segment.rb +1 -1
- data/lib/ruby_routes/strategies/hash_based_strategy.rb +2 -2
- data/lib/ruby_routes/utility/path_utility.rb +3 -2
- data/lib/ruby_routes/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0df861aa54b895a91e6d525f227a6c470b2d66e89e39407dded8c017f30b995
|
4
|
+
data.tar.gz: 25d99af8abfd7962e6ac6cf154ed07c2315605192b30db78a7707bf792afd1cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b7f2af4f6bcffd3238e69ade25876abc785c03c601a0a91db06eb75fce14684c73f21abaf6bf2aa55b0c7f496ca682a6c5393426e802d325a740658ee9c1b1b
|
7
|
+
data.tar.gz: e11dbd0208c20b72954f1b750c475740c7678f465d2ab441bbae01a2f87d1415b2d5219ef2fdfada2f728c43e0212918bf3cc23be4ca665f9e8acbde900f6eb5
|
data/README.md
CHANGED
@@ -36,7 +36,7 @@ A high-performance, lightweight routing system for Ruby applications providing a
|
|
36
36
|
Add this line to your application's Gemfile:
|
37
37
|
|
38
38
|
```ruby
|
39
|
-
gem 'ruby_routes', '~> 2.
|
39
|
+
gem 'ruby_routes', '~> 2.7.0'
|
40
40
|
```
|
41
41
|
|
42
42
|
And then execute:
|
@@ -447,6 +447,29 @@ This gem is available as open source under the terms of the [MIT License](LICENS
|
|
447
447
|
|
448
448
|
This gem was inspired by Rails routing and aims to provide a lightweight alternative for Ruby applications that need flexible routing without the full Rails framework.
|
449
449
|
|
450
|
+
## Thread-safe Build (Isolated Builder)
|
451
|
+
|
452
|
+
Use the builder to accumulate routes without mutating a live router:
|
453
|
+
|
454
|
+
```ruby
|
455
|
+
router = RubyRoutes::Router.build do
|
456
|
+
resources :users
|
457
|
+
namespace :admin do
|
458
|
+
resources :posts
|
459
|
+
end
|
460
|
+
end
|
461
|
+
# router is now finalized (immutable)
|
462
|
+
```
|
463
|
+
|
464
|
+
If you need manual steps:
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
builder = RubyRoutes::Router::Builder.new do
|
468
|
+
get '/health', to: 'system#health'
|
469
|
+
end
|
470
|
+
router = builder.build # finalized
|
471
|
+
```
|
472
|
+
|
450
473
|
## Fluent Method Chaining
|
451
474
|
|
452
475
|
For a more concise style, the routing DSL supports method chaining:
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'route/small_lru'
|
4
|
+
require_relative 'constant'
|
5
|
+
|
6
|
+
module RubyRoutes
|
7
|
+
# CacheSetup: shared module for initializing caches across Route and RouteSet.
|
8
|
+
#
|
9
|
+
# This module provides common cache setup methods to reduce duplication
|
10
|
+
# and ensure consistency in cache initialization.
|
11
|
+
module CacheSetup
|
12
|
+
attr_reader :named_routes, :small_lru, :gen_cache, :query_cache, :validation_cache,
|
13
|
+
:cache_hits, :cache_misses
|
14
|
+
|
15
|
+
# Initialize recognition caches for RouteSet.
|
16
|
+
#
|
17
|
+
# @return [void]
|
18
|
+
def setup_caches
|
19
|
+
@routes = []
|
20
|
+
@named_routes = {}
|
21
|
+
@recognition_cache = {}
|
22
|
+
@cache_mutex = Mutex.new
|
23
|
+
@cache_hits = 0
|
24
|
+
@cache_misses = 0
|
25
|
+
@recognition_cache_max = RubyRoutes::Constant::CACHE_SIZE
|
26
|
+
@small_lru = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
|
27
|
+
@gen_cache = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
|
28
|
+
@query_cache = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
|
29
|
+
@validation_cache = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -28,12 +28,7 @@ module RubyRoutes
|
|
28
28
|
# @return [Object] the cached value
|
29
29
|
def call(lru, key)
|
30
30
|
lru.increment_hits
|
31
|
-
|
32
|
-
# to keep strategy decoupled from public API surface.
|
33
|
-
store = lru.hash
|
34
|
-
value = store.delete(key)
|
35
|
-
store[key] = value
|
36
|
-
value
|
31
|
+
lru.promote(key)
|
37
32
|
end
|
38
33
|
end
|
39
34
|
end
|
@@ -24,6 +24,16 @@ module RubyRoutes
|
|
24
24
|
CAPTURED_PARAMS_BUFFER_KEY = :ruby_routes_finder_captured_params_buffer
|
25
25
|
STATE_BUFFER_KEY = :ruby_routes_finder_state_buffer
|
26
26
|
|
27
|
+
# Clears a thread-local buffer hash.
|
28
|
+
#
|
29
|
+
# @param buffer_key [Symbol] The thread-local key for the buffer.
|
30
|
+
# @return [Hash] The cleared buffer.
|
31
|
+
def clear_buffer(buffer_key)
|
32
|
+
buffer = Thread.current[buffer_key] ||= {}
|
33
|
+
buffer.clear
|
34
|
+
buffer
|
35
|
+
end
|
36
|
+
|
27
37
|
# Evaluate constraint rules for a candidate route.
|
28
38
|
#
|
29
39
|
# @param route_handler [Object]
|
@@ -57,10 +67,11 @@ module RubyRoutes
|
|
57
67
|
segments = split_path_cached(path_input)
|
58
68
|
return [nil, EMPTY_PARAMS] if segments.empty?
|
59
69
|
|
60
|
-
#
|
61
|
-
params =
|
62
|
-
|
63
|
-
captured_params =
|
70
|
+
# Clear and acquire thread-local buffers to avoid new hash creation
|
71
|
+
params = clear_buffer(PARAMS_BUFFER_KEY)
|
72
|
+
params.merge!(params_out) if params_out
|
73
|
+
captured_params = clear_buffer(CAPTURED_PARAMS_BUFFER_KEY)
|
74
|
+
state = acquire_state_buffer # Already clears internally
|
64
75
|
|
65
76
|
result = perform_traversal(segments, state, method, params, captured_params)
|
66
77
|
return result unless result.nil?
|
@@ -72,9 +83,12 @@ module RubyRoutes
|
|
72
83
|
#
|
73
84
|
# @return [Hash] state hash with :current, :best_node, :best_params, :best_captured, :matched
|
74
85
|
def acquire_state_buffer
|
75
|
-
state =
|
76
|
-
state.clear
|
86
|
+
state = clear_buffer(STATE_BUFFER_KEY) # Use clear_buffer for consistency
|
77
87
|
state[:current] = @root
|
88
|
+
state[:best_node] = nil
|
89
|
+
state[:best_params] = nil
|
90
|
+
state[:best_captured] = nil
|
91
|
+
state[:matched] = false
|
78
92
|
state
|
79
93
|
end
|
80
94
|
|
@@ -127,7 +141,7 @@ module RubyRoutes
|
|
127
141
|
# @param captured_params [Hash] captured parameters from traversal
|
128
142
|
def record_candidate(state, _method, params, captured_params)
|
129
143
|
state[:best_node] = state[:current]
|
130
|
-
state[:best_params] = params
|
144
|
+
state[:best_params] = params.dup
|
131
145
|
state[:best_captured] = captured_params.dup
|
132
146
|
end
|
133
147
|
|
@@ -14,14 +14,23 @@ module RubyRoutes
|
|
14
14
|
# this strategy is only used for these lengths.
|
15
15
|
case segments.size
|
16
16
|
when 1
|
17
|
-
traverse_segment(0, segments, state, method, params, captured_params)
|
17
|
+
outcome = traverse_segment(0, segments, state, method, params, captured_params)
|
18
|
+
return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
|
18
19
|
when 2
|
19
|
-
traverse_segment(0, segments, state, method, params, captured_params)
|
20
|
-
|
20
|
+
outcome = traverse_segment(0, segments, state, method, params, captured_params)
|
21
|
+
return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
|
22
|
+
return nil if outcome == true # stop
|
23
|
+
outcome = traverse_segment(1, segments, state, method, params, captured_params)
|
24
|
+
return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
|
21
25
|
when 3
|
22
|
-
traverse_segment(0, segments, state, method, params, captured_params)
|
23
|
-
|
24
|
-
|
26
|
+
outcome = traverse_segment(0, segments, state, method, params, captured_params)
|
27
|
+
return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
|
28
|
+
return nil if outcome == true # stop
|
29
|
+
outcome = traverse_segment(1, segments, state, method, params, captured_params)
|
30
|
+
return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
|
31
|
+
return nil if outcome == true # stop
|
32
|
+
outcome = traverse_segment(2, segments, state, method, params, captured_params)
|
33
|
+
return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
|
25
34
|
end
|
26
35
|
nil # Return nil to indicate successful traversal
|
27
36
|
end
|
@@ -32,7 +41,7 @@ module RubyRoutes
|
|
32
41
|
# Returns true if traversal should stop (e.g., due to wildcard), false otherwise.
|
33
42
|
def traverse_segment(index, segments, state, method, params, captured_params)
|
34
43
|
next_node, stop = @finder.traverse_for_segment(state[:current], segments[index], index, segments, params, captured_params)
|
35
|
-
return
|
44
|
+
return :fail unless next_node
|
36
45
|
|
37
46
|
state[:current] = next_node
|
38
47
|
state[:matched] = true
|
@@ -52,7 +52,7 @@ module RubyRoutes
|
|
52
52
|
# @raise [RubyRoutes::ConstraintViolation] If the value does not match the required format.
|
53
53
|
# @return [void]
|
54
54
|
def check_format(constraint, value)
|
55
|
-
return unless (format = constraint[:format]) && !value
|
55
|
+
return unless (format = constraint[:format]) && value && !value.match?(format)
|
56
56
|
|
57
57
|
raise RubyRoutes::ConstraintViolation, 'Value does not match required format'
|
58
58
|
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'timeout'
|
4
4
|
require_relative '../constant'
|
5
5
|
require_relative 'warning_helpers'
|
6
|
+
require_relative 'check_helpers'
|
6
7
|
|
7
8
|
module RubyRoutes
|
8
9
|
class Route
|
@@ -14,6 +15,7 @@ module RubyRoutes
|
|
14
15
|
# for constraint violations.
|
15
16
|
module ConstraintValidator
|
16
17
|
include RubyRoutes::Route::WarningHelpers
|
18
|
+
include RubyRoutes::Route::CheckHelpers
|
17
19
|
# Validate all constraints for the given parameters.
|
18
20
|
#
|
19
21
|
# This method iterates through all constraints and validates each parameter
|
@@ -152,11 +152,16 @@ module RubyRoutes
|
|
152
152
|
end
|
153
153
|
|
154
154
|
# Internal helper used by hit strategy to promote key.
|
155
|
-
#
|
156
|
-
#
|
155
|
+
# Moves the key-value pair to the end of the hash (most recently used position).
|
156
|
+
#
|
157
|
+
# @param key [Object] The key to promote
|
158
|
+
# @return [Object, nil] The value associated with the key, or nil if not found
|
157
159
|
def promote(key)
|
160
|
+
return nil unless @hash.key?(key)
|
161
|
+
|
158
162
|
val = @hash.delete(key)
|
159
|
-
@hash[key] = val
|
163
|
+
@hash[key] = val
|
164
|
+
val
|
160
165
|
end
|
161
166
|
end
|
162
167
|
end
|
@@ -12,16 +12,6 @@ module RubyRoutes
|
|
12
12
|
module ValidationHelpers
|
13
13
|
include RubyRoutes::Route::CheckHelpers
|
14
14
|
|
15
|
-
# Initialize validation result cache.
|
16
|
-
#
|
17
|
-
# This method initializes an LRU (Least Recently Used) cache for storing
|
18
|
-
# validation results, with a maximum size of 64 entries.
|
19
|
-
#
|
20
|
-
# @return [void]
|
21
|
-
def initialize_validation_cache
|
22
|
-
@validation_cache = SmallLru.new(64)
|
23
|
-
end
|
24
|
-
|
25
15
|
# Validate fundamental route shape.
|
26
16
|
#
|
27
17
|
# This method ensures that the route has a valid controller, action, and
|
data/lib/ruby_routes/route.rb
CHANGED
@@ -6,6 +6,8 @@ require 'rack'
|
|
6
6
|
require 'set'
|
7
7
|
require_relative 'constant'
|
8
8
|
require_relative 'node'
|
9
|
+
require_relative 'cache_setup'
|
10
|
+
require_relative 'route_set/cache_helpers'
|
9
11
|
require_relative 'route/small_lru'
|
10
12
|
require_relative 'utility/key_builder_utility'
|
11
13
|
require_relative 'utility/method_utility'
|
@@ -20,7 +22,6 @@ require_relative 'route/query_helpers'
|
|
20
22
|
require_relative 'route/validation_helpers'
|
21
23
|
require_relative 'route/segment_compiler'
|
22
24
|
require_relative 'route/path_generation'
|
23
|
-
require_relative 'route_set/cache_helpers'
|
24
25
|
|
25
26
|
module RubyRoutes
|
26
27
|
# Route
|
@@ -63,6 +64,7 @@ module RubyRoutes
|
|
63
64
|
include RubyRoutes::Utility::PathUtility
|
64
65
|
include RubyRoutes::Utility::KeyBuilderUtility
|
65
66
|
include RubyRoutes::RouteSet::CacheHelpers
|
67
|
+
include RubyRoutes::CacheSetup
|
66
68
|
|
67
69
|
attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
|
68
70
|
|
@@ -123,14 +125,6 @@ module RubyRoutes
|
|
123
125
|
|
124
126
|
private
|
125
127
|
|
126
|
-
# Split path into parts.
|
127
|
-
#
|
128
|
-
# @param path [String] The path to split.
|
129
|
-
# @return [Array<String>]
|
130
|
-
def split_path(path)
|
131
|
-
path.split('/').reject(&:empty?)
|
132
|
-
end
|
133
|
-
|
134
128
|
# Expose for testing / external callers.
|
135
129
|
public :extract_path_params_fast
|
136
130
|
|
@@ -149,7 +143,7 @@ module RubyRoutes
|
|
149
143
|
|
150
144
|
@is_resource = @path.match?(%r{/:id(?:$|\.)})
|
151
145
|
|
152
|
-
|
146
|
+
setup_caches
|
153
147
|
compile_segments
|
154
148
|
compile_required_params
|
155
149
|
check_static_path
|
@@ -11,7 +11,7 @@ module RubyRoutes
|
|
11
11
|
# implementing eviction policies for route recognition.
|
12
12
|
module CacheHelpers
|
13
13
|
|
14
|
-
attr_reader :
|
14
|
+
attr_reader :small_lru
|
15
15
|
# Recognition cache statistics.
|
16
16
|
#
|
17
17
|
# @return [Hash] A hash containing:
|
@@ -31,23 +31,6 @@ module RubyRoutes
|
|
31
31
|
|
32
32
|
private
|
33
33
|
|
34
|
-
# Set up caches and request-key ring.
|
35
|
-
#
|
36
|
-
# Initializes the internal data structures for managing routes, named routes,
|
37
|
-
# recognition cache, and request-key ring buffer.
|
38
|
-
#
|
39
|
-
# @return [void]
|
40
|
-
def setup_caches
|
41
|
-
@routes = []
|
42
|
-
@named_routes = {}
|
43
|
-
@recognition_cache = {}
|
44
|
-
@recognition_cache_max = RubyRoutes::Constant::CACHE_SIZE
|
45
|
-
@small_lru = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
|
46
|
-
@gen_cache = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
|
47
|
-
@query_cache = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
|
48
|
-
@cache_mutex = Mutex.new
|
49
|
-
end
|
50
|
-
|
51
34
|
# Fetch cached recognition entry while updating hit counter.
|
52
35
|
#
|
53
36
|
# @param lookup_key [String] The cache lookup key.
|
@@ -73,7 +56,7 @@ module RubyRoutes
|
|
73
56
|
@cache_mutex.synchronize do
|
74
57
|
if @recognition_cache.size >= @recognition_cache_max
|
75
58
|
# Calculate how many to keep (3/4 of max, rounded down)
|
76
|
-
keep_count =
|
59
|
+
keep_count = @recognition_cache_max / 4
|
77
60
|
|
78
61
|
# Get the keys to keep (newest 75%, assuming insertion order)
|
79
62
|
keys_to_keep = @recognition_cache.keys.last(keep_count)
|
@@ -74,14 +74,7 @@ module RubyRoutes
|
|
74
74
|
#
|
75
75
|
# @return [void]
|
76
76
|
def clear_routes_and_caches!
|
77
|
-
|
78
|
-
@routes.clear
|
79
|
-
@named_routes.clear
|
80
|
-
@recognition_cache.clear
|
81
|
-
@small_lru.clear_counters!
|
82
|
-
@strategy = @strategy_class.new
|
83
|
-
RubyRoutes::Utility::KeyBuilderUtility.clear!
|
84
|
-
end
|
77
|
+
setup_caches
|
85
78
|
end
|
86
79
|
|
87
80
|
# Get the number of routes.
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'cache_setup'
|
3
4
|
require_relative 'strategies'
|
4
5
|
require_relative 'utility/key_builder_utility'
|
5
6
|
require_relative 'utility/method_utility'
|
@@ -36,6 +37,7 @@ module RubyRoutes
|
|
36
37
|
include RubyRoutes::Route::ParamSupport
|
37
38
|
include RubyRoutes::Route::PathGeneration
|
38
39
|
include RubyRoutes::RouteSet::CollectionHelpers
|
40
|
+
include RubyRoutes::CacheSetup
|
39
41
|
|
40
42
|
# Initialize empty collection and caches.
|
41
43
|
#
|
@@ -35,7 +35,7 @@ module RubyRoutes
|
|
35
35
|
def ensure_child(parent_node)
|
36
36
|
parent_node.dynamic_child ||= Node.new
|
37
37
|
dynamic_child_node = parent_node.dynamic_child
|
38
|
-
dynamic_child_node.param_name
|
38
|
+
dynamic_child_node.param_name ||= @param_name
|
39
39
|
dynamic_child_node
|
40
40
|
end
|
41
41
|
|
@@ -16,13 +16,13 @@ module RubyRoutes
|
|
16
16
|
|
17
17
|
def add(route)
|
18
18
|
route.methods.each do |method|
|
19
|
-
key = "#{method
|
19
|
+
key = "#{normalize_http_method(method)}::#{route.path}"
|
20
20
|
@routes[key] = route
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
24
|
def find(path, http_method)
|
25
|
-
key = "#{
|
25
|
+
key = "#{normalize_http_method(method)}::#{route.path}"
|
26
26
|
route = @routes[key]
|
27
27
|
return nil unless route
|
28
28
|
|
@@ -32,9 +32,10 @@ module RubyRoutes
|
|
32
32
|
# normalize_path('/users') # => "/users"
|
33
33
|
# normalize_path('/') # => "/"
|
34
34
|
def normalize_path(raw_path)
|
35
|
-
|
35
|
+
path_string = raw_path.to_s
|
36
|
+
return '/' if path_string.empty?
|
36
37
|
|
37
|
-
path =
|
38
|
+
path = path_string.start_with?('/') ? path_string : "/#{path_string}"
|
38
39
|
path = path.chomp('/') unless path == '/'
|
39
40
|
path
|
40
41
|
end
|
data/lib/ruby_routes/version.rb
CHANGED
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: 2.
|
4
|
+
version: 2.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yosef Benny Widyokarsono
|
@@ -76,6 +76,7 @@ files:
|
|
76
76
|
- LICENSE
|
77
77
|
- README.md
|
78
78
|
- lib/ruby_routes.rb
|
79
|
+
- lib/ruby_routes/cache_setup.rb
|
79
80
|
- lib/ruby_routes/constant.rb
|
80
81
|
- lib/ruby_routes/lru_strategies/hit_strategy.rb
|
81
82
|
- lib/ruby_routes/lru_strategies/miss_strategy.rb
|