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.
- checksums.yaml +4 -4
- data/README.md +0 -23
- data/lib/ruby_routes/constant.rb +20 -3
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -1
- data/lib/ruby_routes/node.rb +15 -87
- data/lib/ruby_routes/radix_tree/finder.rb +79 -52
- data/lib/ruby_routes/radix_tree/inserter.rb +2 -54
- data/lib/ruby_routes/radix_tree/traversal_strategy/base.rb +18 -0
- data/lib/ruby_routes/radix_tree/traversal_strategy/generic_loop.rb +25 -0
- data/lib/ruby_routes/radix_tree/traversal_strategy/unrolled.rb +45 -0
- data/lib/ruby_routes/radix_tree/traversal_strategy.rb +26 -0
- data/lib/ruby_routes/radix_tree.rb +12 -62
- data/lib/ruby_routes/route/check_helpers.rb +3 -3
- data/lib/ruby_routes/route/constraint_validator.rb +24 -1
- data/lib/ruby_routes/route/matcher.rb +11 -0
- data/lib/ruby_routes/route/param_support.rb +9 -7
- data/lib/ruby_routes/route/path_builder.rb +11 -6
- data/lib/ruby_routes/route/path_generation.rb +5 -1
- data/lib/ruby_routes/route/small_lru.rb +43 -2
- data/lib/ruby_routes/route/validation_helpers.rb +6 -36
- data/lib/ruby_routes/route/warning_helpers.rb +7 -5
- data/lib/ruby_routes/route.rb +35 -55
- data/lib/ruby_routes/route_set/cache_helpers.rb +32 -13
- data/lib/ruby_routes/route_set/collection_helpers.rb +15 -14
- data/lib/ruby_routes/route_set.rb +32 -69
- data/lib/ruby_routes/router/build_helpers.rb +1 -1
- data/lib/ruby_routes/router/builder.rb +16 -33
- data/lib/ruby_routes/router/http_helpers.rb +13 -49
- data/lib/ruby_routes/router/resource_helpers.rb +26 -39
- data/lib/ruby_routes/router/scope_helpers.rb +26 -14
- data/lib/ruby_routes/router.rb +41 -24
- data/lib/ruby_routes/segment.rb +3 -3
- data/lib/ruby_routes/segments/base_segment.rb +8 -0
- data/lib/ruby_routes/segments/static_segment.rb +3 -1
- data/lib/ruby_routes/strategies/base.rb +18 -0
- data/lib/ruby_routes/strategies/hash_based_strategy.rb +33 -0
- data/lib/ruby_routes/strategies/hybrid_strategy.rb +70 -0
- data/lib/ruby_routes/strategies/radix_tree_strategy.rb +24 -0
- data/lib/ruby_routes/strategies.rb +5 -0
- data/lib/ruby_routes/utility/key_builder_utility.rb +4 -26
- data/lib/ruby_routes/utility/method_utility.rb +18 -17
- data/lib/ruby_routes/utility/path_utility.rb +18 -7
- data/lib/ruby_routes/version.rb +1 -1
- data/lib/ruby_routes.rb +3 -1
- metadata +11 -2
- 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 = @
|
23
|
+
total_requests = @small_lru.hits + @small_lru.misses
|
20
24
|
{
|
21
|
-
hits: @
|
22
|
-
misses: @
|
23
|
-
hit_rate: total_requests.zero? ? 0.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 =
|
41
|
-
@
|
42
|
-
@
|
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
|
-
@
|
57
|
+
@small_lru.increment_hits
|
52
58
|
return cached_result
|
53
59
|
end
|
54
|
-
@
|
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
|
-
|
68
|
-
@recognition_cache.
|
69
|
-
|
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
|
-
#
|
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
|
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 = @
|
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
|
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
|
77
|
-
@
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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 '
|
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
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
101
|
+
# Set up the matching strategy.
|
108
102
|
#
|
103
|
+
# @param strategy [Class] The matching strategy class.
|
109
104
|
# @return [void]
|
110
|
-
def
|
111
|
-
@
|
112
|
-
|
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
|
-
|
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
|
-
|
137
|
-
|
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
|
-
#
|
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
|
152
|
-
# @
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
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
|
-
#
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
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
|
-
|
28
|
+
resource_opts = prepare_resource_options(options)
|
30
29
|
|
31
30
|
push_scope(path: "/#{meta[:resource_path]}") do
|
32
|
-
|
33
|
-
handle_nested_option(options,
|
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
|
-
|
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
|
43
|
-
options.key?(:to) ? options.
|
44
|
+
def prepare_resource_options(options)
|
45
|
+
options.key?(:to) ? options.except(:to) : options
|
44
46
|
end
|
45
47
|
|
46
|
-
#
|
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
|
49
|
-
# @param
|
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
|
52
|
-
|
53
|
-
|
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
|
-
|
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
|
-
|
65
|
-
return if
|
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
|
-
|
78
|
-
scoped_path.
|
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
|