ruby_routes 2.3.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.
@@ -40,41 +40,26 @@ module RubyRoutes
40
40
  # @return [String] The generated or cached path string.
41
41
  def build_or_fetch_generated_path(merged_params)
42
42
  generation_cache_key = build_generation_cache_key(merged_params)
43
- if (cached_path = @gen_cache.get(generation_cache_key))
43
+ if (cached_path = @cache_mutex.synchronize { @gen_cache.get(generation_cache_key) })
44
44
  return cached_path
45
45
  end
46
46
 
47
47
  generated_path = generate_path_string(merged_params)
48
- @gen_cache.set(generation_cache_key, generated_path)
49
- generated_path
48
+ frozen_path = generated_path.frozen? ? generated_path : generated_path.dup.freeze
49
+ @cache_mutex.synchronize { @gen_cache.set(generation_cache_key, frozen_path) }
50
+ frozen_path
50
51
  end
51
52
 
52
53
  # Build a generation cache key for merged params.
53
54
  #
54
- # This method creates a cache key based on the required parameters and
55
- # their values in the merged parameters.
55
+ # This method creates a cache key from all dynamic path parameters
56
+ # (required + optional, including splats) present in the merged parameters.
56
57
  #
57
58
  # @param merged_params [Hash] The merged parameters for path generation.
58
59
  # @return [String] The cache key for the generation cache.
59
60
  def build_generation_cache_key(merged_params)
60
- @required_params.empty? ? RubyRoutes::Constant::EMPTY_STRING : build_param_cache_key(merged_params)
61
- end
62
-
63
- # Emit deprecation warning for `Proc` constraints once per parameter.
64
- #
65
- # This method ensures that a deprecation warning for a `Proc` constraint
66
- # is only emitted once per parameter. It tracks parameters for which
67
- # warnings have already been shown.
68
- #
69
- # @param param [String, Symbol] The parameter name for which the warning
70
- # is being emitted.
71
- # @return [void]
72
- def warn_proc_constraint_deprecation(param)
73
- return if @proc_warnings_shown&.include?(param)
74
-
75
- @proc_warnings_shown ||= Set.new
76
- @proc_warnings_shown << param
77
- warn_proc_warning(param)
61
+ names = @required_params.empty? ? @param_names : @required_params
62
+ names.empty? ? RubyRoutes::Constant::EMPTY_STRING : cache_key_for_params(names, merged_params)
78
63
  end
79
64
 
80
65
  # Determine if the route can short-circuit to a static path.
@@ -85,7 +70,7 @@ module RubyRoutes
85
70
  # @param params [Hash] The parameters for path generation.
86
71
  # @return [Boolean] `true` if the route can short-circuit, `false` otherwise.
87
72
  def static_short_circuit?(params)
88
- @static_path && (params.nil? || params.empty?)
73
+ !!@static_path && (params.nil? || params.empty?)
89
74
  end
90
75
 
91
76
  # Determine if the route is trivial.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack'
3
+ require 'rack/utils'
4
4
 
5
5
  module RubyRoutes
6
6
  class Route
@@ -43,12 +43,12 @@ module RubyRoutes
43
43
  query_part = path[(query_index + 1)..]
44
44
  return RubyRoutes::Constant::EMPTY_HASH if query_part.empty? || query_part.match?(/^\?+$/)
45
45
 
46
- if (cached_result = @query_cache.get(query_part))
46
+ if (cached_result = @cache_mutex.synchronize { @query_cache.get(query_part) })
47
47
  return cached_result
48
48
  end
49
49
 
50
50
  parsed_result = Rack::Utils.parse_query(query_part)
51
- @query_cache.set(query_part, parsed_result)
51
+ @cache_mutex.synchronize { @query_cache.set(query_part, parsed_result) }
52
52
  parsed_result
53
53
  end
54
54
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+ require_relative '../utility/path_utility'
5
+
3
6
  module RubyRoutes
4
7
  class Route
5
8
  # SegmentCompiler: path analysis + extraction
@@ -8,9 +11,9 @@ module RubyRoutes
8
11
  # a route path. It includes utilities for compiling path segments, required
9
12
  # parameters, and static paths, as well as extracting parameters from a
10
13
  # request path.
11
- #
12
- # @module RubyRoutes::Route::SegmentCompiler
13
14
  module SegmentCompiler
15
+ include RubyRoutes::Utility::PathUtility
16
+
14
17
  private
15
18
 
16
19
  # Compile the segments from the path.
@@ -43,7 +46,7 @@ module RubyRoutes
43
46
  dynamic_param_names.freeze
44
47
  else
45
48
  dynamic_param_names.reject do |name|
