ruby_routes 2.4.0 → 2.5.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: 57b9470b49019746492c10fd0e202c6c544c78459f36e0d2c1f8d400103def93
4
- data.tar.gz: 8e59a14f7854cadb955d381fcf7947b7769d0041de5067f3e3dc5827d13cbb26
3
+ metadata.gz: 07fa3c9d3d13ac8b73c6f4775871cd5ad6c3cd7c5d0e571797b83fb0dea72189
4
+ data.tar.gz: 2684722163772fe4489d4e8c32eaff036002c5b58e05be68e25e9a0c59cd680b
5
5
  SHA512:
6
- metadata.gz: c746c95407c222fdd209e464c3f1b89971987d8b41a557b7f173e1acb55fa5d6626b030bab26aed81c7ff766bd9da8c4ba4c2895ce167558a61c089923e14f12
7
- data.tar.gz: 6b512f5e810f19fa4031b33f29d3efa72feabb841e8ac3558af67ce6ec43556cacc85577993015cde7f3f400c10e9a36cbc8e007831808a798fa80204fd57046
6
+ metadata.gz: dfc84b5cfa67c1da89ab8067ad01c8706c81717611969c287a4a536055fe1ee1b4ac80b791248c05dddb090548f1e29006889d2d7f5e91b9058349b9f9a8b317
7
+ data.tar.gz: b2ab1c86ca6838a4164621987c813b7320e3f042d1666576e00c3acf48576204c837fd13582ed42f2cf896334b9e1683c8ceab8868c03b7f9a482dcb808a5052
@@ -107,6 +107,8 @@ module RubyRoutes
107
107
  # @return [Integer]
108
108
  QUERY_CACHE_SIZE = 128
109
109
 
110
+ METHOD_CACHE_MAX_SIZE = 1000
111
+
110
112
  # HTTP method constants.
111
113
  HTTP_GET = 'GET'
112
114
  HTTP_POST = 'POST'
@@ -146,7 +148,7 @@ module RubyRoutes
146
148
  # Default result for no traversal match.
147
149
  #
148
150
  # @return [Array]
149
- NO_TRAVERSAL_RESULT = [nil, false, {}].freeze
151
+ NO_TRAVERSAL_RESULT = [nil, false, EMPTY_HASH].freeze
150
152
 
151
153
  # Built-in validators for constraints.
152
154
  #
@@ -171,7 +173,7 @@ module RubyRoutes
171
173
  segment_string = raw.to_s
172
174
  dispatch_key = segment_string.empty? ? :default : segment_string.getbyte(0)
173
175
  factory = DESCRIPTOR_FACTORIES[dispatch_key] || DESCRIPTOR_FACTORIES[:default]
174
- factory.call(segment_string)
176
+ factory.call(segment_string).freeze
175
177
  end
176
178
  end
177
179
  end
@@ -3,6 +3,7 @@
3
3
  require_relative 'segment'
4
4
  require_relative 'utility/path_utility'
5
5
  require_relative 'utility/method_utility'
6
+ require_relative 'constant'
6
7
 
7
8
  module RubyRoutes
8
9
  # Node
@@ -96,7 +96,10 @@ module RubyRoutes
96
96
  # @return [Array] [next_node, stop_traversal, segment_captured]
97
97
  def traverse_for_segment(node, segment, index, segments, params, captured_params)
98
98
  next_node, stop, segment_captured = node.traverse_for(segment, index, segments, params)
99
- captured_params.merge!(segment_captured) if segment_captured
99
+ if segment_captured
100
+ params.merge!(segment_captured) # Merge into running params hash at each step
101
+ captured_params.merge!(segment_captured) # Keep for best candidate consistency
102
+ end
100
103
  [next_node, stop]
101
104
  end
102
105
 
@@ -162,9 +165,7 @@ module RubyRoutes
162
165
  # @param params [Hash] parameters hash
163
166
  # @param captured_params [Hash] captured parameters from traversal
164
167
  # @return [Array] [handler, params] or [nil, params]
165
- def fallback_candidate(state, method, params, captured_params)
166
- finalize_match(state[:best_node], method, state[:best_params], state[:best_captured])
167
- end
168
+
168
169
 
169
170
  # Common method to finalize a match attempt.
170
171
  # Assumes the node is already validated as an endpoint.
@@ -175,20 +176,18 @@ module RubyRoutes
175
176
  # @param captured_params [Hash] captured parameters from traversal
176
177
  # @return [Array] [handler, params] or [nil, params]
