ruby_routes 2.4.0 → 2.6.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -23
  3. data/lib/ruby_routes/constant.rb +20 -3
  4. data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -1
  5. data/lib/ruby_routes/node.rb +15 -87
  6. data/lib/ruby_routes/radix_tree/finder.rb +79 -52
  7. data/lib/ruby_routes/radix_tree/inserter.rb +2 -54
  8. data/lib/ruby_routes/radix_tree/traversal_strategy/base.rb +18 -0
  9. data/lib/ruby_routes/radix_tree/traversal_strategy/generic_loop.rb +25 -0
  10. data/lib/ruby_routes/radix_tree/traversal_strategy/unrolled.rb +45 -0
  11. data/lib/ruby_routes/radix_tree/traversal_strategy.rb +26 -0
  12. data/lib/ruby_routes/radix_tree.rb +12 -62
  13. data/lib/ruby_routes/route/check_helpers.rb +3 -3
  14. data/lib/ruby_routes/route/constraint_validator.rb +24 -1
  15. data/lib/ruby_routes/route/matcher.rb +11 -0
  16. data/lib/ruby_routes/route/param_support.rb +9 -7
  17. data/lib/ruby_routes/route/path_builder.rb +11 -6
  18. data/lib/ruby_routes/route/path_generation.rb +5 -1
  19. data/lib/ruby_routes/route/small_lru.rb +43 -2
  20. data/lib/ruby_routes/route/validation_helpers.rb +6 -36
  21. data/lib/ruby_routes/route/warning_helpers.rb +7 -5
  22. data/lib/ruby_routes/route.rb +35 -55
  23. data/lib/ruby_routes/route_set/cache_helpers.rb +32 -13
  24. data/lib/ruby_routes/route_set/collection_helpers.rb +15 -14
  25. data/lib/ruby_routes/route_set.rb +32 -69
  26. data/lib/ruby_routes/router/build_helpers.rb +1 -1
  27. data/lib/ruby_routes/router/builder.rb +16 -33
  28. data/lib/ruby_routes/router/http_helpers.rb +13 -49
  29. data/lib/ruby_routes/router/resource_helpers.rb +26 -39
  30. data/lib/ruby_routes/router/scope_helpers.rb +26 -14
  31. data/lib/ruby_routes/router.rb +41 -24
  32. data/lib/ruby_routes/segment.rb +3 -3
  33. data/lib/ruby_routes/segments/base_segment.rb +8 -0
  34. data/lib/ruby_routes/segments/static_segment.rb +3 -1
  35. data/lib/ruby_routes/strategies/base.rb +18 -0
  36. data/lib/ruby_routes/strategies/hash_based_strategy.rb +33 -0
  37. data/lib/ruby_routes/strategies/hybrid_strategy.rb +70 -0
  38. data/lib/ruby_routes/strategies/radix_tree_strategy.rb +24 -0
  39. data/lib/ruby_routes/strategies.rb +5 -0
  40. data/lib/ruby_routes/utility/key_builder_utility.rb +4 -26
  41. data/lib/ruby_routes/utility/method_utility.rb +18 -17
  42. data/lib/ruby_routes/utility/path_utility.rb +18 -7
  43. data/lib/ruby_routes/version.rb +1 -1
  44. data/lib/ruby_routes.rb +3 -1
  45. metadata +11 -2
  46. data/lib/ruby_routes/string_extensions.rb +0 -65
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../route/small_lru'
4
+
3
5
  module RubyRoutes
4
6
  class RouteSet
5
7
  # CacheHelpers: extracted cache, request-key, and eviction logic to reduce
@@ -8,6 +10,8 @@ module RubyRoutes
8
10
  # This module provides methods for managing caches, request keys, and
9
11
  # implementing eviction policies for route recognition.
10
12
  module CacheHelpers
13
+
14
+ attr_reader :named_routes, :small_lru
11
15
  # Recognition cache statistics.
12
16
  #
13
17
  # @return [Hash] A hash containing:
@@ -16,11 +20,11 @@ module RubyRoutes
16
20
  # - `:hit_rate` [Float] The cache hit rate as a percentage.
17
21
  # - `:size` [Integer] The current size of the recognition cache.
18
22
  def cache_stats
