ruby_routes 2.2.0 → 2.4.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 +240 -163
- 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 +86 -36
- data/lib/ruby_routes/radix_tree/finder.rb +213 -0
- data/lib/ruby_routes/radix_tree/inserter.rb +96 -0
- data/lib/ruby_routes/radix_tree.rb +65 -230
- data/lib/ruby_routes/route/check_helpers.rb +115 -0
- data/lib/ruby_routes/route/constraint_validator.rb +173 -0
- data/lib/ruby_routes/route/param_support.rb +200 -0
- data/lib/ruby_routes/route/path_builder.rb +84 -0
- data/lib/ruby_routes/route/path_generation.rb +87 -0
- data/lib/ruby_routes/route/query_helpers.rb +56 -0
- data/lib/ruby_routes/route/segment_compiler.rb +166 -0
- data/lib/ruby_routes/route/small_lru.rb +93 -18
- data/lib/ruby_routes/route/validation_helpers.rb +174 -0
- data/lib/ruby_routes/route/warning_helpers.rb +57 -0
- data/lib/ruby_routes/route.rb +127 -501
- data/lib/ruby_routes/route_set/cache_helpers.rb +76 -0
- data/lib/ruby_routes/route_set/collection_helpers.rb +125 -0
- data/lib/ruby_routes/route_set.rb +140 -132
- data/lib/ruby_routes/router/build_helpers.rb +99 -0
- data/lib/ruby_routes/router/builder.rb +97 -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 +127 -0
- data/lib/ruby_routes/router.rb +196 -182
- 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 +58 -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 +171 -77
- data/lib/ruby_routes/utility/method_utility.rb +137 -0
- data/lib/ruby_routes/utility/path_utility.rb +75 -28
- data/lib/ruby_routes/utility/route_utility.rb +30 -2
- data/lib/ruby_routes/version.rb +3 -1
- data/lib/ruby_routes.rb +68 -11
- metadata +27 -7
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timeout'
|
4
|
+
require_relative '../constant'
|
5
|
+
|
6
|
+
module RubyRoutes
|
7
|
+
class Route
|
8
|
+
# ConstraintValidator: extracted constraint logic.
|
9
|
+
#
|
10
|
+
# This module provides methods for validating route constraints, including
|
11
|
+
# support for regular expressions, procs, hash-based constraints, and built-in
|
12
|
+
# validation rules. It also handles timeouts and raises appropriate exceptions
|
13
|
+
# for constraint violations.
|
14
|
+
module ConstraintValidator
|
15
|
+
# Validate all constraints for the given parameters.
|
16
|
+
#
|
17
|
+
# This method iterates through all constraints and validates each parameter
|
18
|
+
# against its corresponding rule.
|
19
|
+
#
|
20
|
+
# @param params [Hash] The parameters to validate.
|
21
|
+
# @return [void]
|
22
|
+
def validate_constraints_fast!(params)
|
23
|
+
@constraints.each do |key, rule|
|
24
|
+
param_key = key.to_s
|
25
|
+
next unless params.key?(param_key)
|
26
|
+
|
27
|
+
validate_constraint_for(rule, key, params[param_key])
|
28
|
+
end
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
# Dispatch a single constraint check.
|
33
|
+
#
|
34
|
+
# This method validates a single parameter against its constraint rule.
|
35
|
+
#
|
36
|
+
# @param rule [Object] The constraint rule (Regexp, Proc, Symbol, Hash).
|
37
|
+
# @param key [String, Symbol] The parameter key.
|
38
|
+
# @param value [Object] The value to validate.
|
39
|
+
# @return [void]
|
40
|
+
def validate_constraint_for(rule, key, value)
|
41
|
+
case rule
|
42
|
+
when Regexp then validate_regexp_constraint(rule, value)
|
43
|
+
when Proc then validate_proc_constraint(key, rule, value)
|
44
|
+
when Hash then validate_hash_constraint!(rule, value.to_s)
|
45
|
+
else
|
46
|
+
validate_builtin_constraint(rule, value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Handle built-in symbol/string rules via a simple lookup.
|
51
|
+
#
|
52
|
+
# @param rule [Symbol, String] The built-in constraint rule.
|
53
|
+
# @param value [Object] The value to validate.
|
54
|
+
# @return [void]
|
55
|
+
def validate_builtin_constraint(rule, value)
|
56
|
+
case rule.to_s
|
57
|
+
when 'int'
|
58
|
+
validate_int_constraint(value)
|
59
|
+
when 'uuid'
|
60
|
+
validate_uuid_constraint(value)
|
61
|
+
when 'email'
|
62
|
+
validate_email_constraint(value)
|
63
|
+
when 'slug'
|
64
|
+
validate_slug_constraint(value)
|
65
|
+
when 'alpha'
|
66
|
+
validate_alpha_constraint(value)
|
67
|
+
when 'alphanumeric'
|
68
|
+
validate_alphanumeric_constraint(value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Validate a regexp constraint.
|
73
|
+
#
|
74
|
+
# This method validates a value against a regular expression constraint.
|
75
|
+
# It raises a timeout error if the validation takes too long.
|
76
|
+
#
|
77
|
+
# @param regexp [Regexp] The regular expression to match.
|
78
|
+
# @param value [Object] The value to validate.
|
79
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value does not match the regexp.
|
80
|
+
def validate_regexp_constraint(regexp, value)
|
81
|
+
Timeout.timeout(0.1) { invalid! unless regexp.match?(value.to_s) }
|
82
|
+
rescue Timeout::Error
|
83
|
+
raise RubyRoutes::ConstraintViolation, 'Regex constraint timed out'
|
84
|
+
end
|
85
|
+
|
86
|
+
# Validate a proc constraint.
|
87
|
+
#
|
88
|
+
# This method validates a value using a proc constraint. It emits a
|
89
|
+
# deprecation warning for proc constraints and handles timeouts and errors.
|
90
|
+
#
|
91
|
+
# @param key [String, Symbol] The parameter key.
|
92
|
+
# @param proc [Proc] The proc to call.
|
93
|
+
# @param value [Object] The value to validate.
|
94
|
+
# @raise [RubyRoutes::ConstraintViolation] If the proc constraint fails or times out.
|
95
|
+
def validate_proc_constraint(key, proc, value)
|
96
|
+
warn_proc_constraint_deprecation(key)
|
97
|
+
Timeout.timeout(0.05) { invalid! unless proc.call(value.to_s) }
|
98
|
+
rescue Timeout::Error
|
99
|
+
raise RubyRoutes::ConstraintViolation, 'Proc constraint timed out'
|
100
|
+
rescue StandardError => e
|
101
|
+
raise RubyRoutes::ConstraintViolation, "Proc constraint failed: #{e.message}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Validate an integer constraint.
|
105
|
+
#
|
106
|
+
# @param value [Object] The value to validate.
|
107
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not an integer.
|
108
|
+
def validate_int_constraint(value)
|
109
|
+
invalid! unless value.to_s.match?(/\A\d+\z/)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Validate a UUID constraint.
|
113
|
+
#
|
114
|
+
# @param value [Object] The value to validate.
|
115
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not a valid UUID.
|
116
|
+
def validate_uuid_constraint(value)
|
117
|
+
validate_uuid!(value)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Validate an email constraint.
|
121
|
+
#
|
122
|
+
# @param value [Object] The value to validate.
|
123
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not a valid email.
|
124
|
+
def validate_email_constraint(value)
|
125
|
+
invalid! unless value.to_s.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Validate a slug constraint.
|
129
|
+
#
|
130
|
+
# @param value [Object] The value to validate.
|
131
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not a valid slug.
|
132
|
+
def validate_slug_constraint(value)
|
133
|
+
invalid! unless value.to_s.match?(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Validate an alpha constraint.
|
137
|
+
#
|
138
|
+
# @param value [Object] The value to validate.
|
139
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not alphabetic.
|
140
|
+
def validate_alpha_constraint(value)
|
141
|
+
invalid! unless value.to_s.match?(/\A[a-zA-Z]+\z/)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Validate an alphanumeric constraint.
|
145
|
+
#
|
146
|
+
# @param value [Object] The value to validate.
|
147
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not alphanumeric.
|
148
|
+
def validate_alphanumeric_constraint(value)
|
149
|
+
invalid! unless value.to_s.match?(/\A[a-zA-Z0-9]+\z/)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Validate a UUID.
|
153
|
+
#
|
154
|
+
# This method validates that a value is a properly formatted UUID.
|
155
|
+
#
|
156
|
+
# @param value [Object] The value to validate.
|
157
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not a valid UUID.
|
158
|
+
def validate_uuid!(value)
|
159
|
+
string = value.to_s
|
160
|
+
unless string.length == 36 && string.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
161
|
+
invalid!
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Raise a constraint violation.
|
166
|
+
#
|
167
|
+
# @raise [RubyRoutes::ConstraintViolation] Always raises this exception.
|
168
|
+
def invalid!
|
169
|
+
raise RubyRoutes::ConstraintViolation
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,200 @@
|
|
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
|
+
module ParamSupport
|
16
|
+
include RubyRoutes::Route::WarningHelpers
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Merge incoming params with route defaults.
|
21
|
+
#
|
22
|
+
# This method merges user-provided parameters with route defaults and returns
|
23
|
+
# a thread-local hash for performance.
|
24
|
+
#
|
25
|
+
# @param params [Hash] The user-provided parameters.
|
26
|
+
# @return [Hash] The merged parameters.
|
27
|
+
def build_merged_params(params)
|
28
|
+
return @defaults if params.nil? || params.empty?
|
29
|
+
|
30
|
+
merged_hash = acquire_merge_hash
|
31
|
+
merge_defaults_into(merged_hash)
|
32
|
+
merge_user_params_into(merged_hash, params)
|
33
|
+
merged_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
# Acquire a thread-local hash for merging.
|
37
|
+
#
|
38
|
+
# @return [Hash] A cleared thread-local hash for merging.
|
39
|
+
def acquire_merge_hash
|
40
|
+
merged_hash = Thread.current[:ruby_routes_merge_hash] ||= {}
|
41
|
+
merged_hash.clear
|
42
|
+
merged_hash
|
43
|
+
end
|
44
|
+
|
45
|
+
# Merge defaults into the hash.
|
46
|
+
#
|
47
|
+
# @param merged_hash [Hash] The hash to merge defaults into.
|
48
|
+
# @return [void]
|
49
|
+
def merge_defaults_into(merged_hash)
|
50
|
+
@defaults.each { |key, value| merged_hash[key] = value } unless @defaults.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Merge user params into the hash.
|
54
|
+
#
|
55
|
+
# This method converts keys to strings and skips nil values.
|
56
|
+
#
|
57
|
+
# @param merged_hash [Hash] The hash to merge user parameters into.
|
58
|
+
# @param params [Hash] The user-provided parameters.
|
59
|
+
# @return [void]
|
60
|
+
def merge_user_params_into(merged_hash, params)
|
61
|
+
params.each do |key, value|
|
62
|
+
next if value.nil?
|
63
|
+
|
64
|
+
merged_hash[key.is_a?(String) ? key : key.to_s] = value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Build a frozen cache key for the merged params and update the 2-slot cache.
|
69
|
+
#
|
70
|
+
# @param merged_params [Hash] The merged parameters.
|
71
|
+
# @return [String] The frozen cache key.
|
72
|
+
def build_param_cache_key(merged_params)
|
73
|
+
param_cache_key = cache_key_for_params(@required_params, merged_params)
|
74
|
+
cache_key_hash = param_cache_key.hash
|
75
|
+
|
76
|
+
if (cache_slot = @param_key_slots[0])[0] == cache_key_hash && cache_slot[1] == param_cache_key
|
77
|
+
return cache_slot[1]
|
78
|
+
elsif (cache_slot = @param_key_slots[1])[0] == cache_key_hash && cache_slot[1] == param_cache_key
|
79
|
+
return cache_slot[1]
|
80
|
+
end
|
81
|
+
|
82
|
+
store_param_key_slot(cache_key_hash, param_cache_key)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Store the param cache key in the 2-slot LRU.
|
86
|
+
#
|
87
|
+
# @param cache_key_hash [Integer] The hash of the cache key.
|
88
|
+
# @param param_cache_key [String] The cache key to store.
|
89
|
+
# @return [String] The stored cache key.
|
90
|
+
def store_param_key_slot(cache_key_hash, param_cache_key)
|
91
|
+
if @param_key_slots[0][0].nil?
|
92
|
+
@param_key_slots[0] = [cache_key_hash, param_cache_key]
|
93
|
+
elsif @param_key_slots[1][0].nil?
|
94
|
+
@param_key_slots[1] = [cache_key_hash, param_cache_key]
|
95
|
+
else
|
96
|
+
@param_key_slots[0] = @param_key_slots[1]
|
97
|
+
@param_key_slots[1] = [cache_key_hash, param_cache_key]
|
98
|
+
end
|
99
|
+
param_cache_key
|
100
|
+
end
|
101
|
+
|
102
|
+
# Extract parameters from a request path (and optionally pre-parsed query).
|
103
|
+
#
|
104
|
+
# @param request_path [String] The request path.
|
105
|
+
# @param parsed_qp [Hash, nil] Pre-parsed query parameters to merge (optional).
|
106
|
+
# @return [Hash] The extracted parameters, with defaults merged in.
|
107
|
+
def extract_params(request_path, parsed_qp = nil)
|
108
|
+
extracted_path_params = extract_path_params_fast(request_path)
|
109
|
+
return RubyRoutes::Constant::EMPTY_HASH unless extracted_path_params
|
110
|
+
|
111
|
+
build_params_hash(extracted_path_params, request_path, parsed_qp)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Build full params hash (path + query + defaults + constraints).
|
115
|
+
#
|
116
|
+
# @param path_params [Hash] The extracted path parameters.
|
117
|
+
# @param request_path [String] The request path.
|
118
|
+
# @param parsed_qp [Hash, nil] Pre-parsed query parameters.
|
119
|
+
# @return [Hash] The full parameters hash.
|
120
|
+
def build_params_hash(path_params, request_path, parsed_qp)
|
121
|
+
params_hash = get_thread_local_hash
|
122
|
+
params_hash.update(path_params)
|
123
|
+
|
124
|
+
merge_query_params_into_hash(params_hash, request_path, parsed_qp)
|
125
|
+
|
126
|
+
merge_defaults_fast(params_hash) unless @defaults.empty?
|
127
|
+
validate_constraints_fast!(params_hash) unless @constraints.empty?
|
128
|
+
params_hash.dup
|
129
|
+
end
|
130
|
+
|
131
|
+
# Merge query parameters (if any) from full path into param hash.
|
132
|
+
#
|
133
|
+
# @param route_obj [Route]
|
134
|
+
# @param full_path [String]
|
135
|
+
# @param param_hash [Hash]
|
136
|
+
# @return [void]
|
137
|
+
def merge_query_params(route_obj, full_path, param_hash)
|
138
|
+
return unless full_path.to_s.include?('?')
|
139
|
+
|
140
|
+
if route_obj.respond_to?(:parse_query_params)
|
141
|
+
qp = route_obj.parse_query_params(full_path)
|
142
|
+
param_hash.merge!(qp) if qp
|
143
|
+
elsif route_obj.respond_to?(:query_params)
|
144
|
+
qp = route_obj.query_params(full_path)
|
145
|
+
param_hash.merge!(qp) if qp
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Acquire thread-local hash.
|
150
|
+
#
|
151
|
+
# @return [Hash]
|
152
|
+
def acquire_thread_local_hash
|
153
|
+
pool = Thread.current[:ruby_routes_hash_pool] ||= []
|
154
|
+
return {} if pool.empty?
|
155
|
+
|
156
|
+
hash = pool.pop
|
157
|
+
hash.clear
|
158
|
+
hash
|
159
|
+
end
|
160
|
+
|
161
|
+
alias get_thread_local_hash acquire_thread_local_hash # backward compatibility if referenced elsewhere
|
162
|
+
|
163
|
+
# Merge query params into the hash.
|
164
|
+
#
|
165
|
+
# @param params_hash [Hash] The hash to merge query parameters into.
|
166
|
+
# @param request_path [String] The request path.
|
167
|
+
# @param parsed_qp [Hash, nil] Pre-parsed query parameters.
|
168
|
+
# @return [void]
|
169
|
+
def merge_query_params_into_hash(params_hash, request_path, parsed_qp)
|
170
|
+
if parsed_qp
|
171
|
+
params_hash.merge!(parsed_qp)
|
172
|
+
elsif request_path.include?('?')
|
173
|
+
query_params = query_params_fast(request_path)
|
174
|
+
params_hash.merge!(query_params) unless query_params.empty?
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Merge defaults where absent.
|
179
|
+
#
|
180
|
+
# @param result [Hash] The hash to merge defaults into.
|
181
|
+
# @return [void]
|
182
|
+
def merge_defaults_fast(result)
|
183
|
+
@defaults.each { |key, value| result[key] = value unless result.key?(key) }
|
184
|
+
end
|
185
|
+
|
186
|
+
# Retrieve query params from route object via supported method.
|
187
|
+
#
|
188
|
+
# @param route_obj [Route] The route object.
|
189
|
+
# @param full_path [String] The full path containing the query string.
|
190
|
+
# @return [Hash, nil] The query parameters, or `nil` if none are found.
|
191
|
+
def retrieve_query_params(route_obj, full_path)
|
192
|
+
if route_obj.respond_to?(:parse_query_params)
|
193
|
+
route_obj.parse_query_params(full_path)
|
194
|
+
elsif route_obj.respond_to?(:query_params)
|
195
|
+
route_obj.query_params(full_path)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyRoutes
|
4
|
+
class Route
|
5
|
+
# PathBuilder: generation + segment encoding
|
6
|
+
module PathBuilder
|
7
|
+
private
|
8
|
+
|
9
|
+
# Generate the path string from merged parameters.
|
10
|
+
#
|
11
|
+
# @param merged [Hash] the merged parameters
|
12
|
+
# @return [String] the generated path
|
13
|
+
def generate_path_string(merged)
|
14
|
+
return RubyRoutes::Constant::ROOT_PATH if @compiled_segments.empty?
|
15
|
+
|
16
|
+
buffer = String.new(capacity: estimate_length)
|
17
|
+
buffer << '/'
|
18
|
+
last_index = @compiled_segments.length - 1
|
19
|
+
@compiled_segments.each_with_index do |segment, index|
|
20
|
+
append_segment(buffer, segment, merged, index, last_index)
|
21
|
+
end
|
22
|
+
buffer
|
23
|
+
end
|
24
|
+
|
25
|
+
# Append a segment to the buffer.
|
26
|
+
#
|
27
|
+
# @param buffer [String] the buffer to append to
|
28
|
+
# @param segment [Hash] the segment to append
|
29
|
+
# @param merged [Hash] the merged parameters
|
30
|
+
# @param index [Integer] the current index
|
31
|
+
# @param last_index [Integer] the last index
|
32
|
+
def append_segment(buffer, segment, merged, index, last_index)
|
33
|
+
case segment[:type]
|
34
|
+
when :static
|
35
|
+
buffer << segment[:value]
|
36
|
+
when :param
|
37
|
+
buffer << encode_segment_fast(merged.fetch(segment[:name]).to_s)
|
38
|
+
when :splat
|
39
|
+
buffer << format_splat_value(merged.fetch(segment[:name], ''))
|
40
|
+
end
|
41
|
+
buffer << '/' unless index == last_index
|
42
|
+
end
|
43
|
+
|
44
|
+
# Estimate the length of the path.
|
45
|
+
#
|
46
|
+
# @return [Integer] the estimated length
|
47
|
+
def estimate_length
|
48
|
+
# Rough heuristic (static sizes + average dynamic)
|
49
|
+
base = 1
|
50
|
+
@compiled_segments.each do |segment|
|
51
|
+
base += case segment[:type]
|
52
|
+
when :static then segment[:value].length + 1
|
53
|
+
else 20
|
54
|
+
end
|
55
|
+
end
|
56
|
+
base
|
57
|
+
end
|
58
|
+
|
59
|
+
# Format a splat value.
|
60
|
+
#
|
61
|
+
# @param value [Object] the value to format
|
62
|
+
# @return [String] the formatted value
|
63
|
+
def format_splat_value(value)
|
64
|
+
case value
|
65
|
+
when Array then value.map { |part| encode_segment_fast(part.to_s) }.join('/')
|
66
|
+
when String then value.split('/').map { |part| encode_segment_fast(part) }.join('/')
|
67
|
+
else encode_segment_fast(value.to_s)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Encode a segment fast.
|
72
|
+
#
|
73
|
+
# @param string [String] the string to encode
|
74
|
+
# @return [String] the encoded string
|
75
|
+
def encode_segment_fast(string)
|
76
|
+
return string if RubyRoutes::Constant::UNRESERVED_RE.match?(string)
|
77
|
+
|
78
|
+
@encoding_cache ||= {}
|
79
|
+
# Use gsub instead of tr for proper replacement of + with %20
|
80
|
+
@encoding_cache[string] ||= URI.encode_www_form_component(string).gsub('+', '%20')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,87 @@
|
|
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 = @cache_mutex.synchronize { @gen_cache.get(generation_cache_key) })
|
44
|
+
return cached_path
|
45
|
+
end
|
46
|
+
|
47
|
+
generated_path = generate_path_string(merged_params)
|
48
|
+
frozen_path = generated_path.frozen? ? generated_path : generated_path.dup.freeze
|
49
|
+
@cache_mutex.synchronize { @gen_cache.set(generation_cache_key, frozen_path) }
|
50
|
+
frozen_path
|
51
|
+
end
|
52
|
+
|
53
|
+
# Build a generation cache key for merged params.
|
54
|
+
#
|
55
|
+
# This method creates a cache key from all dynamic path parameters
|
56
|
+
# (required + optional, including splats) present in the merged parameters.
|
57
|
+
#
|
58
|
+
# @param merged_params [Hash] The merged parameters for path generation.
|
59
|
+
# @return [String] The cache key for the generation cache.
|
60
|
+
def build_generation_cache_key(merged_params)
|
61
|
+
names = @required_params.empty? ? @param_names : @required_params
|
62
|
+
names.empty? ? RubyRoutes::Constant::EMPTY_STRING : cache_key_for_params(names, merged_params)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Determine if the route can short-circuit to a static path.
|
66
|
+
#
|
67
|
+
# This method checks if the route is static and the provided parameters
|
68
|
+
# are empty or nil, allowing the static path to be returned directly.
|
69
|
+
#
|
70
|
+
# @param params [Hash] The parameters for path generation.
|
71
|
+
# @return [Boolean] `true` if the route can short-circuit, `false` otherwise.
|
72
|
+
def static_short_circuit?(params)
|
73
|
+
!!@static_path && (params.nil? || params.empty?)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Determine if the route is trivial.
|
77
|
+
#
|
78
|
+
# A route is considered trivial if it has no dynamic segments, no required
|
79
|
+
# parameters, and no constraints, meaning it can resolve to a static path.
|
80
|
+
#
|
81
|
+
# @return [Boolean] `true` if the route is trivial, `false` otherwise.
|
82
|
+
def trivial_route?
|
83
|
+
@compiled_segments.empty? && @required_params.empty? && @constraints.empty?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/utils'
|
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 = @cache_mutex.synchronize { @query_cache.get(query_part) })
|
47
|
+
return cached_result
|
48
|
+
end
|
49
|
+
|
50
|
+
parsed_result = Rack::Utils.parse_query(query_part)
|
51
|
+
@cache_mutex.synchronize { @query_cache.set(query_part, parsed_result) }
|
52
|
+
parsed_result
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|