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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +240 -163
  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 +86 -36
  7. data/lib/ruby_routes/radix_tree/finder.rb +213 -0
  8. data/lib/ruby_routes/radix_tree/inserter.rb +96 -0
  9. data/lib/ruby_routes/radix_tree.rb +65 -230
  10. data/lib/ruby_routes/route/check_helpers.rb +115 -0
  11. data/lib/ruby_routes/route/constraint_validator.rb +173 -0
  12. data/lib/ruby_routes/route/param_support.rb +200 -0
  13. data/lib/ruby_routes/route/path_builder.rb +84 -0
  14. data/lib/ruby_routes/route/path_generation.rb +87 -0
  15. data/lib/ruby_routes/route/query_helpers.rb +56 -0
  16. data/lib/ruby_routes/route/segment_compiler.rb +166 -0
  17. data/lib/ruby_routes/route/small_lru.rb +93 -18
  18. data/lib/ruby_routes/route/validation_helpers.rb +174 -0
  19. data/lib/ruby_routes/route/warning_helpers.rb +57 -0
  20. data/lib/ruby_routes/route.rb +127 -501
  21. data/lib/ruby_routes/route_set/cache_helpers.rb +76 -0
  22. data/lib/ruby_routes/route_set/collection_helpers.rb +125 -0
  23. data/lib/ruby_routes/route_set.rb +140 -132
  24. data/lib/ruby_routes/router/build_helpers.rb +99 -0
  25. data/lib/ruby_routes/router/builder.rb +97 -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 +127 -0
  29. data/lib/ruby_routes/router.rb +196 -182
  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 +58 -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 +171 -77
  39. data/lib/ruby_routes/utility/method_utility.rb +137 -0
  40. data/lib/ruby_routes/utility/path_utility.rb +75 -28
  41. data/lib/ruby_routes/utility/route_utility.rb +30 -2
  42. data/lib/ruby_routes/version.rb +3 -1
  43. data/lib/ruby_routes.rb +68 -11
  44. 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