19
- total_requests = @cache_hits + @cache_misses
23
+ total_requests = @small_lru.hits + @small_lru.misses
20
24
  {
21
- hits: @cache_hits,
22
- misses: @cache_misses,
23
- hit_rate: total_requests.zero? ? 0.0 : (@cache_hits.to_f / total_requests * 100.0),
25
+ hits: @small_lru.hits,
26
+ misses: @small_lru.misses,
27
+ hit_rate: total_requests.zero? ? 0.0 : (@small_lru.hits.to_f / total_requests * 100.0),
24
28
  size: @recognition_cache.size
25
29
  }
26
30
  end
@@ -37,9 +41,11 @@ module RubyRoutes
37
41
  @routes = []
38
42
  @named_routes = {}
39
43
  @recognition_cache = {}
40
- @recognition_cache_max = 2048
41
- @cache_hits = 0
42
- @cache_misses = 0
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
43
49
  end
44
50
 
45
51
  # Fetch cached recognition entry while updating hit counter.
@@ -48,10 +54,10 @@ module RubyRoutes
48
54
  # @return [Hash, nil] The cached recognition entry, or `nil` if not found.
49
55
  def fetch_cached_recognition(lookup_key)
50
56
  if (cached_result = @recognition_cache[lookup_key])
51
- @cache_hits += 1
57
+ @small_lru.increment_hits
52
58
  return cached_result
53
59
  end
54
- @cache_misses += 1
60
+ @small_lru.increment_misses
55
61
  nil
56
62
  end
57
63
 
@@ -64,12 +70,25 @@ module RubyRoutes
64
70
  # @param entry [Hash] The cache entry.
65
71
  # @return [void]
66
72
  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)
73
+ @cache_mutex.synchronize do
74
+ if @recognition_cache.size >= @recognition_cache_max
75
+ # Calculate how many to keep (3/4 of max, rounded down)
76
+ keep_count = (@recognition_cache_max * 3 / 4).to_i
77
+
78
+ # Get the keys to keep (newest 75%, assuming insertion order)
79
+ keys_to_keep = @recognition_cache.keys.last(keep_count)
80
+
81
+ # Get the entries to keep
82
+ entries_to_keep = @recognition_cache.slice(*keys_to_keep)
83
+
84
+ # Clear the entire cache (evicts the oldest 25%)
85
+ @recognition_cache.clear
86
+
87
+ # Re-add the kept entries (3/4)
88
+ @recognition_cache.merge!(entries_to_keep)
70
89
  end
90
+ @recognition_cache[cache_key] = entry
71
91
  end
72
- @recognition_cache[cache_key] = entry
73
92
  end
74
93
  end
75
94
  end
@@ -12,16 +12,16 @@ module RubyRoutes
12
12
  # Add a route object to internal structures.
13
13
  #
14
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
15
+ # matching strategy for fast path/method lookups, and registers the route in the
16
16
  # named routes collection if it has a name.
17
17
  #
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
+ @strategy.add(route)
25
25
  @routes << route
26
26
  route
27
27
  end
@@ -40,14 +40,14 @@ module RubyRoutes
40
40
 
41
41
  # Find any route (no params) for a method/path.
42
42
  #
43
- # This method searches the radix tree for a route matching the given HTTP
43
+ # This method searches the matching strategy for a route matching the given HTTP
44
44
  # method and path.
45
45
  #
46
46
  # @param http_method [String, Symbol] The HTTP method (e.g., `:get`, `:post`).
47
47
  # @param path [String] The path to match.
48
48
  # @return [Route, nil] The matching route, or `nil` if no match is found.
49
49
  def find_route(http_method, path)
50
- route, _params = @radix_tree.find(path, http_method)
50
+ route, _params = @strategy.find(path, http_method)
51
51
  route
52
52
  end
53
53
 
@@ -69,18 +69,19 @@ module RubyRoutes
69
69
  # Clear all routes and caches.
70
70
  #
71
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
72
+ # cache, and matching strategy. It also resets cache hit/miss counters and clears
73
73
  # the global request key cache.
74
74
  #
75
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!
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
84
85
  end
85
86
 
86
87
  # Get the number of routes.
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'radix_tree'
3
+ require_relative 'strategies'
4
4
  require_relative 'utility/key_builder_utility'
5
5
  require_relative 'utility/method_utility'
6
6
  require_relative 'route_set/cache_helpers'
7
7
  require_relative 'route_set/collection_helpers'
8
8
  require_relative 'route/param_support'
9
+ require_relative 'route/path_generation'
9
10
 
10
11
  module RubyRoutes
