ruby_routes 2.5.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -23
  3. data/lib/ruby_routes/constant.rb +18 -3
  4. data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -1
  5. data/lib/ruby_routes/node.rb +14 -87
  6. data/lib/ruby_routes/radix_tree/finder.rb +75 -47
  7. data/lib/ruby_routes/radix_tree/inserter.rb +2 -55
  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 +22 -1
  15. data/lib/ruby_routes/route/matcher.rb +11 -0
  16. data/lib/ruby_routes/route/param_support.rb +9 -8
  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.rb +35 -56
  22. data/lib/ruby_routes/route_set/cache_helpers.rb +29 -13
  23. data/lib/ruby_routes/route_set/collection_helpers.rb +8 -10
  24. data/lib/ruby_routes/route_set.rb +34 -59
  25. data/lib/ruby_routes/router/build_helpers.rb +1 -7
  26. data/lib/ruby_routes/router/builder.rb +12 -12
  27. data/lib/ruby_routes/router/http_helpers.rb +7 -48
  28. data/lib/ruby_routes/router/resource_helpers.rb +23 -37
  29. data/lib/ruby_routes/router/scope_helpers.rb +26 -14
  30. data/lib/ruby_routes/router.rb +28 -29
  31. data/lib/ruby_routes/segment.rb +3 -3
  32. data/lib/ruby_routes/segments/base_segment.rb +8 -0
  33. data/lib/ruby_routes/segments/static_segment.rb +3 -1
  34. data/lib/ruby_routes/strategies/base.rb +18 -0
  35. data/lib/ruby_routes/strategies/hash_based_strategy.rb +33 -0
  36. data/lib/ruby_routes/strategies/hybrid_strategy.rb +70 -0
  37. data/lib/ruby_routes/strategies/radix_tree_strategy.rb +24 -0
  38. data/lib/ruby_routes/strategies.rb +5 -0
  39. data/lib/ruby_routes/utility/key_builder_utility.rb +4 -26
  40. data/lib/ruby_routes/utility/method_utility.rb +11 -11
  41. data/lib/ruby_routes/utility/path_utility.rb +18 -7
  42. data/lib/ruby_routes/version.rb +1 -1
  43. data/lib/ruby_routes.rb +3 -1
  44. metadata +11 -2
  45. 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,22 @@ module RubyRoutes
64
70
  # @param entry [Hash] The cache entry.
65
71
  # @return [void]
66
72
  def insert_cache_entry(cache_key, entry)
67
- @cache_mutex ||= Mutex.new
68
73
  @cache_mutex.synchronize do
69
74
  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
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)
73
89
  end
74
90
  @recognition_cache[cache_key] = entry
75
91
  end
@@ -12,7 +12,7 @@ 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.
@@ -21,7 +21,7 @@ module RubyRoutes
21
21
  return route if @routes.include?(route) # Prevent duplicate insertion
22
22
 
23
23
  @named_routes[route.name] = route if route.named?
24
- @radix_tree.add(route.path, route.methods, route)
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,19 +69,17 @@ 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
- @cache_mutex ||= Mutex.new
76
+ def clear_routes_and_caches!
78
77
  @cache_mutex.synchronize do
79
78
  @routes.clear
80
79
  @named_routes.clear
81
80
  @recognition_cache.clear
82
- @cache_hits = 0
83
- @cache_misses = 0
84
- @radix_tree = RadixTree.new
81
+ @small_lru.clear_counters!
82
+ @strategy = @strategy_class.new
85
83
  RubyRoutes::Utility::KeyBuilderUtility.clear!
86
84
  end
87
85
  end
@@ -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,25 +92,19 @@ module RubyRoutes
90
92
  route.generate_path(params)
91
93
  end
92
94
 
95
+ def clear_counters!
96
+ clear_routes_and_caches!
97
+ end
98
+
93
99
  private
94
100
 
95
- # Set up the radix tree for structural path matching.
101
+ # Set up the matching strategy.
96
102
  #
103
+ # @param strategy [Class] The matching strategy class.
97
104
  # @return [void]