177
178
  def finalize_match(node, method, params, captured_params)
179
+ # Apply captured params once at the beginning
180
+ apply_captured_params(params, captured_params)
181
+
178
182
  if node && endpoint_with_method?(node, method)
179
183
  handler = node.handlers[method]
180
- # Apply captured params before constraint validation
181
- apply_captured_params(params, captured_params)
182
184
  if check_constraints(handler, params)
183
185
  return [handler, params]
184
186
  end
185
187
  end
186
188
  # For non-matching paths, return nil
187
- apply_captured_params(params, captured_params)
188
189
  [nil, params]
189
190
  end
190
-
191
- # Handles matching for the root path.
192
191
  #
193
192
  # @param method [String] HTTP method
194
193
  # @param params_out [Hash] parameters hash
@@ -46,6 +46,7 @@ module RubyRoutes
46
46
  # @return [Node] the dynamic child node
47
47
  def handle_dynamic(current_node, token)
48
48
  param_name = token[1..]
49
+ raise ArgumentError, "Dynamic parameter name cannot be empty" if param_name.nil? || param_name.empty?
49
50
  current_node.dynamic_child ||= build_param_node(param_name)
50
51
  current_node.dynamic_child
51
52
  end
@@ -66,6 +66,8 @@ module RubyRoutes
66
66
  validate_alpha_constraint(value)
67
67
  when 'alphanumeric'
68
68
  validate_alphanumeric_constraint(value)
69
+ else
70
+ invalid!
69
71
  end
70
72
  end
71
73
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'path_generation'
4
4
  require_relative 'warning_helpers'
5
+ require_relative 'constraint_validator'
5
6
 
6
7
  module RubyRoutes
7
8
  class Route
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
+ require 'thread'
4
5
 
5
6
  module RubyRoutes
6
7
  class Route
@@ -19,13 +20,14 @@ module RubyRoutes
19
20
  # @param param [String, Symbol] The parameter name for which the warning
20
21
  # is being emitted.
21
22
  # @return [void]
23
+
22
24
  def warn_proc_constraint_deprecation(param)
23
25
  key = param.to_sym
24
- return if @proc_warnings_shown&.include?(key)
25
-
26
- @proc_warnings_shown ||= Set.new
27
- @proc_warnings_shown << key
28
- warn_proc_warning(key)
26
+ @warnings_mutex ||= Mutex.new
27
+ @warnings_mutex.synchronize do
28
+ @proc_warnings_shown ||= Set.new
29
+ warn_proc_warning(key) if @proc_warnings_shown.add?(key)
30
+ end
29
31
  end
30
32
 
31
33
  # Warn about `Proc` constraint deprecation.
@@ -83,7 +83,7 @@ module RubyRoutes
83
83
  setup_controller_and_action(options)
84
84
 
85
85
  @name = options[:as]
86
- @constraints = options[:constraints] || {}
86
+ @constraints = (options[:constraints] || {}).freeze
87
87
  @defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
88
88
  @param_key_slots = [[nil, nil], [nil, nil]]
89
89
  @required_validated_once = false
@@ -145,8 +145,9 @@ module RubyRoutes
145
145
  # @param options [Hash] The options for the route.
146
146
  # @return [String, nil] The inferred controller name.
147
147
  def extract_controller(options)
148
+ return options[:controller] if options[:controller]
148
149
  to = options[:to]
149
- return options[:controller] unless to
150
+ return nil unless to
150
151
 
151
152
  to.to_s.split('#', 2).first
152
153
  end
@@ -64,12 +64,15 @@ module RubyRoutes
64
64
  # @param entry [Hash] The cache entry.
65
65
  # @return [void]
66
66
  def insert_cache_entry(cache_key, entry)
67
- if @recognition_cache.size >= @recognition_cache_max
68
- @recognition_cache.keys.first(@recognition_cache_max / 4).each do |evict_key|
69
- @recognition_cache.delete(evict_key)
67
+ @cache_mutex ||= Mutex.new
68
+ @cache_mutex.synchronize do
69
+ if @recognition_cache.size >= @recognition_cache_max
70
+ @recognition_cache.keys.first(@recognition_cache_max / 4).each do |evict_key|
71
+ @recognition_cache.delete(evict_key)
72
+ end
70
73
  end
74
+ @recognition_cache[cache_key] = entry
71
75
  end
72
- @recognition_cache[cache_key] = entry
73
76
  end
74
77
  end
75
78
  end