11
12
  # RouteSet
@@ -17,7 +18,7 @@ module RubyRoutes
17
18
  # - Index named routes.
18
19
  # - Provide fast recognition (method + path → route, params) with
19
20
  # a small in‑memory recognition cache.
20
- # - Delegate structural path matching to an internal RadixTree.
21
+ # - Delegate structural path matching to a configurable strategy.
21
22
  #
22
23
  # Thread Safety:
23
24
  # - RouteSet instances are not fully thread-safe for modifications.
@@ -33,14 +34,16 @@ module RubyRoutes
33
34
  include RubyRoutes::Utility::MethodUtility
34
35
  include RubyRoutes::RouteSet::CacheHelpers
35
36
  include RubyRoutes::Route::ParamSupport
37
+ include RubyRoutes::Route::PathGeneration
36
38
  include RubyRoutes::RouteSet::CollectionHelpers
37
39
 
38
40
  # Initialize empty collection and caches.
39
41
  #
42
+ # @param strategy [Class] The matching strategy to use.
40
43
  # @return [void]
41
- def initialize
44
+ def initialize(strategy: Strategies::HybridStrategy)
42
45
  setup_caches
43
- setup_radix_tree
46
+ setup_strategy(strategy)
44
47
  end
45
48
 
46
49
  # Recognize a request (method + path) returning route + params.
@@ -49,7 +52,7 @@ module RubyRoutes
49
52
  # @param path [String] The request path.
50
53
  # @return [Hash, nil] A hash containing the matched route and parameters, or `nil` if no match is found.
51
54
  def match(http_method, path)
52
- normalized_method = normalize_method_for_match(http_method)
55
+ normalized_method = normalize_http_method(http_method)
53
56
  raw_path = path.to_s
54
57
  lookup_key = cache_key_for_request(normalized_method, raw_path)
55
58
 
@@ -77,8 +80,7 @@ module RubyRoutes
77
80
  # @param params [Hash] The parameters for path generation.
78
81
  # @return [String] The generated path.
79
82
  def generate_path(name, params = {})
80
- route = find_named_route(name)
81
- route.generate_path(params)
83
+ generate_path_from_route(find_named_route(name), params)
82
84
  end
83
85
 
84
86
  # Generate path from a direct route reference.
@@ -90,37 +92,19 @@ module RubyRoutes
90
92
  route.generate_path(params)
91
93
  end
92
94
 
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
95
+ def clear_counters!
96
+ clear_routes_and_caches!
103
97
  end
104
98
 
105
99
  private
106
100
 
107
- # Set up the radix tree for structural path matching.
101
+ # Set up the matching strategy.
108
102
  #
103
+ # @param strategy [Class] The matching strategy class.
109
104
  # @return [void]
110
- def setup_radix_tree
111
- @radix_tree = RadixTree.new
112
- end
113
-
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
105
+ def setup_strategy(strategy)
106
+ @strategy_class = strategy
107
+ @strategy = @strategy_class.new
124
108
  end
125
109
 
126
110
  # Perform the route matching process.
@@ -129,31 +113,28 @@ module RubyRoutes
129
113
  # @param raw_path [String] The raw request path.
130
114
  # @return [Hash, nil] A hash containing the matched route and parameters, or `nil` if no match is found.
131
115
  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)
116
+ matched_route, path_params = @strategy.find(raw_path, normalized_method)
134
117
  return nil unless matched_route
135
118
 
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
142
-
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)
119
+ final_params = build_final_params(matched_route, path_params, raw_path)
120
+ build_match_result(matched_route, final_params)
146
121
  end
147
122
 
148
- # Merge default parameters into the extracted parameters.
123
+ # Build the final parameters hash by merging path, query, and default params.
149
124
  #
150
125
  # @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
155
-
156
- matched_route.defaults.each { |key, value| extracted_params[key] = value unless extracted_params.key?(key) }
126
+ # @param path_params [Hash] The parameters extracted from the path.
127
+ # @param raw_path [String] The full request path including query string.
128
+ # @return [Hash] The final, merged parameters hash.
129
+ def build_final_params(matched_route, path_params, raw_path)
130
+ # Optimized merge order: defaults -> path -> query
131
+ # Start with defaults, which have the lowest precedence.
132
+ final_params = matched_route.defaults.dup
133
+ # Merge path parameters, which override defaults.
134
+ final_params.merge!(path_params) if path_params
135
+ matched_route.merge_query_params_into_hash(final_params, raw_path, nil)
136
+
137
+ final_params
157
138
  end
