ruby_routes 2.2.0 → 2.4.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +240 -163
  3. data/lib/ruby_routes/constant.rb +137 -18
  4. data/lib/ruby_routes/lru_strategies/hit_strategy.rb +31 -4
  5. data/lib/ruby_routes/lru_strategies/miss_strategy.rb +21 -0
  6. data/lib/ruby_routes/node.rb +86 -36
  7. data/lib/ruby_routes/radix_tree/finder.rb +213 -0
  8. data/lib/ruby_routes/radix_tree/inserter.rb +96 -0
  9. data/lib/ruby_routes/radix_tree.rb +65 -230
  10. data/lib/ruby_routes/route/check_helpers.rb +115 -0
  11. data/lib/ruby_routes/route/constraint_validator.rb +173 -0
  12. data/lib/ruby_routes/route/param_support.rb +200 -0
  13. data/lib/ruby_routes/route/path_builder.rb +84 -0
  14. data/lib/ruby_routes/route/path_generation.rb +87 -0
  15. data/lib/ruby_routes/route/query_helpers.rb +56 -0
  16. data/lib/ruby_routes/route/segment_compiler.rb +166 -0
  17. data/lib/ruby_routes/route/small_lru.rb +93 -18
  18. data/lib/ruby_routes/route/validation_helpers.rb +174 -0
  19. data/lib/ruby_routes/route/warning_helpers.rb +57 -0
  20. data/lib/ruby_routes/route.rb +127 -501
  21. data/lib/ruby_routes/route_set/cache_helpers.rb +76 -0
  22. data/lib/ruby_routes/route_set/collection_helpers.rb +125 -0
  23. data/lib/ruby_routes/route_set.rb +140 -132
  24. data/lib/ruby_routes/router/build_helpers.rb +99 -0
  25. data/lib/ruby_routes/router/builder.rb +97 -0
  26. data/lib/ruby_routes/router/http_helpers.rb +135 -0
  27. data/lib/ruby_routes/router/resource_helpers.rb +137 -0
  28. data/lib/ruby_routes/router/scope_helpers.rb +127 -0
  29. data/lib/ruby_routes/router.rb +196 -182
  30. data/lib/ruby_routes/segment.rb +28 -8
  31. data/lib/ruby_routes/segments/base_segment.rb +40 -4
  32. data/lib/ruby_routes/segments/dynamic_segment.rb +48 -12
  33. data/lib/ruby_routes/segments/static_segment.rb +43 -7
  34. data/lib/ruby_routes/segments/wildcard_segment.rb +58 -12
  35. data/lib/ruby_routes/string_extensions.rb +52 -15
  36. data/lib/ruby_routes/url_helpers.rb +106 -24
  37. data/lib/ruby_routes/utility/inflector_utility.rb +35 -0
  38. data/lib/ruby_routes/utility/key_builder_utility.rb +171 -77
  39. data/lib/ruby_routes/utility/method_utility.rb +137 -0
  40. data/lib/ruby_routes/utility/path_utility.rb +75 -28
  41. data/lib/ruby_routes/utility/route_utility.rb +30 -2
  42. data/lib/ruby_routes/version.rb +3 -1
  43. data/lib/ruby_routes.rb +68 -11
  44. metadata +27 -7
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRoutes
4
+ class RouteSet
5
+ # CacheHelpers: extracted cache, request-key, and eviction logic to reduce
6
+ # the size of the main RouteSet class.
7
+ #
8
+ # This module provides methods for managing caches, request keys, and
9
+ # implementing eviction policies for route recognition.
10
+ module CacheHelpers
11
+ # Recognition cache statistics.
12
+ #
13
+ # @return [Hash] A hash containing:
14
+ # - `:hits` [Integer] The number of cache hits.
15
+ # - `:misses` [Integer] The number of cache misses.
16
+ # - `:hit_rate` [Float] The cache hit rate as a percentage.
17
+ # - `:size` [Integer] The current size of the recognition cache.
18
+ def cache_stats
19
+ total_requests = @cache_hits + @cache_misses
20
+ {
21
+ hits: @cache_hits,
22
+ misses: @cache_misses,
23
+ hit_rate: total_requests.zero? ? 0.0 : (@cache_hits.to_f / total_requests * 100.0),
24
+ size: @recognition_cache.size
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ # Set up caches and request-key ring.
31
+ #
32
+ # Initializes the internal data structures for managing routes, named routes,
33
+ # recognition cache, and request-key ring buffer.
34
+ #
35
+ # @return [void]
36
+ def setup_caches
37
+ @routes = []
38
+ @named_routes = {}
39
+ @recognition_cache = {}
40
+ @recognition_cache_max = 2048
41
+ @cache_hits = 0
42
+ @cache_misses = 0
43
+ end
44
+
45
+ # Fetch cached recognition entry while updating hit counter.
46
+ #
47
+ # @param lookup_key [String] The cache lookup key.
48
+ # @return [Hash, nil] The cached recognition entry, or `nil` if not found.
49
+ def fetch_cached_recognition(lookup_key)
50
+ if (cached_result = @recognition_cache[lookup_key])
51
+ @cache_hits += 1
52
+ return cached_result
53
+ end
54
+ @cache_misses += 1
55
+ nil
56
+ end
57
+
58
+ # Cache insertion with simple segment eviction (25% oldest).
59
+ #
60
+ # Adds a new entry to the recognition cache, evicting the oldest 25% of entries
61
+ # if the cache exceeds its maximum size.
62
+ #
63
+ # @param cache_key [String] The cache key.
64
+ # @param entry [Hash] The cache entry.
65
+ # @return [void]
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)
70
+ end
71
+ end
72
+ @recognition_cache[cache_key] = entry
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRoutes
4
+ class RouteSet
5
+ # CollectionHelpers: extracted route collection and enumeration helpers
6
+ # to keep RouteSet implementation small.
7
+ #
8
+ # This module provides methods for managing and querying routes within a
9
+ # `RouteSet`. It includes functionality for adding, finding, clearing, and
10
+ # enumerating routes, as well as managing named routes and caches.
11
+ module CollectionHelpers
12
+ # Add a route object to internal structures.
13
+ #
14
+ # This method adds a route to the internal route collection, updates the
15
+ # radix tree for fast path/method lookups, and registers the route in the
16
+ # named routes collection if it has a name.
17
+ #
18
+ # @param route [Route] The route to add.
19
+ # @return [Route] The added route.
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
+ return route if @routes.include?(route) # Prevent duplicate insertion
24
+
25
+ @routes << route
26
+ route
27
+ end
28
+ alias add_route add_to_collection
29
+
30
+ # Register a newly created Route (called from RouteUtility#define).
31
+ #
32
+ # This method initializes the route collection if it is not already set
33
+ # and adds the given route to the collection.
34
+ #
35
+ # @param route [Route] The route to register.
36
+ # @return [Route] The registered route.
37
+ def register(route)
38
+ add_to_collection(route)
39
+ end
40
+
41
+ # Find any route (no params) for a method/path.
42
+ #
43
+ # This method searches the radix tree for a route matching the given HTTP
44
+ # method and path.
45
+ #
46
+ # @param http_method [String, Symbol] The HTTP method (e.g., `:get`, `:post`).
47
+ # @param path [String] The path to match.
48
+ # @return [Route, nil] The matching route, or `nil` if no match is found.
49
+ def find_route(http_method, path)
50
+ route, _params = @radix_tree.find(path, http_method)
51
+ route
52
+ end
53
+
54
+ # Retrieve a named route.
55
+ #
56
+ # This method retrieves a route by its name from the named routes collection.
57
+ # If no route is found, it raises a `RouteNotFound` error.
58
+ #
59
+ # @param name [Symbol] The name of the route.
60
+ # @return [Route] The named route.
61
+ # @raise [RouteNotFound] If no route with the given name is found.
62
+ def find_named_route(name)
63
+ route = @named_routes[name]
64
+ raise RouteNotFound, "No route named '#{name}'" unless route
65
+
66
+ route
67
+ end
68
+
69
+ # Clear all routes and caches.
70
+ #
71
+ # This method clears the internal route collection, named routes, recognition
72
+ # cache, and radix tree. It also resets cache hit/miss counters and clears
73
+ # the global request key cache.
74
+ #
75
+ # @return [void]
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!
84
+ end
85
+
86
+ # Get the number of routes.
87
+ #
88
+ # @return [Integer] The number of routes in the collection.
89
+ def size
90
+ @routes.size
91
+ end
92
+
93
+ # Check if the route collection is empty.
94
+ #
95
+ # @return [Boolean] `true` if the collection is empty, `false` otherwise.
96
+ def empty?
97
+ @routes.empty?
98
+ end
99
+
100
+ # Enumerate routes.
101
+ #
102
+ # This method yields each route in the collection to the given block. If no
103
+ # block is provided, it returns an enumerator.
104
+ #
105
+ # @yield [route] Yields each route in the collection.
106
+ # @return [Enumerator, self] An enumerator if no block is given, or `self`.
107
+ def each(&block)
108
+ return enum_for(:each) unless block
109
+
110
+ @routes.each(&block)
111
+ self
112
+ end
113
+
114
+ # Test membership.
115
+ #
116
+ # This method checks if the given route is included in the route collection.
117
+ #
118
+ # @param route [Route] The route to check.
119
+ # @return [Boolean] `true` if the route is in the collection, `false` otherwise.
120
+ def include?(route)
121
+ @routes.include?(route)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -1,179 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'radix_tree'
1
4
  require_relative 'utility/key_builder_utility'