@@ -18,10 +18,10 @@ module RubyRoutes
18
18
  # @param route [Route] The route to add.
19
19
  # @return [Route] The added route.
20
20
  def add_to_collection(route)
21
- @named_routes[route.name] = route if route.named?
22
- @radix_tree.add(route.path, route.methods, route)
23
21
  return route if @routes.include?(route) # Prevent duplicate insertion
24
22
 
23
+ @named_routes[route.name] = route if route.named?
24
+ @radix_tree.add(route.path, route.methods, route)
25
25
  @routes << route
26
26
  route
27
27
  end
@@ -74,13 +74,16 @@ module RubyRoutes
74
74
  #
75
75
  # @return [void]
76
76
  def clear!
77
- @routes.clear
78
- @named_routes.clear
79
- @recognition_cache.clear
80
- @cache_hits = 0
81
- @cache_misses = 0
82
- @radix_tree = RadixTree.new
83
- RubyRoutes::Utility::KeyBuilderUtility.clear!
77
+ @cache_mutex ||= Mutex.new
78
+ @cache_mutex.synchronize do
79
+ @routes.clear
80
+ @named_routes.clear
81
+ @recognition_cache.clear
82
+ @cache_hits = 0
83
+ @cache_misses = 0
84
+ @radix_tree = RadixTree.new
85
+ RubyRoutes::Utility::KeyBuilderUtility.clear!
86
+ end
84
87
  end
85
88
 
86
89
  # Get the number of routes.
@@ -90,18 +90,6 @@ module RubyRoutes
90
90
  route.generate_path(params)
91
91
  end
92
92
 
93
- # Replay recorded calls on the router instance.
94
- #
95
- # This method replays all the recorded route definitions and other
96
- # configuration calls on the given router instance.
97
- #
98
- # @param router [Router] The router instance to replay calls on.
99
- # @return [void]
100
- def replay_recorded_calls(router)
101
- # Placeholder for actual implementation
102
- # Iterate over recorded calls and apply them to the router
103
- end
104
-
105
93
  private
106
94
 
107
95
  # Set up the radix tree for structural path matching.
@@ -17,6 +17,12 @@ module RubyRoutes
17
17
  def build
18
18
  router = Router.new
19
19
  validate_calls(@recorded_calls)
20
+ RubyRoutes::Constant::RECORDED_METHODS.each do |method_name|
21
+ define_method(method_name) do |*arguments, &definition_block|
22
+ @recorded_calls << [__method__, arguments, definition_block]
23
+ nil
24
+ end
25
+ end
20
26
  router.finalize!
21
27
  router
22
28
  end
@@ -42,7 +42,10 @@ module RubyRoutes
42
42
  # @return [Array<Array(Symbol, Array, Proc|NilClass)>]
43
43
  # A snapshot of the recorded calls to avoid external mutation.
44
44
  def recorded_calls
45
- @recorded_calls.dup.freeze
45
+ # Deep-copy each recorded call’s args array and freeze the result to prevent mutation
46
+ @recorded_calls
47
+ .map { |(method_name, args, block)| [method_name, args.dup.freeze, block] }
48
+ .freeze
46
49
  end
47
50
 
48
51
  # Initialize the Builder.
@@ -72,26 +75,6 @@ module RubyRoutes
72
75
  nil
73
76
  end
74
77
  end
75
-
76
- private
77
-
78
- # Validate the recorded calls.
79
- #
80
- # This method ensures that all recorded calls use valid router methods
81
- # as defined in `RubyRoutes::Constant::RECORDED_METHODS`.
82
- #
83
- # @param recorded_calls [Array<Array(Symbol, Array, Proc|NilClass)>]
84
- # The recorded calls to validate.
85
- # @raise [ArgumentError] If any recorded call uses an invalid method.
86
- # @return [void]
87
- def validate_calls(recorded_calls)
88
- allowed_router_methods = RubyRoutes::Constant::RECORDED_METHODS
89
- recorded_calls.each do |(router_method, _arguments, _definition_block)|
90
- unless router_method.is_a?(Symbol) && allowed_router_methods.include?(router_method)
91
- raise ArgumentError, "Invalid router method: #{router_method.inspect}"
92
- end
93
- end
94
- end
95
78
  end
96
79
  end
97
80
  end
@@ -83,7 +83,12 @@ module RubyRoutes
83
83
  via = options[:via]
84
84
  raise ArgumentError, 'match requires :via (e.g., via: [:get, :post])' if via.nil? || Array(via).empty?