158
139
 
159
140
  # Build the match result hash.
@@ -169,23 +150,5 @@ module RubyRoutes
169
150
  action: matched_route.action
170
151
  }
171
152
  end
172
-
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
177
- thread_params = Thread.current[:ruby_routes_params_pool] ||= []
178
- thread_params.empty? ? {} : thread_params.pop.clear
179
- end
180
-
181
- # Return a parameters hash to the thread-local pool.
182
- #
183
- # @param params [Hash] The parameters hash to return.
184
- # @return [void]
185
- def return_params_to_pool(params)
186
- params.clear
187
- thread_pool = Thread.current[:ruby_routes_params_pool] ||= []
188
- thread_pool << params if thread_pool.size < 10 # Limit pool size
189
- end
190
153
  end
191
154
  end
@@ -89,7 +89,7 @@ module RubyRoutes
89
89
  def validate_calls(recorded_calls)
90
90
  allowed_router_methods = RubyRoutes::Constant::RECORDED_METHODS
91
91
  recorded_calls.each do |(router_method, _arguments, _definition_block)|
92
- unless router_method.is_a?(Symbol) && allowed_router_methods.include?(router_method)
92
+ unless allowed_router_methods.include?(router_method)
93
93
  raise ArgumentError, "Invalid router method: #{router_method.inspect}"
94
94
  end
95
95
  end
@@ -32,19 +32,6 @@ module RubyRoutes
32
32
  class Builder
33
33
  include RubyRoutes::Router::BuildHelpers
34
34
 
35
- # Array of recorded calls: [method_symbol, args_array, block].
36
- #
37
- # Each tuple contains:
38
- # - The method name (as a Symbol).
39
- # - The arguments (as an Array).
40
- # - The block (as a Proc or `nil`).
41
- #
42
- # @return [Array<Array(Symbol, Array, Proc|NilClass)>]
43
- # A snapshot of the recorded calls to avoid external mutation.
44
- def recorded_calls
45
- @recorded_calls.dup.freeze
46
- end
47
-
48
35
  # Initialize the Builder.
49
36
  #
50
37
  # This method initializes the `@recorded_calls` array and optionally
@@ -57,6 +44,22 @@ module RubyRoutes
57
44
  instance_eval(&definition_block) if definition_block
58
45
  end
59
46
 
47
+ # Array of recorded calls: [method_symbol, args_array, block].
48
+ #
49
+ # Each tuple contains:
50
+ # - The method name (as a Symbol).
51
+ # - The arguments (as an Array).
52
+ # - The block (as a Proc or `nil`).
53
+ #
54
+ # @return [Array<Array(Symbol, Array, Proc|NilClass)>]
55
+ # A snapshot of the recorded calls to avoid external mutation.
56
+ def recorded_calls
57
+ # Deep-copy each recorded call’s args array and freeze the result to prevent mutation
58
+ @recorded_calls
59
+ .map { |(method_name, args, block)| [method_name, args.dup.freeze, block] }
60
+ .freeze
61
+ end
62
+
60
63
  # ---- DSL Recording -------------------------------------------------
61
64
  # Dynamically define methods for all DSL methods specified in
62
65
  # `RubyRoutes::Constant::RECORDED_METHODS`. Each method records its
@@ -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
@@ -22,54 +22,13 @@ module RubyRoutes
22
22
 
23
23
  # ---- HTTP Verb Helpers -------------------------------------------------
24
24
 