5
+ require_relative 'utility/method_utility'
6
+ require_relative 'route_set/cache_helpers'
7
+ require_relative 'route_set/collection_helpers'
8
+ require_relative 'route/param_support'
2
9
 
3
10
  module RubyRoutes
11
+ # RouteSet
12
+ #
13
+ # Collection + lookup facade for Route instances.
14
+ #
15
+ # Responsibilities:
16
+ # - Hold all defined routes (ordered).
17
+ # - Index named routes.
18
+ # - Provide fast recognition (method + path → route, params) with
19
+ # a small in‑memory recognition cache.
20
+ # - Delegate structural path matching to an internal RadixTree.
21
+ #
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).
27
+ #
28
+ # @api public (primary integration surface)
4
29
  class RouteSet
5
30
  attr_reader :routes
6
31
 
7
32
  include RubyRoutes::Utility::KeyBuilderUtility
8
-
33
+ include RubyRoutes::Utility::MethodUtility
34
+ include RubyRoutes::RouteSet::CacheHelpers
35
+ include RubyRoutes::Route::ParamSupport
36
+ include RubyRoutes::RouteSet::CollectionHelpers
37
+
38
+ # Initialize empty collection and caches.
39
+ #
40
+ # @return [void]
9
41
  def initialize
10
- @routes = []
11
- @named_routes = {}
12
- @recognition_cache = {}
13
- @recognition_cache_max = 2048
14
- @cache_hits = 0
15
- @cache_misses = 0
16
- @radix_tree = RadixTree.new
17
- end
18
-
19
- def add_to_collection(route)
20
- @routes << route
21
- @radix_tree.add(route.path, route.methods, route)
22
- @named_routes[route.name] = route if route.named?
23
- end
24
-
25
- alias_method :add_route, :add_to_collection
26
-
27
- def find_route(method, path)
28
- route, _ = @radix_tree.find(path, method)
29
- route
30
- end
31
-
32
- def find_named_route(name)
33
- route = @named_routes[name]
34
- raise RouteNotFound.new("No route named '#{name}'") unless route
35
- route
36
- end
37
-
38
- FAST_METHOD_MAP = {
39
- get: 'GET', post: 'POST', put: 'PUT', patch: 'PATCH',
40
- delete: 'DELETE', head: 'HEAD', options: 'OPTIONS'
41
- }.freeze
42
-
43
- def normalize_method_input(method)
44
- case method
45
- when Symbol
46
- FAST_METHOD_MAP[method] || method.to_s.upcase
47
- when String
48
- # Fast path: assume already correct; fallback only for common lowercase
49
- return method if method.length <= 6 && method == method.upcase
50
- FAST_METHOD_MAP[method.downcase.to_sym] || method.upcase
51
- else
52
- s = method.to_s
53
- FAST_METHOD_MAP[s.downcase.to_sym] || s.upcase
54
- end
55
- end
56
- private :normalize_method_input
57
-
58
- def match(method, path)
59
- m = normalize_method_input(method)
60
- raw = path.to_s
61
- cache_key = cache_key_for_request(m, raw)
62
-
63
- # Single cache lookup with proper hit accounting
64
- if (hit = @recognition_cache[cache_key])
65
- @cache_hits += 1
66
- return hit
67
- end
68
-
69
- @cache_misses += 1
70
-
71
- path_without_query, _qs = raw.split('?', 2)
72
-
73
- # Use normalized method (m) for trie lookup
74
- route, params = @radix_tree.find(path_without_query, m)
75
- return nil unless route
76
-
77
- merge_query_params(route, raw, params)
78
-
79
- if route.respond_to?(:defaults) && route.defaults
80
- route.defaults.each { |k,v| params[k.to_s] = v unless params.key?(k.to_s) }
42
+ setup_caches
43
+ setup_radix_tree
44
+ end
45
+
46
+ # Recognize a request (method + path) returning route + params.
47
+ #
48
+ # @param http_method [String, Symbol] The HTTP method (e.g., "GET").
49
+ # @param path [String] The request path.
50
+ # @return [Hash, nil] A hash containing the matched route and parameters, or `nil` if no match is found.
51
+ def match(http_method, path)
52
+ normalized_method = normalize_method_for_match(http_method)
53
+ raw_path = path.to_s
54
+ lookup_key = cache_key_for_request(normalized_method, raw_path)
55
+
56
+ if (cached_result = fetch_cached_recognition(lookup_key))
57
+ return cached_result
81
58
  end