85
85
 
86
- add_route(path, options)
86
+ normalized_via = Array(via)
87
+ opts = options.dup
88
+ # Keep :via in opts only if BuildHelpers expects it; otherwise delete.
89
+ # If BuildHelpers infers from the second arg, delete it:
90
+ opts.delete(:via)
91
+ add_route(path, build_route_options(opts, normalized_via))
87
92
  self
88
93
  end
89
94
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'build_helpers'
4
+ require_relative '../utility/inflector_utility'
4
5
 
5
6
  module RubyRoutes
6
7
  class Router
@@ -13,8 +14,6 @@ module RubyRoutes
13
14
  module ResourceHelpers
14
15
  include RubyRoutes::Router::BuildHelpers
15
16
 
16
- private
17
-
18
17
  # Define RESTful routes for a resource.
19
18
  #
20
19
  # @param resource_name [Symbol, String] The name of the resource.
@@ -35,6 +34,8 @@ module RubyRoutes
35
34
  end
36
35
  end
37
36
 
37
+ private
38
+
38
39
  # Prepare options by removing the `:to` key if present.
39
40
  #
40
41
  # @param options [Hash] The options hash.
@@ -3,6 +3,7 @@
3
3
  require_relative 'utility/inflector_utility'
4
4
  require_relative 'utility/route_utility'
5
5
  require_relative 'router/http_helpers'
6
+ require_relative 'router/resource_helpers'
6
7
  require_relative 'constant'
7
8
  require_relative 'route_set'
8
9
  module RubyRoutes
@@ -87,11 +88,16 @@ module RubyRoutes
87
88
  return self if @frozen
88
89
 
89
90
  @frozen = true
91
+ if @route_set.respond_to?(:finalize!)
92
+ @route_set.finalize!
93
+ else
94
+ @route_set.freeze
95
+ end
96
+ @route_utils.freeze if @route_utils.respond_to?(:freeze)
90
97
  @scope_stack.freeze
91
98
  @concerns.freeze
92
99
  self
93
100
  end
94
-
95
101
  # Check if the router is frozen.
96
102
  #
97
103
  # @return [Boolean] `true` if the router is frozen, `false` otherwise.
@@ -99,12 +105,17 @@ module RubyRoutes
99
105
  !!@frozen
100
106
  end
101
107
 
102
- # Define a root route.
108
+ # Define the root route.
103
109
  #
104
110
  # @param options [Hash] The options for the root route.
105
111
  # @return [Router] self.
106
112
  def root(options = {})
107
- add_route('/', build_route_options(options, :get))
113
+ ensure_unfrozen!
114
+ if options[:to]
115
+ add_route('/', via: [:get], **options)
116
+ else
117
+ add_route('/', controller: 'root', action: :index, via: [:get], **options)
118
+ end
108
119
  self
109
120
  end
110
121
 
@@ -117,6 +128,7 @@ module RubyRoutes
117
128
  # @param nested_block [Proc] The block for nested routes.
118
129
  # @return [Router] self.
119
130
  def resources(resource_name, options = {}, &nested_block)
131
+ ensure_unfrozen!
120
132
  define_resource_routes(resource_name, options, &nested_block)
121
133
  self
122
134
  end
@@ -127,6 +139,7 @@ module RubyRoutes
127
139
  # @param options [Hash] The options for the resource.
128
140
  # @return [Router] self.
129
141
  def resource(resource_name, options = {})
142
+ ensure_unfrozen!
130
143
  singular = RubyRoutes::Utility::InflectorUtility.singularize(resource_name.to_s)
131
144
  controller = options[:controller] || singular
132
145
  define_singular_routes(singular, controller, options)
@@ -141,6 +154,7 @@ module RubyRoutes
141
154
  # @param block [Proc] The block for nested routes.
142
155
  # @return [Router] self.
143
156
  def namespace(namespace_name, options = {}, &block)
157
+ ensure_unfrozen!
144
158
  push_scope({ path: "/#{namespace_name}", module: namespace_name }.merge(options)) do
145
159
  instance_eval(&block) if block
146
160
  end
@@ -152,6 +166,7 @@ module RubyRoutes
152
166
  # @param block [Proc] The block for nested routes.
153
167
  # @return [Router] self.
154
168
  def scope(options_or_path = {}, &block)
169
+ ensure_unfrozen!
155
170
  scope_entry = options_or_path.is_a?(String) ? { path: options_or_path } : options_or_path