25
- # Define a GET route.
26
- #
27
- # @param path [String] The path for the route.
28
- # @param options [Hash] The options for the route.
29
- # @return [Router] Returns self for chaining.
30
- def get(path, options = {})
31
- add_route(path, build_route_options(options, :get))
32
- self
33
- end
34
-
35
- # Define a POST route.
36
- #
37
- # @param path [String] The path for the route.
38
- # @param options [Hash] The options for the route.
39
- # @return [Router] Returns self for chaining.
40
- def post(path, options = {})
41
- add_route(path, build_route_options(options, :post))
42
- self
43
- end
44
-
45
- # Define a PUT route.
46
- #
47
- # @param path [String] The path for the route.
48
- # @param options [Hash] The options for the route.
49
- # @return [Router] Returns self for chaining.
50
- def put(path, options = {})
51
- add_route(path, build_route_options(options, :put))
52
- self
53
- end
54
-
55
- # Define a PATCH route.
56
- #
57
- # @param path [String] The path for the route.
58
- # @param options [Hash] The options for the route.
59
- # @return [Router] Returns self for chaining.
60
- def patch(path, options = {})
61
- add_route(path, build_route_options(options, :patch))
62
- self
63
- end
64
-
65
- # Define a DELETE route.
66
- #
67
- # @param path [String] The path for the route.
68
- # @param options [Hash] The options for the route.
69
- # @return [Router] Returns self for chaining.
70
- def delete(path, options = {})
71
- add_route(path, build_route_options(options, :delete))
72
- self
25
+ # Metaprogram `get`, `post`, `put`, `patch`, `delete` for DRYness.
26
+ # These methods define a route for a specific HTTP verb.
27
+ %i[get post put patch delete].each do |verb|
28
+ define_method(verb) do |path, options = {}|
29
+ add_route(path, build_route_options(options, verb))
30
+ self
31
+ end
73
32
  end
74
33
 
75
34
  # Define a route for multiple HTTP methods.
@@ -83,7 +42,12 @@ module RubyRoutes
83
42
  via = options[:via]
84
43
  raise ArgumentError, 'match requires :via (e.g., via: [:get, :post])' if via.nil? || Array(via).empty?
85
44
 
86
- add_route(path, options)
45
+ normalized_via = Array(via)
46
+ opts = options.dup
47
+ # Keep :via in opts only if BuildHelpers expects it; otherwise delete.
48
+ # If BuildHelpers infers from the second arg, delete it:
49
+ opts.delete(:via)
50
+ add_route(path, build_route_options(opts, normalized_via))
87
51
  self
88
52
  end
89
53
 
@@ -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.
@@ -26,31 +25,43 @@ module RubyRoutes
26
25
  # @return [void]
27
26
  def define_resource_routes(resource_name, options = {}, &nested_block)
28
27
  meta = resource_meta(resource_name, options)
29
- opts = prepare_options(options)
28
+ resource_opts = prepare_resource_options(options)
30
29
 
31
30
  push_scope(path: "/#{meta[:resource_path]}") do
32
- build_routes(opts, meta)
33
- handle_nested_option(options, opts)
31
+ define_resource_actions(resource_opts, meta[:controller])
32
+ handle_nested_option(options, resource_opts)
34
33
  apply_nested_block(nested_block)
35
34
  end
36
35
  end
37
36
 
38
- # Prepare options by removing the `:to` key if present.
37
+ private
38
+
39
+ # Prepare options for resource routes, removing the `:to` key to avoid conflicts.
40
+ # This avoids creating a new hash if it's not necessary.
39
41
  #
40
42
  # @param options [Hash] The options hash.
41
43
  # @return [Hash] The prepared options.
42
- def prepare_options(options)
43
- options.key?(:to) ? options.dup.tap { |h| h.delete(:to) } : options
44
+ def prepare_resource_options(options)
45
+ options.key?(:to) ? options.except(:to) : options
44
46
  end
45
47
 
46
- # Build collection and member routes for a resource.
48
+ # Defines the seven standard RESTful actions for a resource.
49
+ # This method is data-driven to reduce duplication and improve clarity.
47
50
  #
48
- # @param opts [Hash] The options hash.
49
- # @param meta [Hash] The resource metadata.
51
+ # @param resource_opts [Hash] The options for the resource routes.
52
+ # @param controller [String] The controller name for the actions.
53
+ # @param member_param [String] The parameter name for member routes (e.g., ':id').
50
54
  # @return [void]
51
- def build_routes(opts, meta)
52
- build_collection_routes(opts, meta[:to_index], meta[:to_new], meta[:to_create])
53
- build_member_routes(opts, meta[:to_show], meta[:to_edit], meta[:to_update], meta[:to_destroy])
55
+ def define_resource_actions(resource_opts, controller, member_param: ':id')
56
+ # Collection routes
57
+ add_route('', build_route_options(resource_opts, :get, "#{controller}#index"))
58
+ add_route('/new', build_route_options(resource_opts, :get, "#{controller}#new"))
59
+ add_route('', build_route_options(resource_opts, :post, "#{controller}#create"))
60
+ # Member routes
61
+ add_route("/#{member_param}", build_route_options(resource_opts, :get, "#{controller}#show"))
62
+ add_route("/#{member_param}/edit", build_route_options(resource_opts, :get, "#{controller}#edit"))
63
+ add_route("/#{member_param}", resource_opts.merge(via: %i[put patch], to: "#{controller}#update"))
64
+ add_route("/#{member_param}", build_route_options(resource_opts, :delete, "#{controller}#destroy"))
54
65
  end
