ruby_routes 2.1.0 → 2.3.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 +232 -162
  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 +82 -41
  7. data/lib/ruby_routes/radix_tree/finder.rb +164 -0
  8. data/lib/ruby_routes/radix_tree/inserter.rb +98 -0
  9. data/lib/ruby_routes/radix_tree.rb +83 -142
  10. data/lib/ruby_routes/route/check_helpers.rb +109 -0
  11. data/lib/ruby_routes/route/constraint_validator.rb +159 -0
  12. data/lib/ruby_routes/route/param_support.rb +202 -0
  13. data/lib/ruby_routes/route/path_builder.rb +86 -0
  14. data/lib/ruby_routes/route/path_generation.rb +102 -0
  15. data/lib/ruby_routes/route/query_helpers.rb +56 -0
  16. data/lib/ruby_routes/route/segment_compiler.rb +163 -0
  17. data/lib/ruby_routes/route/small_lru.rb +96 -17
  18. data/lib/ruby_routes/route/validation_helpers.rb +151 -0
  19. data/lib/ruby_routes/route/warning_helpers.rb +54 -0
  20. data/lib/ruby_routes/route.rb +121 -451
  21. data/lib/ruby_routes/route_set/cache_helpers.rb +174 -0
  22. data/lib/ruby_routes/route_set/collection_helpers.rb +127 -0
  23. data/lib/ruby_routes/route_set.rb +126 -148
  24. data/lib/ruby_routes/router/build_helpers.rb +100 -0
  25. data/lib/ruby_routes/router/builder.rb +96 -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 +109 -0
  29. data/lib/ruby_routes/router.rb +196 -179
  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 +56 -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 +179 -0
  39. data/lib/ruby_routes/utility/method_utility.rb +137 -0
  40. data/lib/ruby_routes/utility/path_utility.rb +89 -0
  41. data/lib/ruby_routes/utility/route_utility.rb +49 -0
  42. data/lib/ruby_routes/version.rb +3 -1
  43. data/lib/ruby_routes.rb +68 -11
  44. metadata +30 -7
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'path_generation'
4
+ require_relative 'warning_helpers'
5
+
6
+ module RubyRoutes
7
+ class Route
8
+ # ParamSupport: helpers for parameter merging and cache key generation.
9
+ #
10
+ # This module provides methods for efficiently merging parameters, generating
11
+ # cache keys, and extracting parameters from request paths. It uses thread-local
12
+ # hashes for performance and includes a 2-slot LRU cache for param key generation.
13
+ #
14
+ # Thread-safety: Thread-local storage is used to avoid allocation and cross-thread mutation.
15
+ #
16
+ # @module RubyRoutes::Route::ParamSupport
17
+ module ParamSupport
18
+ include RubyRoutes::Route::WarningHelpers
19
+
20
+ private
21
+
22
+ # Merge incoming params with route defaults.
23
+ #
24
+ # This method merges user-provided parameters with route defaults and returns
25
+ # a thread-local hash for performance.
26
+ #
27
+ # @param params [Hash] The user-provided parameters.
28
+ # @return [Hash] The merged parameters.
29
+ def build_merged_params(params)
30
+ return @defaults if params.nil? || params.empty?
31
+
32
+ merged_hash = acquire_merge_hash
33
+ merge_defaults_into(merged_hash)
34
+ merge_user_params_into(merged_hash, params)
35
+ merged_hash
36
+ end
37
+
38
+ # Acquire a thread-local hash for merging.
39
+ #
40
+ # @return [Hash] A cleared thread-local hash for merging.
41
+ def acquire_merge_hash
42
+ merged_hash = Thread.current[:ruby_routes_merge_hash] ||= {}
43
+ merged_hash.clear
44
+ merged_hash
45
+ end
46
+
47
+ # Merge defaults into the hash.
48
+ #
49
+ # @param merged_hash [Hash] The hash to merge defaults into.
50
+ # @return [void]
51
+ def merge_defaults_into(merged_hash)
52
+ @defaults.each { |key, value| merged_hash[key] = value } unless @defaults.empty?
53
+ end
54
+
55
+ # Merge user params into the hash.
56
+ #
57
+ # This method converts keys to strings and skips nil values.
58
+ #
59
+ # @param merged_hash [Hash] The hash to merge user parameters into.
60
+ # @param params [Hash] The user-provided parameters.
61
+ # @return [void]
62
+ def merge_user_params_into(merged_hash, params)
63
+ params.each do |key, value|
64
+ next if value.nil?
65
+
66
+ merged_hash[key.is_a?(String) ? key : key.to_s] = value
67
+ end
68
+ end
69
+
70
+ # Build a frozen cache key for the merged params and update the 2-slot cache.
71
+ #
72
+ # @param merged_params [Hash] The merged parameters.
73
+ # @return [String] The frozen cache key.
74
+ def build_param_cache_key(merged_params)
75
+ param_cache_key = cache_key_for_params(@required_params, merged_params)
76
+ cache_key_hash = param_cache_key.hash
77
+
78
+ if (cache_slot = @param_key_slots[0])[0] == cache_key_hash && cache_slot[1] == param_cache_key
79
+ return cache_slot[1]
80
+ elsif (cache_slot = @param_key_slots[1])[0] == cache_key_hash && cache_slot[1] == param_cache_key
81
+ return cache_slot[1]
82
+ end
83
+
84
+ store_param_key_slot(cache_key_hash, param_cache_key)
85
+ end
86
+
87
+ # Store the param cache key in the 2-slot LRU.
88
+ #
89
+ # @param cache_key_hash [Integer] The hash of the cache key.
90
+ # @param param_cache_key [String] The cache key to store.
91
+ # @return [String] The stored cache key.
92
+ def store_param_key_slot(cache_key_hash, param_cache_key)
93
+ if @param_key_slots[0][0].nil?
94
+ @param_key_slots[0] = [cache_key_hash, param_cache_key]
95
+ elsif @param_key_slots[1][0].nil?
96
+ @param_key_slots[1] = [cache_key_hash, param_cache_key]
97
+ else
98
+ @param_key_slots[0] = @param_key_slots[1]
99
+ @param_key_slots[1] = [cache_key_hash, param_cache_key]
100
+ end
101
+ param_cache_key
102
+ end
103
+
104
+ # Extract parameters from a request path (and optionally pre-parsed query).
105
+ #
106
+ # @param request_path [String] The request path.
107
+ # @param parsed_qp [Hash, nil] Pre-parsed query parameters to merge (optional).
108
+ # @return [Hash] The extracted parameters, with defaults merged in.
109
+ def extract_params(request_path, parsed_qp = nil)
110
+ extracted_path_params = extract_path_params_fast(request_path)
111
+ return RubyRoutes::Constant::EMPTY_HASH unless extracted_path_params
112
+
113
+ build_params_hash(extracted_path_params, request_path, parsed_qp)
114
+ end
115
+
116
+ # Build full params hash (path + query + defaults + constraints).
117
+ #
118
+ # @param path_params [Hash] The extracted path parameters.
119
+ # @param request_path [String] The request path.
120
+ # @param parsed_qp [Hash, nil] Pre-parsed query parameters.
121
+ # @return [Hash] The full parameters hash.
122
+ def build_params_hash(path_params, request_path, parsed_qp)
123
+ params_hash = get_thread_local_hash
124
+ params_hash.update(path_params)
125
+
126
+ merge_query_params_into_hash(params_hash, request_path, parsed_qp)
127
+
128
+ merge_defaults_fast(params_hash) unless @defaults.empty?
129
+ validate_constraints_fast!(params_hash) unless @constraints.empty?
130
+ params_hash
131
+ end
132
+
133
+ # Merge query parameters (if any) from full path into param hash.
134
+ #
135
+ # @param route_obj [Route]
136
+ # @param full_path [String]
137
+ # @param param_hash [Hash]
138
+ # @return [void]
139
+ def merge_query_params(route_obj, full_path, param_hash)
140
+ return unless full_path.to_s.include?('?')
141
+
142
+ if route_obj.respond_to?(:parse_query_params)
143
+ qp = route_obj.parse_query_params(full_path)
144
+ param_hash.merge!(qp) if qp
145
+ elsif route_obj.respond_to?(:query_params)
146
+ qp = route_obj.query_params(full_path)
147
+ param_hash.merge!(qp) if qp
148
+ end
149
+ end
150
+
151
+ # Acquire thread-local hash.
152
+ #
153
+ # @return [Hash]
154
+ def acquire_thread_local_hash
155
+ pool = Thread.current[:ruby_routes_hash_pool] ||= []
156
+ return {} if pool.empty?
157
+
158
+ hash = pool.pop
159
+ hash.clear
160
+ hash
161
+ end
162
+
163
+ alias get_thread_local_hash acquire_thread_local_hash # backward compatibility if referenced elsewhere
164
+
165
+ # Merge query params into the hash.
166
+ #
167
+ # @param params_hash [Hash] The hash to merge query parameters into.
168
+ # @param request_path [String] The request path.
169
+ # @param parsed_qp [Hash, nil] Pre-parsed query parameters.
170
+ # @return [void]
171
+ def merge_query_params_into_hash(params_hash, request_path, parsed_qp)
172
+ if parsed_qp
173
+ params_hash.merge!(parsed_qp)
174
+ elsif request_path.include?('?')
175
+ query_params = query_params_fast(request_path)
176
+ params_hash.merge!(query_params) unless query_params.empty?
177
+ end
178
+ end
179
+
180
+ # Merge defaults where absent.
181
+ #
182
+ # @param result [Hash] The hash to merge defaults into.
183
+ # @return [void]
184
+ def merge_defaults_fast(result)
185
+ @defaults.each { |key, value| result[key] = value unless result.key?(key) }
186
+ end
187
+
188
+ # Retrieve query params from route object via supported method.
189
+ #
190
+ # @param route_obj [Route] The route object.
191
+ # @param full_path [String] The full path containing the query string.
192
+ # @return [Hash, nil] The query parameters, or `nil` if none are found.
193
+ def retrieve_query_params(route_obj, full_path)
194
+ if route_obj.respond_to?(:parse_query_params)
195
+ route_obj.parse_query_params(full_path)
196
+ elsif route_obj.respond_to?(:query_params)
197
+ route_obj.query_params(full_path)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRoutes
4
+ class Route
5
+ # PathBuilder: generation + segment encoding
6
+ #
7
+ # @module RubyRoutes::Route::PathBuilder
8
+ module PathBuilder
9
+ private
10
+
11
+ # Generate the path string from merged parameters.
12
+ #
13
+ # @param merged [Hash] the merged parameters
14
+ # @return [String] the generated path
15
+ def generate_path_string(merged)
16
+ return RubyRoutes::Constant::ROOT_PATH if @compiled_segments.empty?
17
+
18
+ buffer = String.new(capacity: estimate_length)
19
+ buffer << '/'
20
+ last_index = @compiled_segments.length - 1
21
+ @compiled_segments.each_with_index do |segment, index|
22
+ append_segment(buffer, segment, merged, index, last_index)
23
+ end
24
+ buffer
25
+ end
26
+
27
+ # Append a segment to the buffer.
28
+ #
29
+ # @param buffer [String] the buffer to append to
30
+ # @param segment [Hash] the segment to append
31
+ # @param merged [Hash] the merged parameters
32
+ # @param index [Integer] the current index
33
+ # @param last_index [Integer] the last index
34
+ def append_segment(buffer, segment, merged, index, last_index)
35
+ case segment[:type]
36
+ when :static
37
+ buffer << segment[:value]
38
+ when :param
39
+ buffer << encode_segment_fast(merged.fetch(segment[:name]).to_s)
40
+ when :splat
41
+ buffer << format_splat_value(merged.fetch(segment[:name], ''))
42
+ end
43
+ buffer << '/' unless index == last_index
44
+ end
45
+
46
+ # Estimate the length of the path.
47
+ #
48
+ # @return [Integer] the estimated length
49
+ def estimate_length
50
+ # Rough heuristic (static sizes + average dynamic)
51
+ base = 1
52
+ @compiled_segments.each do |segment|
53
+ base += case segment[:type]
54
+ when :static then segment[:value].length + 1
55
+ else 20
56
+ end
57
+ end
58
+ base
59
+ end
60
+
61
+ # Format a splat value.
62
+ #
63
+ # @param value [Object] the value to format
64
+ # @return [String] the formatted value
65
+ def format_splat_value(value)
66
+ case value
67
+ when Array then value.map { |part| encode_segment_fast(part.to_s) }.join('/')
68
+ when String then value.split('/').map { |part| encode_segment_fast(part) }.join('/')
69
+ else encode_segment_fast(value.to_s)
70
+ end
71
+ end
72
+
73
+ # Encode a segment fast.
74
+ #
75
+ # @param string [String] the string to encode
76
+ # @return [String] the encoded string
77
+ def encode_segment_fast(string)
78
+ return string if RubyRoutes::Constant::UNRESERVED_RE.match?(string)
79
+
80
+ @encoding_cache ||= {}
81
+ # Use gsub instead of tr for proper replacement of + with %20
82
+ @encoding_cache[string] ||= URI.encode_www_form_component(string).gsub('+', '%20')
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'warning_helpers'
4
+
5
+ module RubyRoutes
6
+ class Route
7
+ # PathGeneration:
8
+ # Small focused helpers related to building generated paths and emitting
9
+ # route-related warnings (kept separate to reduce parent module size).
10
+ module PathGeneration
11
+ include RubyRoutes::Route::WarningHelpers
12
+
13
+ private
14
+
15
+ # Generate a path string from supplied params.
16
+ #
17
+ # Rules:
18
+ # - Required params must be present and non‑nil (unless defaulted).
19
+ # - Caches the result keyed on ordered required param values.
20
+ #
21
+ # @param params [Hash] The parameters for path generation (String/Symbol keys).
22
+ # @return [String] The generated path string.
23
+ # @raise [RouteNotFound] If required params are missing or nil.
24
+ def generate_path(params = {})
25
+ return @static_path if static_short_circuit?(params)
26
+ return @static_path || RubyRoutes::Constant::ROOT_PATH if trivial_route?
27
+
28
+ validate_required_once(params)
29
+ merged_params = build_merged_params(params)
30
+
31
+ build_or_fetch_generated_path(merged_params)
32
+ end
33
+
34
+ # Build or fetch a generated path from the cache.
35
+ #
36
+ # This method generates a path string from the merged parameters or fetches
37
+ # it from the cache if it already exists.
38
+ #
39
+ # @param merged_params [Hash] The merged parameters for path generation.
40
+ # @return [String] The generated or cached path string.
41
+ def build_or_fetch_generated_path(merged_params)
42
+ generation_cache_key = build_generation_cache_key(merged_params)
43
+ if (cached_path = @gen_cache.get(generation_cache_key))
44
+ return cached_path
45
+ end
46
+
47
+ generated_path = generate_path_string(merged_params)
48
+ @gen_cache.set(generation_cache_key, generated_path)
49
+ generated_path
50
+ end
51
+
52
+ # Build a generation cache key for merged params.
53
+ #
54
+ # This method creates a cache key based on the required parameters and
55
+ # their values in the merged parameters.
56
+ #
57
+ # @param merged_params [Hash] The merged parameters for path generation.
58
+ # @return [String] The cache key for the generation cache.
59
+ def build_generation_cache_key(merged_params)
60
+ @required_params.empty? ? RubyRoutes::Constant::EMPTY_STRING : build_param_cache_key(merged_params)
61
+ end
62
+
63
+ # Emit deprecation warning for `Proc` constraints once per parameter.
64
+ #
65
+ # This method ensures that a deprecation warning for a `Proc` constraint
66
+ # is only emitted once per parameter. It tracks parameters for which
67
+ # warnings have already been shown.
68
+ #
69
+ # @param param [String, Symbol] The parameter name for which the warning
70
+ # is being emitted.
71
+ # @return [void]
72
+ def warn_proc_constraint_deprecation(param)
73
+ return if @proc_warnings_shown&.include?(param)
74
+
75
+ @proc_warnings_shown ||= Set.new
76
+ @proc_warnings_shown << param
77
+ warn_proc_warning(param)
78
+ end
79
+
80
+ # Determine if the route can short-circuit to a static path.
81
+ #
82
+ # This method checks if the route is static and the provided parameters
83
+ # are empty or nil, allowing the static path to be returned directly.
84
+ #
85
+ # @param params [Hash] The parameters for path generation.
86
+ # @return [Boolean] `true` if the route can short-circuit, `false` otherwise.
87
+ def static_short_circuit?(params)
88
+ @static_path && (params.nil? || params.empty?)
89
+ end
90
+
91
+ # Determine if the route is trivial.
92
+ #
93
+ # A route is considered trivial if it has no dynamic segments, no required
94
+ # parameters, and no constraints, meaning it can resolve to a static path.
95
+ #
96
+ # @return [Boolean] `true` if the route is trivial, `false` otherwise.
97
+ def trivial_route?
98
+ @compiled_segments.empty? && @required_params.empty? && @constraints.empty?
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+
5
+ module RubyRoutes
6
+ class Route
7
+ # QueryHelpers: encapsulate query parsing/caching for Route instances.
8
+ #
9
+ # This module provides methods for parsing query parameters from a URL path
10
+ # and caching the results for improved performance. It includes a wrapper
11
+ # for cached parsing and a low-level implementation with an LRU cache.
12
+ #
13
+ # Provides:
14
+ # - `parse_query_params(path)` -> Hash (public): Cached parsing wrapper.
15
+ # - `query_params_fast(path)` -> Hash (public): Low-level parsing with LRU caching.
16
+ module QueryHelpers
17
+ # Parse query params (wrapper for internal caching).
18
+ #
19
+ # This method parses the query parameters from the given path and caches
20
+ # the result for future lookups. It is a wrapper around the low-level
21
+ # `query_params_fast` method.
22
+ #
23
+ # @param path [String] The URL path containing the query string.
24
+ # @return [Hash] A hash of parsed query parameters.
25
+ def parse_query_params(path)
26
+ query_params_fast(path)
27
+ end
28
+ alias query_params parse_query_params
29
+
30
+ # Query param parsing with simple LRU caching.
31
+ #
32
+ # This method parses the query parameters from the given path and caches
33
+ # the result using a Least Recently Used (LRU) cache. If the query string
34
+ # is already cached, the cached result is returned.
35
+ #
36
+ # @param path [String] The URL path containing the query string.
37
+ # @return [Hash] A hash of parsed query parameters, or an empty hash if
38
+ # the path does not contain a valid query string.
39
+ def query_params_fast(path)
40
+ query_index = path.index('?')
41
+ return RubyRoutes::Constant::EMPTY_HASH unless query_index
42
+
43
+ query_part = path[(query_index + 1)..]
44
+ return RubyRoutes::Constant::EMPTY_HASH if query_part.empty? || query_part.match?(/^\?+$/)
45
+
46
+ if (cached_result = @query_cache.get(query_part))
47
+ return cached_result
48
+ end
49
+
50
+ parsed_result = Rack::Utils.parse_query(query_part)
51
+ @query_cache.set(query_part, parsed_result)
52
+ parsed_result
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRoutes
4
+ class Route
5
+ # SegmentCompiler: path analysis + extraction
6
+ #
7
+ # This module provides methods for analyzing and extracting segments from
8
+ # a route path. It includes utilities for compiling path segments, required
9
+ # parameters, and static paths, as well as extracting parameters from a
10
+ # request path.
11
+ #
12
+ # @module RubyRoutes::Route::SegmentCompiler
13
+ module SegmentCompiler
14
+ private
15
+
16
+ # Compile the segments from the path.
17
+ #
18
+ # This method splits the path into segments, analyzes each segment, and
19
+ # compiles metadata for static and dynamic segments.
20
+ #
21
+ # @return [void]
22
+ def compile_segments
23
+ @compiled_segments =
24
+ if @path == RubyRoutes::Constant::ROOT_PATH
25
+ RubyRoutes::Constant::EMPTY_ARRAY
26
+ else
27
+ @path.split('/').reject(&:empty?)
28
+ .map { |segment| RubyRoutes::Constant.segment_descriptor(segment) }
29
+ .freeze
30
+ end
31
+ end
32
+
33
+ # Compile the required parameters.
34
+ #
35
+ # This method identifies dynamic parameters in the path and determines
36
+ # which parameters are required based on the defaults provided.
37
+ #
38
+ # @return [void]
39
+ def compile_required_params
40
+ dynamic_param_names = @compiled_segments.filter_map { |segment| segment[:name] if segment[:type] != :static }
41
+ @param_names = dynamic_param_names.freeze
42
+ @required_params = if @defaults.empty?
43
+ dynamic_param_names.freeze
44
+ else
45
+ dynamic_param_names.reject do |name|
46
+ @defaults.key?(name) || @defaults.key?(name.to_sym)
47
+ end.freeze
48
+ end
49
+ @required_params_set = @required_params.to_set.freeze
50
+ end
51
+
52
+ # Check if the path is static.
53
+ #
54
+ # This method determines if the path contains only static segments. If so,
55
+ # it generates the static path.
56
+ #
57
+ # @return [void]
58
+ def check_static_path
59
+ return unless @compiled_segments.all? { |segment| segment[:type] == :static }
60
+
61
+ @static_path = generate_static_path
62
+ end
63
+
64
+ # Generate the static path.
65
+ #
66
+ # This method constructs the static path from the compiled segments.
67
+ #
68
+ # @return [String] The generated static path.
69
+ def generate_static_path
70
+ return RubyRoutes::Constant::ROOT_PATH if @compiled_segments.empty?
71
+
72
+ "/#{@compiled_segments.map { |segment| segment[:value] }.join('/')}"
73
+ end
74
+
75
+ # Extract path parameters fast.
76
+ #
77
+ # This method extracts parameters from a request path based on the compiled
78
+ # segments. It performs validation and handles dynamic, static, and splat
79
+ # segments.
80
+ #
81
+ # @param request_path [String] The request path.
82
+ # @return [Hash, nil] The extracted parameters, or `nil` if extraction fails.
83
+ def extract_path_params_fast(request_path)
84
+ return RubyRoutes::Constant::EMPTY_HASH if root_path_and_empty_segments?(request_path)
85
+
86
+ return nil if @compiled_segments.empty?
87
+
88
+ path_parts = split_path(request_path)
89
+ return nil unless valid_parts_count?(path_parts)
90
+
91
+ extract_params_from_parts(path_parts)
92
+ end
93
+
94
+ # Check if it's a root path with empty segments.
95
+ #
96
+ # This method checks if the request path is the root path and the compiled
97
+ # segments are empty.
98
+ #
99
+ # @param request_path [String] The request path.
100
+ # @return [Boolean] `true` if the path is the root path with empty segments, `false` otherwise.
101
+ def root_path_and_empty_segments?(request_path)
102
+ @compiled_segments.empty? && request_path == RubyRoutes::Constant::ROOT_PATH
103
+ end
104
+
105
+ # Validate the parts count.
106
+ #
107
+ # This method checks if the number of parts in the request path matches
108
+ # the expected number of segments, accounting for splat segments.
109
+ #
110
+ # @param path_parts [Array<String>] The path parts.
111
+ # @return [Boolean] `true` if the parts count is valid, `false` otherwise.
112
+ def valid_parts_count?(path_parts)
113
+ has_splat = @compiled_segments.any? { |segment| segment[:type] == :splat }
114
+ (!has_splat && path_parts.size == @compiled_segments.size) ||
115
+ (has_splat && path_parts.size >= (@compiled_segments.size - 1))
116
+ end
117
+
118
+ # Extract parameters from parts.
119
+ #
120
+ # This method processes each segment and extracts parameters from the
121
+ # corresponding parts of the request path.
122
+ #
123
+ # @param path_parts [Array<String>] The path parts.
124
+ # @return [Hash, nil] The extracted parameters, or `nil` if extraction fails.
125
+ def extract_params_from_parts(path_parts)
126
+ params_hash = {}
127
+ @compiled_segments.each_with_index do |segment, index|
128
+ result = process_segment(segment, index, path_parts, params_hash)
129
+ return nil if result == false
130
+ break if result == :break
131
+ end
132
+ params_hash
133
+ end
134
+
135
+ # Process a segment.
136
+ #
137
+ # This method processes a single segment, extracting parameters or
138
+ # validating static segments.
139
+ #
140
+ # @param segment [Hash] The segment metadata.
141
+ # @param index [Integer] The index of the segment.
142
+ # @param path_parts [Array<String>] The path parts.
143
+ # @param params_hash [Hash] The parameters hash.
144
+ # @return [Boolean, Symbol] `true` if processed successfully,
145
+ # `false` if validation fails, `:break` for splat segments.
146
+ def process_segment(segment, index, path_parts, params_hash)
147
+ case segment[:type]
148
+ when :static
149
+ segment[:value] == path_parts[index]
150
+ when :param
151
+ params_hash[segment[:name]] = path_parts[index]
152
+ true
153
+ when :splat
154
+ params_hash[segment[:name]] = path_parts[index..].join('/')
155
+ :break
156
+ end
157
+ end
158
+
159
+ # Expose for testing / external callers that need fast path extraction.
160
+ public :extract_path_params_fast
161
+ end
162
+ end
163
+ end