156
171
  push_scope(scope_entry) { instance_eval(&block) if block }
157
172
  end
@@ -162,6 +177,7 @@ module RubyRoutes
162
177
  # @param block [Proc] The block for nested routes.
163
178
  # @return [Router] self.
164
179
  def constraints(constraints_hash = {}, &block)
180
+ ensure_unfrozen!
165
181
  push_scope(constraints: constraints_hash) { instance_eval(&block) if block }
166
182
  end
167
183
 
@@ -171,6 +187,7 @@ module RubyRoutes
171
187
  # @param block [Proc] The block for nested routes.
172
188
  # @return [Router] self.
173
189
  def defaults(defaults_hash = {}, &block)
190
+ ensure_unfrozen!
174
191
  push_scope(defaults: defaults_hash) { instance_eval(&block) if block }
175
192
  end
176
193
 
@@ -192,6 +209,7 @@ module RubyRoutes
192
209
  # @param block [Proc] The block for additional routes.
193
210
  # @return [void]
194
211
  def concerns(*concern_names, &block)
212
+ ensure_unfrozen!
195
213
  concern_names.each do |name|
196
214
  concern_block = @concerns[name]
197
215
  raise "Concern '#{name}' not found" unless concern_block
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../constant'
4
+ require_relative '../route/small_lru'
4
5
 
5
6
  module RubyRoutes
6
7
  module Utility
@@ -38,15 +39,10 @@ module RubyRoutes
38
39
  }.freeze
39
40
 
40
41
  # Cache for non‑predefined or previously seen method tokens.
41
- # Keys: original `String` or `Symbol`
42
- # Values: frozen uppercase `String`
42
+ # Now uses SmallLru for LRU eviction instead of simple clearing.
43
43
  #
44
- # Note: This is intentionally mutable for caching purposes.
45
- #
46
- # @return [Hash{(String,Symbol) => String}]
47
- # rubocop:disable Style/MutableConstant
48
- METHOD_CACHE = {} # Intentionally mutable for caching
49
- # rubocop:enable Style/MutableConstant
44
+ # @return [SmallLru]
45
+ METHOD_CACHE = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::METHOD_CACHE_MAX_SIZE)
50
46
 
51
47
  # Normalize an HTTP method‑like input to a canonical uppercase `String`.
52
48
  #
@@ -80,8 +76,9 @@ module RubyRoutes
80
76
  def normalize_string_method(method_input)
81
77
  return method_input if already_upper_ascii?(method_input)
82
78
 
83
- key = method_input.dup
84
- METHOD_CACHE[key] ||= ascii_upcase(key).freeze
79
+ # Use SmallLru for LRU eviction, freeze key to prevent mutation
80
+ key = method_input.dup.freeze
81
+ METHOD_CACHE.get(key) || METHOD_CACHE.set(key, ascii_upcase(method_input.dup).freeze)
85
82
  end
86
83
 
87
84
  # Normalize a `Symbol` HTTP method.
@@ -89,7 +86,10 @@ module RubyRoutes
89
86
  # @param method_input [Symbol] The HTTP method input.
90
87
  # @return [String] The normalized HTTP method.
91
88
  def normalize_symbol_method(method_input)
92
- SYMBOL_MAP[method_input] || (METHOD_CACHE[method_input] ||= ascii_upcase(method_input.to_s).freeze)
89
+ SYMBOL_MAP[method_input] || begin
90
+ key = method_input.to_s.freeze
91
+ METHOD_CACHE.get(key) || METHOD_CACHE.set(key, ascii_upcase(method_input.to_s).freeze)
92
+ end
93
93
  end
94
94
 
95
95
  # Normalize an arbitrary HTTP method input.
@@ -100,8 +100,9 @@ module RubyRoutes
100
100
  coerced = method_input.to_s
101
101
  return coerced if already_upper_ascii?(coerced)
102
102
 
103
- key = coerced.dup
104
- METHOD_CACHE[key] ||= ascii_upcase(key).freeze
103
+ # Use SmallLru for LRU eviction, freeze key to prevent mutation
104
+ key = coerced.dup.freeze
105
+ METHOD_CACHE.get(key) || METHOD_CACHE.set(key, ascii_upcase(coerced.dup).freeze)
105
106
  end
106
107
 
107
108
  # Determine if a `String` consists solely of uppercase ASCII (`A–Z`) or non‑letters.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyRoutes
4
- VERSION = '2.4.0'
4
+ VERSION = '2.5.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.4.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yosef Benny Widyokarsono