46
- @defaults.key?(name) || @defaults.key?(name.to_sym)
49
+ @defaults.key?(name) || (@defaults.key?(name.to_sym) if name.is_a?(String))
47
50
  end.freeze
48
51
  end
49
52
  @required_params_set = @required_params.to_set.freeze
@@ -39,19 +39,31 @@ module RubyRoutes
39
39
  # Validate required parameters once.
40
40
  #
41
41
  # This method validates that all required parameters are present and not
42
- # nil. It ensures that validation is performed only once per request.
42
+ # nil. It uses per-params caching to avoid re-validation for the same
43
+ # frozen params.
43
44
  #
44
45
  # @param params [Hash] The parameters to validate.
45
46
  # @raise [RouteNotFound] If required parameters are missing or nil.
46
47
  # @return [void]
47
48
  def validate_required_once(params)
48
- return if @required_params.empty? || @required_validated_once
49
+ return if @required_params.empty?
49
50
 
50
- missing, nils = validate_required_params(params)
51
+ # Check cache for existing validation result
52
+ cached_result = get_cached_validation(params)
53
+ if cached_result
54
+ missing, nils = cached_result
55
+ else
56
+ # Perform validation
57
+ missing, nils = validate_required_params(params)
58
+ # Cache the result only if params are frozen
59
+ if params.frozen?
60
+ cache_validation_result(params, [missing, nils])
61
+ end
62
+ end
63
+
64
+ # Raise if invalid
51
65
  raise RouteNotFound, "Missing params: #{missing.join(', ')}" unless missing.empty?
52
66
  raise RouteNotFound, "Missing or nil params: #{nils.join(', ')}" unless nils.empty?
53
-
54
- @required_validated_once = true
55
67
  end
56
68
 
57
69
  # Validate required parameters.
@@ -64,13 +76,20 @@ module RubyRoutes
64
76
  # - `nils` [Array<String>] The keys of parameters that are nil.
65
77
  def validate_required_params(params)
66
78
  return RubyRoutes::Constant::EMPTY_PAIR if @required_params.empty?
79
+ params ||= {}
80
+
81
+ if (cached = get_cached_validation(params))
82
+ return cached
83
+ end
67
84
 
68
85
  missing = []
69
86
  nils = []
70
87
  @required_params.each do |required_key|
71
88
  process_required_key(required_key, params, missing, nils)
72
89
  end
73
- [missing, nils]
90
+ result = [missing, nils]
91
+ cache_validation_result(params, result)
92
+ result
74
93
  end
75
94
 
76
95
  # Per-key validation helper used by `validate_required_params`.
@@ -85,10 +104,13 @@ module RubyRoutes
85
104
  def process_required_key(required_key, params, missing, nils)
86
105
  if params.key?(required_key)
87
106
  nils << required_key if params[required_key].nil?
88
- elsif params.key?(symbol_key = required_key.to_sym)
89
- nils << required_key if params[symbol_key].nil?
90
107
  else
91
- missing << required_key
108
+ symbol_key = required_key.to_sym
109
+ if params.key?(symbol_key)
110
+ nils << required_key if params[symbol_key].nil?
111
+ else
112
+ missing << required_key
113
+ end
92
114
  end
93
115
  end
94
116
 
@@ -104,7 +126,7 @@ module RubyRoutes
104
126
  return unless params.frozen?
105
127
  return unless @validation_cache && @validation_cache.size < 64
106
128
 
107
- @validation_cache.set(params.hash, result)
129
+ @cache_mutex.synchronize { @validation_cache.set(params.hash, result) }
108
130
  end
109
131
 
110
132
  # Fetch cached validation result.
@@ -114,7 +136,8 @@ module RubyRoutes
114
136
  # @param params [Hash] The parameters used for validation.
115
137
  # @return [Object, nil] The cached validation result, or `nil` if not found.
116
138
  def get_cached_validation(params)
117
- @validation_cache&.get(params.hash)
139
+ return nil unless params && @validation_cache
140
+ @cache_mutex.synchronize { @validation_cache.get(params.hash) }
118
141
  end
119
142
 
120
143
  # Return hash to pool.
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+ require 'thread'
5
+
3
6
  module RubyRoutes
4
7
  class Route
5
8
  # WarningHelpers: encapsulate deprecation / warning helpers.
@@ -17,12 +20,14 @@ module RubyRoutes
17
20
  # @param param [String, Symbol] The parameter name for which the warning
18
21
  # is being emitted.
19
22
  # @return [void]
20
- def warn_proc_constraint_deprecation(param)
21
- return if @proc_warnings_shown&.include?(param)
22
23
 