82
59
 
83
- result = {
84
- route: route,
85
- params: params,
86
- controller: route.controller,
87
- action: route.action
88
- }
89
-
90
- insert_cache_entry(cache_key, result)
60
+ result = perform_match(normalized_method, raw_path)
61
+ insert_cache_entry(lookup_key, result) if result
91
62
  result
92
63
  end
93
64
 
65
+ # Convenience alias for Rack‑style recognizer.
66
+ #
67
+ # @param path [String] The request path.
68
+ # @param method [String, Symbol] The HTTP method (default: "GET").
69
+ # @return [Hash, nil] A hash containing the matched route and parameters, or `nil` if no match is found.
94
70
  def recognize_path(path, method = 'GET')
95
71
  match(method, path)
96
72
  end
97
73
 
74
+ # Generate path via named route.
75
+ #
76
+ # @param name [Symbol, String] The name of the route.
77
+ # @param params [Hash] The parameters for path generation.
78
+ # @return [String] The generated path.
98
79
  def generate_path(name, params = {})
99
80
  route = find_named_route(name)
100
81
  route.generate_path(params)
101
82
  end
102
83
 
84
+ # Generate path from a direct route reference.
85
+ #
86
+ # @param route [Route] The route instance.
87
+ # @param params [Hash] The parameters for path generation.
88
+ # @return [String] The generated path.
103
89
  def generate_path_from_route(route, params = {})
