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.
- checksums.yaml +4 -4
- data/README.md +232 -162
- data/lib/ruby_routes/constant.rb +137 -18
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +31 -4
- data/lib/ruby_routes/lru_strategies/miss_strategy.rb +21 -0
- data/lib/ruby_routes/node.rb +82 -41
- data/lib/ruby_routes/radix_tree/finder.rb +164 -0
- data/lib/ruby_routes/radix_tree/inserter.rb +98 -0
- data/lib/ruby_routes/radix_tree.rb +83 -142
- data/lib/ruby_routes/route/check_helpers.rb +109 -0
- data/lib/ruby_routes/route/constraint_validator.rb +159 -0
- data/lib/ruby_routes/route/param_support.rb +202 -0
- data/lib/ruby_routes/route/path_builder.rb +86 -0
- data/lib/ruby_routes/route/path_generation.rb +102 -0
- data/lib/ruby_routes/route/query_helpers.rb +56 -0
- data/lib/ruby_routes/route/segment_compiler.rb +163 -0
- data/lib/ruby_routes/route/small_lru.rb +96 -17
- data/lib/ruby_routes/route/validation_helpers.rb +151 -0
- data/lib/ruby_routes/route/warning_helpers.rb +54 -0
- data/lib/ruby_routes/route.rb +121 -451
- data/lib/ruby_routes/route_set/cache_helpers.rb +174 -0
- data/lib/ruby_routes/route_set/collection_helpers.rb +127 -0
- data/lib/ruby_routes/route_set.rb +126 -148
- data/lib/ruby_routes/router/build_helpers.rb +100 -0
- data/lib/ruby_routes/router/builder.rb +96 -0
- data/lib/ruby_routes/router/http_helpers.rb +135 -0
- data/lib/ruby_routes/router/resource_helpers.rb +137 -0
- data/lib/ruby_routes/router/scope_helpers.rb +109 -0
- data/lib/ruby_routes/router.rb +196 -179
- data/lib/ruby_routes/segment.rb +28 -8
- data/lib/ruby_routes/segments/base_segment.rb +40 -4
- data/lib/ruby_routes/segments/dynamic_segment.rb +48 -12
- data/lib/ruby_routes/segments/static_segment.rb +43 -7
- data/lib/ruby_routes/segments/wildcard_segment.rb +56 -12
- data/lib/ruby_routes/string_extensions.rb +52 -15
- data/lib/ruby_routes/url_helpers.rb +106 -24
- data/lib/ruby_routes/utility/inflector_utility.rb +35 -0
- data/lib/ruby_routes/utility/key_builder_utility.rb +179 -0
- data/lib/ruby_routes/utility/method_utility.rb +137 -0
- data/lib/ruby_routes/utility/path_utility.rb +89 -0
- data/lib/ruby_routes/utility/route_utility.rb +49 -0
- data/lib/ruby_routes/version.rb +3 -1
- data/lib/ruby_routes.rb +68 -11
- 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
|