23
- @proc_warnings_shown ||= Set.new
24
- @proc_warnings_shown << param
25
- warn_proc_warning(param)
24
+ def warn_proc_constraint_deprecation(param)
25
+ key = param.to_sym
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
26
31
  end
27
32
 
28
33
  # Warn about `Proc` constraint deprecation.
@@ -3,6 +3,7 @@
3
3
  require 'uri'
4
4
  require 'timeout'
5
5
  require 'rack'
6
+ require 'set'
6
7
  require_relative 'constant'
7
8
  require_relative 'node'
8
9
  require_relative 'route/small_lru'
@@ -35,9 +36,10 @@ module RubyRoutes
35
36
  # - Minimal object allocation in hot paths
36
37
  #
37
38
  # Thread Safety:
38
- # - Instance is effectively read‑only after initialization aside from
39
- # internal caches which are not synchronized but safe for typical
40
- # single writer (boot) + many reader (request) usage.
39
+ # - Instance is effectively read‑only after initialization.
40
+ # - Internal caches (@query_cache, @gen_cache, @validation_cache) are protected
41
+ # by a mutex for safe concurrent access across multiple threads.
42
+ # - Designed for "build during boot, read per request" usage pattern.
41
43
  #
42
44
  # Public API Surface (stable):
43
45
  # - #match?
@@ -81,7 +83,7 @@ module RubyRoutes
81
83
  setup_controller_and_action(options)
82
84
 
83
85
  @name = options[:as]
84
- @constraints = options[:constraints] || {}
86
+ @constraints = (options[:constraints] || {}).freeze
85
87
  @defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
86
88
  @param_key_slots = [[nil, nil], [nil, nil]]
87
89
  @required_validated_once = false
@@ -143,8 +145,9 @@ module RubyRoutes
143
145
  # @param options [Hash] The options for the route.
144
146
  # @return [String, nil] The inferred controller name.
145
147
  def extract_controller(options)
148
+ return options[:controller] if options[:controller]
146
149
  to = options[:to]
147
- return options[:controller] unless to
150
+ return nil unless to
148
151
 
149
152
  to.to_s.split('#', 2).first
150
153
  end
@@ -166,6 +169,7 @@ module RubyRoutes
166
169
  @is_resource = @path.match?(%r{/:id(?:$|\.)})
167
170
  @gen_cache = SmallLru.new(512)
168
171
  @query_cache = SmallLru.new(RubyRoutes::Constant::QUERY_CACHE_SIZE)
172
+ @cache_mutex = Mutex.new # Thread-safe access to caches
169
173
  initialize_validation_cache
170
174
  compile_segments
171
175
  compile_required_params
@@ -40,104 +40,6 @@ module RubyRoutes
40
40
  @recognition_cache_max = 2048
41
41
  @cache_hits = 0
42
42
  @cache_misses = 0
