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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33b60562a702b7d2def6960af7e255d460b17e508367563aefd9ef85856abe52
4
- data.tar.gz: be3560f98947dd313afeef38603a9f97bb6a39917d471db9a26b4dcdbb42d2bc
3
+ metadata.gz: e0df861aa54b895a91e6d525f227a6c470b2d66e89e39407dded8c017f30b995
4
+ data.tar.gz: 25d99af8abfd7962e6ac6cf154ed07c2315605192b30db78a7707bf792afd1cc
5
5
  SHA512:
6
- metadata.gz: 87b6d61fda5213a0db9e5fba0ad35a3158341e3df9af8e708c38aee60524c3b1c4d2ca85051a4578c3caa57dcfc5fc01d3c37c833652fcb69bd23b6c35580a2e
7
- data.tar.gz: 2378650e65113fc8d29c2062dbfb85127629f18d7f82a4dc00a98f5453b9c933c3fce20cbcc27e6a25b9d07313fec4c03778015ab0a55cf2e55abd4fb251029b
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.3.0'
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
- # Internal storage name (@hash) is intentionally accessed reflectively
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
- # Use thread-local, reusable hashes to avoid allocations
61
- params = acquire_params_buffer(params_out)
62
- state = acquire_state_buffer
63
- captured_params = acquire_captured_params_buffer
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 = Thread.current[STATE_BUFFER_KEY] ||= {}
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
- traverse_segment(1, segments, state, method, params, captured_params)
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
- traverse_segment(1, segments, state, method, params, captured_params)
24
- traverse_segment(2, segments, state, method, params, captured_params)
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 false unless next_node
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&.match?(format)
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
- # @param key [Object]
156
- # @return [void]
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 if 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
@@ -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
- initialize_validation_cache
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 :named_routes, :small_lru
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 = (@recognition_cache_max * 3 / 4).to_i
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
- @cache_mutex.synchronize do
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 = @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.upcase}::#{route.path&.downcase}"
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 = "#{http_method.upcase}::#{path&.downcase}"
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
- return '/' if raw_path.nil? || raw_path.empty?
35
+ path_string = raw_path.to_s
36
+ return '/' if path_string.empty?
36
37
 
37
- path = raw_path.start_with?('/') ? raw_path : "/#{raw_path}"
38
+ path = path_string.start_with?('/') ? path_string : "/#{path_string}"
38
39
  path = path.chomp('/') unless path == '/'
39
40
  path
40
41
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyRoutes
4
- VERSION = '2.6.0'
4
+ VERSION = '2.7.0'
5
5
  end
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.6.0
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