55
66
 
56
67
  # Apply a nested block of routes within the scope of a resource.
@@ -102,36 +113,12 @@ module RubyRoutes
102
113
 
103
114
  nested_name = options[:nested].to_s
104
115
  nested_path = RubyRoutes::Utility::InflectorUtility.pluralize(nested_name)
105
- build_nested_routes(nested_path, opts)
106
- end
107
-
108
- # Build nested resource routes.
109
- #
110
- # @param nested_path [String] The path for the nested resource.
111
- # @param opts [Hash] The options hash.
112
- # @return [void]
113
- def build_nested_routes(nested_path, opts)
114
116
  push_scope(path: '/:id') do
115
117
  push_scope(path: "/#{nested_path}") do
116
- add_nested_routes(nested_path, opts)
118
+ define_resource_actions(opts, nested_path, member_param: ':nested_id')
117
119
  end
118
120
  end
119
121
  end
120
-
121
- # Add routes for a nested resource.
122
- #
123
- # @param nested_path [String] The path for the nested resource.
124
- # @param opts [Hash] The options hash.
125
- # @return [void]
126
- def add_nested_routes(nested_path, opts)
127
- add_route('', build_route_options(opts, :get, "#{nested_path}#index"))
128
- add_route('/new', build_route_options(opts, :get, "#{nested_path}#new"))
129
- add_route('', build_route_options(opts, :post, "#{nested_path}#create"))
130
- add_route('/:nested_id', build_route_options(opts, :get, "#{nested_path}#show"))
131
- add_route('/:nested_id/edit', build_route_options(opts, :get, "#{nested_path}#edit"))
132
- add_route('/:nested_id', opts.merge(via: %i[put patch], to: "#{nested_path}#update"))
133
- add_route('/:nested_id', build_route_options(opts, :delete, "#{nested_path}#destroy"))
134
- end
135
122
  end
136
123
  end
137
124
  end
@@ -61,21 +61,11 @@ module RubyRoutes
61
61
  # @param scoped_path [String] The path to prepend the scope's path to.
62
62
  # @return [void]
63
63
  def apply_path_scope(scope, scoped_path)
64
- path = scope[:path]&.to_s
65
- return if path.nil? || path.empty?
66
-
67
- if path.end_with?('/')
68
- if scoped_path.start_with?('/')
69
- scoped_path.prepend(path.chomp('/'))
70
- else
71
- scoped_path.prepend(path)
72
- end
73
- else
74
- scoped_path.prepend(scoped_path.start_with?('/') ? path : "#{path}/")
75
- end
64
+ scope_path = scope[:path]&.to_s
65
+ return if scope_path.nil? || scope_path.empty?
76
66
 
77
- # Normalize: Ensure the final path starts with '/'
78
- scoped_path.prepend('/') unless scoped_path.start_with?('/')
67
+ parts = [scope_path, scoped_path].map { |p| p.to_s.gsub(%r{^/|/$}, '') }.reject(&:empty?)
68
+ scoped_path.replace("/#{parts.join('/')}")
79
69
  end
80
70
 
81
71
  # Apply the module from a scope to the given options.
@@ -122,6 +112,28 @@ module RubyRoutes
122
112
 
123
113
  scoped_options[:constraints] = scope[:constraints].merge(scoped_options[:constraints] || {})
124
114
  end
115
+
116
+ # Get the current merged scope from the scope stack
117
+ #
118
+ # @return [Hash] The merged scope with combined namespaces
119
+ def current_scope
120
+ merged = {}
121
+ namespace_parts = []
122
+
123
+ @scope_stack.each do |scope|
124
+ if scope[:namespace]
125
+ namespace_parts << scope[:namespace].to_s
126
+ end
127
+ merged.merge!(scope)
128
+ end
129
+
130
+ # Combine namespaces for nested namespace support
131
+ if namespace_parts.any?
132
+ merged[:namespace] = namespace_parts.join('/')
133
+ end
134
+
135
+ merged
136
+ end
125
137
  end
126
138
  end
127
139
  end