43
- @request_key_pool = {}
44
- @request_key_ring = Array.new(RubyRoutes::Constant::REQUEST_KEY_CAPACITY)
45
- @entry_count = 0
46
- @ring_index = 0
47
- end
48
-
49
- # Fetch (or build) a composite request cache key with ring-buffer eviction.
50
- #
51
- # Ensures consistent use of frozen method/path keys to avoid mixed key space bugs.
52
- #
53
- # @param http_method [String, Symbol] The HTTP method (e.g., `:get`, `:post`).
54
- # @param request_path [String] The request path.
55
- # @return [String] The composite request key.
56
- def fetch_request_key(http_method, request_path)
57
- method_key, path_key = normalize_keys(http_method, request_path)
58
- composite_key = build_composite_key(method_key, path_key)
59
-
60
- return composite_key if handle_cache_hit(method_key, path_key, composite_key)
61
-
62
- handle_cache_miss(method_key, path_key, composite_key)
63
- composite_key
64
- end
65
-
66
- # Normalize keys.
67
- #
68
- # Converts the HTTP method and request path into frozen strings for consistent
69
- # key usage.
70
- #
71
- # @param http_method [String, Symbol] The HTTP method.
72
- # @param request_path [String] The request path.
73
- # @return [Array<String>] An array containing the normalized method and path keys.
74
- def normalize_keys(http_method, request_path)
75
- method_key = http_method.is_a?(String) ? http_method.upcase.freeze : http_method.to_s.upcase.freeze
76
- path_key = request_path.is_a?(String) ? request_path.freeze : request_path.to_s.freeze
77
- [method_key, path_key]
78
- end
79
-
80
- # Build composite key.
81
- #
82
- # Combines the HTTP method and path into a single composite key.
83
- #
84
- # @param method_key [String] The normalized HTTP method key.
85
- # @param path_key [String] The normalized path key.
86
- # @return [String] The composite key.
87
- def build_composite_key(method_key, path_key)
88
- "#{method_key}:#{path_key}".freeze
89
- end
90
-
91
- # Handle cache hit.
92
- #
93
- # Checks if the composite key already exists in the request key pool.
94
- #
95
- # @param method_key [String] The normalized HTTP method key.
96
- # @param path_key [String] The normalized path key.
97
- # @param _composite_key [String] The composite key (unused).
98
- # @return [Boolean] `true` if the key exists, `false` otherwise.
99
- def handle_cache_hit(method_key, path_key, _composite_key)
100
- return true if @request_key_pool[method_key]&.key?(path_key)
101
-
102
- false
103
- end
104
-
105
- # Handle cache miss.
106
- #
107
- # Adds the composite key to the request key pool and manages the ring buffer
108
- # for eviction.
109
- #
110
- # @param method_key [String] The normalized HTTP method key.
111
- # @param path_key [String] The normalized path key.
112
- # @param composite_key [String] The composite key.
113
- # @return [void]
114
- def handle_cache_miss(method_key, path_key, composite_key)
115
- @request_key_pool[method_key][path_key] = composite_key if @request_key_pool[method_key]
116
- @request_key_pool[method_key] = { path_key => composite_key } unless @request_key_pool[method_key]
117
-
118
- if @entry_count < RubyRoutes::Constant::REQUEST_KEY_CAPACITY
119
- @request_key_ring[@entry_count] = [method_key, path_key]
120
- @entry_count += 1
121
- else
122
- evict_old_entry(method_key, path_key)
123
- end
124
- end
125
-
126
- # Evict old entry.
127
- #
128
- # Removes the oldest entry from the request key pool and updates the ring buffer.
129
- #
130
- # @param method_key [String] The normalized HTTP method key.
131
- # @param path_key [String] The normalized path key.
132
- # @return [void]
133
- def evict_old_entry(method_key, path_key)
134
- evict_method, evict_path = @request_key_ring[@ring_index]
135
- if (evict_bucket = @request_key_pool[evict_method]) && evict_bucket.delete(evict_path) && evict_bucket.empty?
136
- @request_key_pool.delete(evict_method)
137
- end
138
- @request_key_ring[@ring_index] = [method_key, path_key]
139
- @ring_index += 1
140
- @ring_index = 0 if @ring_index == RubyRoutes::Constant::REQUEST_KEY_CAPACITY
141
43
  end
142
44
 
143
45
  # Fetch cached recognition entry while updating hit counter.
@@ -162,12 +64,15 @@ module RubyRoutes
162
64
  # @param entry [Hash] The cache entry.
163
65
  # @return [void]
164
66
  def insert_cache_entry(cache_key, entry)
165
- if @recognition_cache.size >= @recognition_cache_max
166
- @recognition_cache.keys.first(@recognition_cache_max / 4).each do |evict_key|
167
- @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
168
73
  end
74
+ @recognition_cache[cache_key] = entry
169
75
  end
170
- @recognition_cache[cache_key] = entry
171
76
  end
172
77
  end
173
78
  end
@@ -18,9 +18,11 @@ 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
- @routes << route
22
- @radix_tree.add(route.path, route.methods, route)
21
+ return route if @routes.include?(route) # Prevent duplicate insertion
22
+
23
23
  @named_routes[route.name] = route if route.named?
24
+ @radix_tree.add(route.path, route.methods, route)
25
+ @routes << route
24
26
  route
25
27
  end
26
28
  alias add_route add_to_collection
@@ -33,8 +35,7 @@ module RubyRoutes
33
35
  # @param route [Route] The route to register.
34
36
  # @return [Route] The registered route.
35
37
  def register(route)
36
- (@routes ||= []) << route
37
- route
38
+ add_to_collection(route)
38
39
  end
39
40
 
40
41
  # Find any route (no params) for a method/path.
@@ -55,7 +56,7 @@ module RubyRoutes
55
56
  # This method retrieves a route by its name from the named routes collection.
56
57
  # If no route is found, it raises a `RouteNotFound` error.
57
58
  #
58
- # @param name [Symbol, String] The name of the route.
59
+ # @param name [Symbol] The name of the route.
59
60
  # @return [Route] The named route.
60
61
  # @raise [RouteNotFound] If no route with the given name is found.
61
62
  def find_named_route(name)
@@ -68,21 +69,21 @@ module RubyRoutes
68
69
  # Clear all routes and caches.
69
70
  #
70
71
  # This method clears the internal route collection, named routes, recognition