104
90
  route.generate_path(params)
105
91
  end
106
92
 
107
- def clear!
108
- @routes.clear
109
- @named_routes.clear
110
- @recognition_cache.clear
111
- @cache_hits = 0
112
- @cache_misses = 0
113
- # Create a new radix tree since we can't clear it
114
- @radix_tree = RadixTree.new
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
115
103
  end
116
104
 
117
- def size
118
- @routes.size
119
- end
105
+ private
120
106
 
121
- def empty?
122
- @routes.empty?
107
+ # Set up the radix tree for structural path matching.
108
+ #
109
+ # @return [void]
110
+ def setup_radix_tree
111
+ @radix_tree = RadixTree.new
123
112
  end
124
113
 
125
- def cache_stats
126
- lookups = @cache_hits + @cache_misses
127
- {
128
- hits: @cache_hits,
129
- misses: @cache_misses,
130
- hit_rate: lookups.zero? ? 0.0 : (@cache_hits.to_f / lookups * 100.0),
131
- size: @recognition_cache.size
132
- }
114
+ # Normalize the HTTP method for matching.
115
+ #
116
+ # @param http_method [String, Symbol] The HTTP method.
117
+ # @return [String] The normalized HTTP method.
118
+ def normalize_method_for_match(http_method)
119
+ if http_method.is_a?(String) && normalize_http_method(http_method).equal?(http_method)
120
+ http_method
121
+ else
122
+ normalize_http_method(http_method)
123
+ end
133
124
  end