98
- def setup_radix_tree
99
- @radix_tree = RadixTree.new
100
- end
101
-
102
- # Normalize the HTTP method for matching.
103
- #
104
- # @param http_method [String, Symbol] The HTTP method.
105
- # @return [String] The normalized HTTP method.
106
- def normalize_method_for_match(http_method)
107
- if http_method.is_a?(String) && normalize_http_method(http_method).equal?(http_method)
108
- http_method
109
- else
110
- normalize_http_method(http_method)
111
- end
105
+ def setup_strategy(strategy)
106
+ @strategy_class = strategy
107
+ @strategy = @strategy_class.new
112
108
  end
113
109
 
114
110
  # Perform the route matching process.
@@ -117,31 +113,28 @@ module RubyRoutes
117
113
  # @param raw_path [String] The raw request path.
118
114
  # @return [Hash, nil] A hash containing the matched route and parameters, or `nil` if no match is found.
119
115
  def perform_match(normalized_method, raw_path)
120
- path_without_query, _query = raw_path.split('?', 2)
121
- matched_route, extracted_params = @radix_tree.find(path_without_query, normalized_method)
116
+ matched_route, path_params = @strategy.find(raw_path, normalized_method)
122
117
  return nil unless matched_route
123
118
 
124
- # Ensure we have a mutable hash for merging defaults / query params.
125
- if extracted_params.nil?
126
- extracted_params = {}
127
- elsif extracted_params.frozen?
128
- extracted_params = extracted_params.dup
129
- end
130
-
131
- merge_query_params(matched_route, raw_path, extracted_params)
132
- merge_defaults(matched_route, extracted_params)
133
- 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)
134
121
  end
135
122
 
136
- # Merge default parameters into the extracted parameters.
123
+ # Build the final parameters hash by merging path, query, and default params.
137
124
  #
138
125
  # @param matched_route [Route] The matched route.
139
- # @param extracted_params [Hash] The extracted parameters.
140
- # @return [void]
141
- def merge_defaults(matched_route, extracted_params)
142
- return unless matched_route.respond_to?(:defaults) && matched_route.defaults
143
-
144
- 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
145
138
  end
146
139
 
147
140
  # Build the match result hash.
@@ -157,23 +150,5 @@ module RubyRoutes
157
150
  action: matched_route.action
158
151
  }
159
152
  end
160
-
161
- # Obtain a pooled hash for temporary parameters.
162
- #
163
- # @return [Hash] A thread-local hash for temporary parameter storage.
164
- def thread_local_params
165
- thread_params = Thread.current[:ruby_routes_params_pool] ||= []
166
- thread_params.empty? ? {} : thread_params.pop.clear
167
- end
168
-
169
- # Return a parameters hash to the thread-local pool.
170
- #
171
- # @param params [Hash] The parameters hash to return.
172
- # @return [void]
173
- def return_params_to_pool(params)
174
- params.clear
175
- thread_pool = Thread.current[:ruby_routes_params_pool] ||= []
176
- thread_pool << params if thread_pool.size < 10 # Limit pool size
177
- end
178
153
  end
179
154
  end
@@ -17,12 +17,6 @@ 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
26
20
  router.finalize!
27
21
  router
28
22
  end
@@ -95,7 +89,7 @@ module RubyRoutes
95
89
  def validate_calls(recorded_calls)
96
90
  allowed_router_methods = RubyRoutes::Constant::RECORDED_METHODS
97
91
  recorded_calls.each do |(router_method, _arguments, _definition_block)|
98
- unless router_method.is_a?(Symbol) && allowed_router_methods.include?(router_method)
92
+ unless allowed_router_methods.include?(router_method)
99
93
  raise ArgumentError, "Invalid router method: #{router_method.inspect}"
100
94
  end
101
95
  end
@@ -32,6 +32,18 @@ module RubyRoutes
32
32
  class Builder
33
33
  include RubyRoutes::Router::BuildHelpers
34
34
 
35
+ # Initialize the Builder.
36
+ #
37
+ # This method initializes the `@recorded_calls` array and optionally
38
+ # evaluates the provided block in the context of the Builder instance.
39
+ #
40
+ # @yield [definition_block] Runs the routing DSL in a recording context (optional).
41
+ # @return [void]
42
+ def initialize(&definition_block)
43
+ @recorded_calls = []
44
+ instance_eval(&definition_block) if definition_block
45
+ end
46
+
35
47
  # Array of recorded calls: [method_symbol, args_array, block].