71
- # cache, and radix tree. It also resets cache hit/miss counters and the
72
- # request key pool.
72
+ # cache, and radix tree. It also resets cache hit/miss counters and clears
73
+ # the global request key cache.
73
74
  #
74
75
  # @return [void]
75
76
  def clear!
76
- @routes.clear
77
- @named_routes.clear
78
- @recognition_cache.clear
79
- @cache_hits = 0
80
- @cache_misses = 0
81
- @radix_tree = RadixTree.new
82
- @request_key_pool.clear
83
- @request_key_ring.fill(nil)
84
- @entry_count = 0
85
- @ring_index = 0
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
86
87
  end
87
88
 
88
89
  # Get the number of routes.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'radix_tree'
3
4
  require_relative 'utility/key_builder_utility'
4
5
  require_relative 'utility/method_utility'
5
6
  require_relative 'route_set/cache_helpers'
@@ -18,7 +19,11 @@ module RubyRoutes
18
19
  # a small in‑memory recognition cache.
19
20
  # - Delegate structural path matching to an internal RadixTree.
20
21
  #
21
- # Thread safety: not thread‑safe; build during boot, read per request.
22
+ # Thread Safety:
23
+ # - RouteSet instances are not fully thread-safe for modifications.
24
+ # - Build during boot/initialization, then use read-only per request.
25
+ # - Global caches (via KeyBuilderUtility) are thread-safe for concurrent reads.
26
+ # - Per-instance recognition cache is not protected (single-threaded usage assumed).
22
27
  #
23
28
  # @api public (primary integration surface)
24
29
  class RouteSet
@@ -117,7 +122,11 @@ module RubyRoutes
117
122
  return nil unless matched_route
118
123
 
119
124
  # Ensure we have a mutable hash for merging defaults / query params.
120
- extracted_params = extracted_params.dup if extracted_params&.frozen?
125
+ if extracted_params.nil?
126
+ extracted_params = {}
127
+ elsif extracted_params.frozen?
128
+ extracted_params = extracted_params.dup
129
+ end
121
130
 
122
131
  merge_query_params(matched_route, raw_path, extracted_params)
123
132
  merge_defaults(matched_route, extracted_params)
@@ -17,7 +17,12 @@ module RubyRoutes
17
17
  def build
18
18
  router = Router.new
19
19
  validate_calls(@recorded_calls)
20
- router.freeze
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
21
26
  router.finalize!
22
27
  router
23
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.
@@ -62,8 +65,9 @@ module RubyRoutes
62
65
  # `RubyRoutes::Constant::RECORDED_METHODS`. Each method records its
63
66
  # invocation (method name, arguments, and block) in `@recorded_calls`.
64
67
  #
65
- # @param *arguments [Array] The arguments passed to the DSL method.
66
- # @param definition_block [Proc, nil] The block passed to the DSL method.
68
+ # The dynamically defined methods accept arbitrary arguments and an optional block,
69
+ # which are recorded for later processing by the router.
70
+ #
67
71
  # @return [nil]
68
72
  RubyRoutes::Constant::RECORDED_METHODS.each do |method_name|
69
73
  define_method(method_name) do |*arguments, &definition_block|
@@ -71,26 +75,6 @@ module RubyRoutes
71
75
  nil
72
76
  end
73
77
  end
74
-
75
- private
76
-
77
- # Validate the recorded calls.
78
- #
79
- # This method ensures that all recorded calls use valid router methods
80
- # as defined in `RubyRoutes::Constant::RECORDED_METHODS`.
81
- #
82
- # @param recorded_calls [Array<Array(Symbol, Array, Proc|NilClass)>]
83
- # The recorded calls to validate.
84
- # @raise [ArgumentError] If any recorded call uses an invalid method.
85
- # @return [void]
86
- def validate_calls(recorded_calls)
87
- allowed_router_methods = RubyRoutes::Constant::RECORDED_METHODS
88
- recorded_calls.each do |(router_method, _arguments, _definition_block)|
89
- unless router_method.is_a?(Symbol) && allowed_router_methods.include?(router_method)
90
- raise ArgumentError, "Invalid router method: #{router_method.inspect}"
91
- end
92
- end
93
- end
94
78
  end
95
79
  end
96
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
 
@@ -108,7 +113,7 @@ module RubyRoutes
108
113
  # @raise [RuntimeError] If the router is frozen.
109
114
  # @return [void]
110
115
  def ensure_unfrozen!
111
- raise 'Router finalized (immutable)' if @frozen
116
+ raise 'Router finalized (immutable)' if @frozen || frozen?
112
117
  end
113
118
 
114
119
  # Define routes for a singular resource.
@@ -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.