134
125
 
135
- def each(&block)
136
- @routes.each(&block)
137
- end
126
+ # Perform the route matching process.
127
+ #
128
+ # @param normalized_method [String] The normalized HTTP method.
129
+ # @param raw_path [String] The raw request path.
130
+ # @return [Hash, nil] A hash containing the matched route and parameters, or `nil` if no match is found.
131
+ def perform_match(normalized_method, raw_path)
132
+ path_without_query, _query = raw_path.split('?', 2)
133
+ matched_route, extracted_params = @radix_tree.find(path_without_query, normalized_method)
134
+ return nil unless matched_route
135
+
136
+ # Ensure we have a mutable hash for merging defaults / query params.
137
+ if extracted_params.nil?
138
+ extracted_params = {}
139
+ elsif extracted_params.frozen?
140
+ extracted_params = extracted_params.dup
141
+ end
138
142
 
139
- def include?(route)
140
- @routes.include?(route)
143
+ merge_query_params(matched_route, raw_path, extracted_params)
144
+ merge_defaults(matched_route, extracted_params)
145
+ build_match_result(matched_route, extracted_params)
141
146
  end
142
147
 
143
- private
148
+ # Merge default parameters into the extracted parameters.
149
+ #
150
+ # @param matched_route [Route] The matched route.
151
+ # @param extracted_params [Hash] The extracted parameters.
152
+ # @return [void]
153
+ def merge_defaults(matched_route, extracted_params)
154
+ return unless matched_route.respond_to?(:defaults) && matched_route.defaults
144
155
 
145
- def insert_cache_entry(key, value)
146
- # unchanged cache insert (key already frozen & reusable)
147
- if @recognition_cache.size >= @recognition_cache_max
148
- @recognition_cache.keys.first(@recognition_cache_max / 4).each { |k| @recognition_cache.delete(k) }
149
- end
150
- @recognition_cache[key] = value
156
+ matched_route.defaults.each { |key, value| extracted_params[key] = value unless extracted_params.key?(key) }
151
157
  end
152
158
 
153
- # Add the missing method for merging query params
154
- def merge_query_params(route, path, params)
155
- # Check for query string
156
- if path.to_s.include?('?')
157
- if route.respond_to?(:parse_query_params)
158
- query_params = route.parse_query_params(path)
159
- params.merge!(query_params) if query_params
160
- elsif route.respond_to?(:query_params)
161
- query_params = route.query_params(path)
162
- params.merge!(query_params) if query_params
163
- end
164
- end
159
+ # Build the match result hash.
160
+ #
161
+ # @param matched_route [Route] The matched route.
162
+ # @param extracted_params [Hash] The extracted parameters.
163
+ # @return [Hash] A hash containing the matched route, parameters, controller, and action.
164
+ def build_match_result(matched_route, extracted_params)
165
+ {
166
+ route: matched_route,
167
+ params: extracted_params,
168
+ controller: matched_route.controller,
169
+ action: matched_route.action
170
+ }
165
171
  end
166
172
 
167
- # Add thread-local params pool methods
168
- def get_thread_local_params
173
+ # Obtain a pooled hash for temporary parameters.
174
+ #
175
+ # @return [Hash] A thread-local hash for temporary parameter storage.
176
+ def thread_local_params
169
177
  thread_params = Thread.current[:ruby_routes_params_pool] ||= []
170
- if thread_params.empty?
171
- {}
172
- else
173
- thread_params.pop.clear
174
- end
178
+ thread_params.empty? ? {} : thread_params.pop.clear
175
179
  end
176
180
 
181
+ # Return a parameters hash to the thread-local pool.
182
+ #
183
+ # @param params [Hash] The parameters hash to return.
184
+ # @return [void]
177
185
  def return_params_to_pool(params)
178
186
  params.clear