36
48
  #
37
49
  # Each tuple contains:
@@ -48,18 +60,6 @@ module RubyRoutes
48
60
  .freeze
49
61
  end
50
62
 
51
- # Initialize the Builder.
52
- #
53
- # This method initializes the `@recorded_calls` array and optionally
54
- # evaluates the provided block in the context of the Builder instance.
55
- #
56
- # @yield [definition_block] Runs the routing DSL in a recording context (optional).
57
- # @return [void]
58
- def initialize(&definition_block)
59
- @recorded_calls = []
60
- instance_eval(&definition_block) if definition_block
61
- end
62
-
63
63
  # ---- DSL Recording -------------------------------------------------
64
64
  # Dynamically define methods for all DSL methods specified in
65
65
  # `RubyRoutes::Constant::RECORDED_METHODS`. Each method records its
@@ -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.
@@ -25,33 +25,43 @@ module RubyRoutes
25
25
  # @return [void]
26
26
  def define_resource_routes(resource_name, options = {}, &nested_block)
27
27
  meta = resource_meta(resource_name, options)
28
- opts = prepare_options(options)
28
+ resource_opts = prepare_resource_options(options)
29
29
 
30
30
  push_scope(path: "/#{meta[:resource_path]}") do
31
- build_routes(opts, meta)
32
- handle_nested_option(options, opts)
31
+ define_resource_actions(resource_opts, meta[:controller])
32
+ handle_nested_option(options, resource_opts)
33
33
  apply_nested_block(nested_block)
34
34
  end
35
35
  end
36
36
 
37
37
  private
38
38
 
39
- # Prepare options by removing the `:to` key if present.
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.
40
41
  #
41
42
  # @param options [Hash] The options hash.
42
43
  # @return [Hash] The prepared options.
43
- def prepare_options(options)
44
- 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
45
46
  end
46
47
 
47
- # 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.
48
50
  #
49
- # @param opts [Hash] The options hash.
50
- # @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').
51
54
  # @return [void]
52
- def build_routes(opts, meta)
53
- build_collection_routes(opts, meta[:to_index], meta[:to_new], meta[:to_create])
54
- 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"))
55
65
  end
56
66
 
57
67
  # Apply a nested block of routes within the scope of a resource.
@@ -103,36 +113,12 @@ module RubyRoutes
103
113
 
104
114
  nested_name = options[:nested].to_s
105
115
  nested_path = RubyRoutes::Utility::InflectorUtility.pluralize(nested_name)
106
- build_nested_routes(nested_path, opts)
107
- end
108
-
109
- # Build nested resource routes.
110
- #
111
- # @param nested_path [String] The path for the nested resource.
112
- # @param opts [Hash] The options hash.
113
- # @return [void]
114
- def build_nested_routes(nested_path, opts)
115
116
  push_scope(path: '/:id') do
116
117
  push_scope(path: "/#{nested_path}") do
117
- add_nested_routes(nested_path, opts)
118
+ define_resource_actions(opts, nested_path, member_param: ':nested_id')
118
119
  end
119
120
  end
120
121
  end
121
-
122
- # Add routes for a nested resource.
123
- #
124
- # @param nested_path [String] The path for the nested resource.
125
- # @param opts [Hash] The options hash.
126
- # @return [void]
127
- def add_nested_routes(nested_path, opts)
128
- add_route('', build_route_options(opts, :get, "#{nested_path}#index"))
129
- add_route('/new', build_route_options(opts, :get, "#{nested_path}#new"))
130
- add_route('', build_route_options(opts, :post, "#{nested_path}#create"))
131
- add_route('/:nested_id', build_route_options(opts, :get, "#{nested_path}#show"))
132
- add_route('/:nested_id/edit', build_route_options(opts, :get, "#{nested_path}#edit"))
133
- add_route('/:nested_id', opts.merge(via: %i[put patch], to: "#{nested_path}#update"))
134
- add_route('/:nested_id', build_route_options(opts, :delete, "#{nested_path}#destroy"))
135
- end
136
122
  end
137
123
  end
138
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