179
187
  thread_pool = Thread.current[:ruby_routes_params_pool] ||= []
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../constant'
4
+
5
+ module RubyRoutes
6
+ class Router
7
+ # BuildHelpers
8
+ #
9
+ # Small, focused utilities used while constructing routes and routers.
10
+ module BuildHelpers
11
+ # Build the router from recorded calls.
12
+ #
13
+ # This method creates a new `Router` instance, validates the recorded calls,
14
+ # finalizes the router (making it immutable), and returns the frozen instance.
15
+ #
16
+ # @return [Router] The finalized router.
17
+ def build
18
+ router = Router.new
19
+ validate_calls(@recorded_calls)
20
+ router.finalize!
21
+ router
22
+ end
23
+
24
+ # Build route options.
25
+ #
26
+ # This method creates a copy of the base options and conditionally adds
27
+ # the `:via` and `:to` keys if they are not already present or need to be overridden.
28
+ #
29
+ # @param base_options [Hash] The base options for the route.
30
+ # @param via_sym [Symbol, nil] The HTTP method (e.g., `:get`, `:post`).
31
+ # @param to_string [String, nil] The controller#action string (e.g., `"users#index"`).
32
+ # @return [Hash] The updated route options.
33
+ def build_route_options(base_options, via_sym = nil, to_string = nil)
34
+ force_via = !via_sym.nil? && base_options[:via] != via_sym
35
+ add_to = !to_string.nil? && !base_options.key?(:to)
36
+ return base_options unless force_via || add_to
37
+
38
+ options_copy = base_options.dup
39
+ options_copy[:via] = via_sym if force_via
40
+ options_copy[:to] = to_string if add_to
41
+ options_copy
42
+ end
43
+
44
+ # Helper: Build collection routes inside a resource scope.
45
+ #
46
+ # This method defines routes for collection-level actions (e.g., `index`, `new`, `create`)
47
+ # within the scope of a resource.
48
+ #
49
+ # @param opts [Hash] The options to apply to the routes.
50
+ # @param to_index [String] The controller#action string for the `index` action.
51
+ # @param to_new [String] The controller#action string for the `new` action.
52
+ # @param to_create [String] The controller#action string for the `create` action.
53
+ # @return [void]
54
+ def build_collection_routes(opts, to_index, to_new, to_create)
55
+ add_route('', build_route_options(opts, :get, to_index))
56
+ add_route('/new', build_route_options(opts, :get, to_new))
57
+ add_route('', build_route_options(opts, :post, to_create))
58
+ end
59
+
60
+ # Helper: Build member routes inside a resource scope.
61
+ #
62
+ # This method defines routes for member-level actions (e.g., `show`, `edit`, `update`, `destroy`)
63
+ # within the scope of a resource.
64
+ #
65
+ # @param opts [Hash] The options to apply to the routes.
66
+ # @param to_show [String] The controller#action string for the `show` action.
67
+ # @param to_edit [String] The controller#action string for the `edit` action.
68
+ # @param to_update [String] The controller#action string for the `update` action.
69
+ # @param to_destroy [String] The controller#action string for the `destroy` action.
70
+ # @return [void]
71
+ def build_member_routes(opts, to_show, to_edit, to_update, to_destroy)
72
+ add_route('/:id', build_route_options(opts, :get, to_show))
73
+ add_route('/:id/edit', build_route_options(opts, :get, to_edit))
74
+ add_route('/:id', opts.merge(via: %i[put patch], to: to_update))
75
+ add_route('/:id', build_route_options(opts, :delete, to_destroy))
76
+ end
77
+
78
+ private
79
+
80
+ # Validate the recorded calls.
81
+ #
82
+ # This method ensures that all recorded calls use valid router methods
83
+ # as defined in `RubyRoutes::Constant::RECORDED_METHODS`.
84
+ #
85
+ # @param recorded_calls [Array<Array(Symbol, Array, Proc|NilClass)>]
86
+ # The recorded calls to validate.
87
+ # @raise [ArgumentError] If any recorded call uses an invalid method.
88
+ # @return [void]
89
+ def validate_calls(recorded_calls)
90
+ allowed_router_methods = RubyRoutes::Constant::RECORDED_METHODS
91
+ recorded_calls.each do |(router_method, _arguments, _definition_block)|
92
+ unless router_method.is_a?(Symbol) && allowed_router_methods.include?(router_method)
93
+ raise ArgumentError, "Invalid router method: #{router_method.inspect}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end