ruby_routes 2.2.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 +75 -33
- 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 +79 -227
- 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 +93 -18
- 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 +124 -501
- 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 +120 -133
- 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 +193 -181
- 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 +161 -84
- 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
@@ -1,15 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'route/small_lru'
|
1
4
|
require_relative 'segment'
|
2
5
|
require_relative 'utility/path_utility'
|
6
|
+
require_relative 'utility/method_utility'
|
7
|
+
require_relative 'node'
|
8
|
+
require_relative 'radix_tree/inserter'
|
9
|
+
require_relative 'radix_tree/finder'
|
3
10
|
|
4
11
|
module RubyRoutes
|
5
|
-
# RadixTree
|
6
|
-
#
|
7
|
-
#
|
12
|
+
# RadixTree
|
13
|
+
#
|
14
|
+
# Compact routing trie supporting:
|
15
|
+
# - Static segments (/users)
|
16
|
+
# - Dynamic segments (/users/:id)
|
17
|
+
# - Wildcard splat (/assets/*path)
|
18
|
+
#
|
19
|
+
# Design / Behavior:
|
20
|
+
# - Traversal keeps track of the deepest valid endpoint encountered
|
21
|
+
# (best_match_node) so that if a later branch fails, we can still
|
22
|
+
# return a shorter matching route.
|
23
|
+
# - Dynamic and wildcard captures are written directly into the caller
|
24
|
+
# supplied params Hash (or a fresh one) to avoid intermediate objects.
|
25
|
+
# - A very small manual LRU (Hash + order Array) caches the result of
|
26
|
+
# splitting raw paths into their segment arrays.
|
27
|
+
#
|
28
|
+
# Matching Precedence:
|
29
|
+
# static > dynamic > wildcard
|
30
|
+
#
|
31
|
+
# Thread Safety:
|
32
|
+
# - Not thread‑safe for mutation (intended for boot‑time construction).
|
33
|
+
# Safe for concurrent reads after routes are added.
|
34
|
+
#
|
35
|
+
# @api internal
|
8
36
|
class RadixTree
|
9
37
|
include RubyRoutes::Utility::PathUtility
|
38
|
+
include RubyRoutes::Utility::MethodUtility
|
39
|
+
include Inserter
|
40
|
+
include Finder
|
10
41
|
|
11
42
|
class << self
|
12
|
-
#
|
43
|
+
# Backwards DSL convenience: RadixTree.new(args) → Route
|
13
44
|
def new(*args, &block)
|
14
45
|
if args.any?
|
15
46
|
RubyRoutes::Route.new(*args, &block)
|
@@ -19,244 +50,65 @@ module RubyRoutes
|
|
19
50
|
end
|
20
51
|
end
|
21
52
|
|
53
|
+
# Initialize empty tree and split cache.
|
22
54
|
def initialize
|
23
|
-
@
|
24
|
-
@split_cache
|
25
|
-
@
|
26
|
-
@
|
27
|
-
@
|
55
|
+
@root_node = Node.new
|
56
|
+
@split_cache = RubyRoutes::Route::SmallLru.new(2048)
|
57
|
+
@split_cache_max = 2048
|
58
|
+
@split_cache_order = []
|
59
|
+
@empty_segment_list = [].freeze
|
28
60
|
end
|
29
61
|
|
30
|
-
# Add a route to the
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
62
|
+
# Add a route to the tree (delegates insertion logic).
|
63
|
+
#
|
64
|
+
# @param raw_path [String]
|
65
|
+
# @param http_methods [Array<String,Symbol>]
|
66
|
+
# @param route_handler [Object]
|
67
|
+
# @return [Object] route_handler
|
68
|
+
def add(raw_path, http_methods, route_handler)
|
69
|
+
normalized_path = normalize_path(raw_path)
|
70
|
+
normalized_methods = http_methods.map { |m| normalize_http_method(m) }
|
71
|
+
insert_route(normalized_path, normalized_methods, route_handler)
|
72
|
+
route_handler
|
41
73
|
end
|
42
74
|
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
# Skip empty paths
|
47
|
-
return handler if path_str.nil? || path_str.empty?
|
48
|
-
|
49
|
-
path_parts = split_path(path_str)
|
50
|
-
current_node = @root
|
51
|
-
|
52
|
-
# Add path segments to tree
|
53
|
-
path_parts.each_with_index do |segment, i|
|
54
|
-
if segment.start_with?(':')
|
55
|
-
# Dynamic segment (e.g., :id)
|
56
|
-
param_name = segment[1..-1]
|
57
|
-
|
58
|
-
# Create dynamic child if needed
|
59
|
-
unless current_node.dynamic_child
|
60
|
-
current_node.dynamic_child = Node.new
|
61
|
-
current_node.dynamic_child.param_name = param_name
|
62
|
-
end
|
63
|
-
|
64
|
-
current_node = current_node.dynamic_child
|
65
|
-
elsif segment.start_with?('*')
|
66
|
-
# Wildcard segment (e.g., *path)
|
67
|
-
param_name = segment[1..-1]
|
68
|
-
|
69
|
-
# Create wildcard child if needed
|
70
|
-
unless current_node.wildcard_child
|
71
|
-
current_node.wildcard_child = Node.new
|
72
|
-
current_node.wildcard_child.param_name = param_name
|
73
|
-
end
|
74
|
-
|
75
|
-
current_node = current_node.wildcard_child
|
76
|
-
break # Wildcard consumes the rest of the path
|
77
|
-
else
|
78
|
-
# Static segment - freeze key for memory efficiency and performance
|
79
|
-
segment_key = segment.freeze
|
80
|
-
unless current_node.static_children[segment_key]
|
81
|
-
current_node.static_children[segment_key] = Node.new
|
82
|
-
end
|
83
|
-
|
84
|
-
current_node = current_node.static_children[segment_key]
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
# Mark node as endpoint and add handler for methods
|
89
|
-
current_node.is_endpoint = true
|
90
|
-
Array(methods).each do |method|
|
91
|
-
method_str = method.to_s.upcase
|
92
|
-
current_node.handlers[method_str] = handler
|
93
|
-
end
|
94
|
-
|
95
|
-
handler
|
96
|
-
end
|
97
|
-
|
98
|
-
# Find a matching route in the radix tree with longest prefix match support.
|
99
|
-
# Tracks the deepest endpoint node during traversal so partial matches return
|
100
|
-
# the longest valid prefix, increasing matching flexibility and correctness
|
101
|
-
# for overlapping/static/dynamic/wildcard routes.
|
102
|
-
def find(path, method, params_out = {})
|
103
|
-
# Handle empty path as root
|
104
|
-
path_str = path.to_s
|
105
|
-
method_str = method.to_s.upcase
|
106
|
-
|
107
|
-
# Special case for root path
|
108
|
-
if path_str.empty? || path_str == '/'
|
109
|
-
if @root.is_endpoint && @root.handlers[method_str]
|
110
|
-
return [@root.handlers[method_str], params_out || {}]
|
111
|
-
else
|
112
|
-
return [nil, params_out || {}]
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
# Split path into segments
|
117
|
-
segments = split_path_cached(path_str)
|
118
|
-
return [nil, params_out || {}] if segments.empty?
|
119
|
-
|
120
|
-
params = params_out || {}
|
121
|
-
|
122
|
-
# Track the longest prefix match (deepest endpoint found during traversal)
|
123
|
-
# Only consider nodes that actually match some part of the path
|
124
|
-
longest_match_node = nil
|
125
|
-
longest_match_params = nil
|
126
|
-
|
127
|
-
# Traverse the tree to find matching route
|
128
|
-
current_node = @root
|
129
|
-
segments.each_with_index do |segment, i|
|
130
|
-
next_node, should_break = current_node.traverse_for(segment, i, segments, params)
|
131
|
-
|
132
|
-
# No match found for this segment
|
133
|
-
unless next_node
|
134
|
-
# Return longest prefix match if we found any valid endpoint during traversal
|
135
|
-
if longest_match_node
|
136
|
-
handler = longest_match_node.handlers[method_str]
|
137
|
-
if handler.respond_to?(:constraints)
|
138
|
-
constraints = handler.constraints
|
139
|
-
if constraints && !constraints.empty?
|
140
|
-
return check_constraints(handler, longest_match_params) ? [handler, longest_match_params] : [nil, params]
|
141
|
-
end
|
142
|
-
end
|
143
|
-
return [handler, longest_match_params]
|
144
|
-
end
|
145
|
-
return [nil, params]
|
146
|
-
end
|
147
|
-
|
148
|
-
current_node = next_node
|
149
|
-
|
150
|
-
# Check if current node is a valid endpoint after successful traversal
|
151
|
-
if current_node.is_endpoint && current_node.handlers[method_str]
|
152
|
-
# Store this as our current best match
|
153
|
-
longest_match_node = current_node
|
154
|
-
longest_match_params = params.dup
|
155
|
-
end
|
156
|
-
|
157
|
-
break if should_break # For wildcard paths
|
158
|
-
end
|
159
|
-
|
160
|
-
# Check if final node is an endpoint and has a handler for the method
|
161
|
-
if current_node.is_endpoint && current_node.handlers[method_str]
|
162
|
-
handler = current_node.handlers[method_str]
|
163
|
-
|
164
|
-
# Handle constraints correctly - only check constraints
|
165
|
-
# Don't try to call matches? which test doubles won't have properly stubbed
|
166
|
-
if handler.respond_to?(:constraints)
|
167
|
-
constraints = handler.constraints
|
168
|
-
if constraints && !constraints.empty?
|
169
|
-
if check_constraints(handler, params)
|
170
|
-
return [handler, params]
|
171
|
-
else
|
172
|
-
# If constraints fail, try longest prefix match as fallback
|
173
|
-
if longest_match_node && longest_match_node != current_node
|
174
|
-
fallback_handler = longest_match_node.handlers[method_str]
|
175
|
-
return [fallback_handler, longest_match_params] if fallback_handler
|
176
|
-
end
|
177
|
-
return [nil, params]
|
178
|
-
end
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
return [handler, params]
|
183
|
-
end
|
184
|
-
|
185
|
-
# If we reach here, final node isn't an endpoint - return longest prefix match
|
186
|
-
if longest_match_node
|
187
|
-
handler = longest_match_node.handlers[method_str]
|
188
|
-
if handler.respond_to?(:constraints)
|
189
|
-
constraints = handler.constraints
|
190
|
-
if constraints && !constraints.empty?
|
191
|
-
return check_constraints(handler, longest_match_params) ? [handler, longest_match_params] : [nil, params]
|
192
|
-
end
|
193
|
-
end
|
194
|
-
return [handler, longest_match_params]
|
195
|
-
end
|
196
|
-
|
197
|
-
[nil, params]
|
75
|
+
# Public find delegates to Finder#find (now simplified on this class).
|
76
|
+
def find(request_path_input, request_method_input, params_out = {})
|
77
|
+
super
|
198
78
|
end
|
199
79
|
|
200
80
|
private
|
201
81
|
|
202
|
-
#
|
203
|
-
#
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
if @split_cache.key?(path)
|
209
|
-
# Move accessed key to end for proper LRU behavior
|
210
|
-
@split_cache_order.delete(path)
|
211
|
-
@split_cache_order << path
|
212
|
-
return @split_cache[path]
|
213
|
-
end
|
214
|
-
|
215
|
-
# Split path and add to cache
|
216
|
-
segments = split_path(path)
|
217
|
-
|
218
|
-
# Manage cache size - evict oldest entries when limit reached
|
219
|
-
if @split_cache.size >= @split_cache_max
|
220
|
-
old_key = @split_cache_order.shift
|
221
|
-
@split_cache.delete(old_key)
|
222
|
-
end
|
82
|
+
# Split path with small manual LRU cache.
|
83
|
+
#
|
84
|
+
# @param raw_path [String]
|
85
|
+
# @return [Array<String>]
|
86
|
+
def split_path_cached(raw_path)
|
87
|
+
return @empty_segment_list if raw_path == '/'
|
223
88
|
|
224
|
-
|
225
|
-
|
89
|
+
cached = @split_cache.get(raw_path)
|
90
|
+
return cached if cached
|
226
91
|
|
92
|
+
segments = split_path(raw_path)
|
93
|
+
@split_cache.set(raw_path, segments)
|
227
94
|
segments
|
228
95
|
end
|
229
96
|
|
230
|
-
#
|
231
|
-
#
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
return true unless
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
case constraint
|
245
|
-
when Regexp
|
246
|
-
return false unless constraint.match?(value.to_s)
|
247
|
-
when :int
|
248
|
-
return false unless value.to_s.match?(/\A\d+\z/)
|
249
|
-
when :uuid
|
250
|
-
return false unless value.to_s.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
251
|
-
when Hash
|
252
|
-
if constraint[:range].is_a?(Range)
|
253
|
-
value_num = value.to_i
|
254
|
-
return false unless constraint[:range].include?(value_num)
|
255
|
-
end
|
256
|
-
end
|
97
|
+
# Evaluate constraint rules for a candidate route.
|
98
|
+
#
|
99
|
+
# @param route_handler [Object]
|
100
|
+
# @param captured_params [Hash]
|
101
|
+
# @return [Boolean]
|
102
|
+
def check_constraints(route_handler, captured_params)
|
103
|
+
return true unless route_handler.respond_to?(:validate_constraints_fast!)
|
104
|
+
|
105
|
+
begin
|
106
|
+
# Use a duplicate to avoid unintended mutation by validators.
|
107
|
+
route_handler.validate_constraints_fast!(captured_params)
|
108
|
+
true
|
109
|
+
rescue RubyRoutes::ConstraintViolation
|
110
|
+
false
|
257
111
|
end
|
258
|
-
|
259
|
-
true
|
260
112
|
end
|
261
113
|
end
|
262
114
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyRoutes
|
4
|
+
class Route
|
5
|
+
# CheckHelpers: small helpers for validating hash-form constraints.
|
6
|
+
#
|
7
|
+
# This module provides methods for validating various hash-form constraints,
|
8
|
+
# such as minimum length, maximum length, format, inclusion, exclusion, and range.
|
9
|
+
# Each method raises a `RubyRoutes::ConstraintViolation` exception if the
|
10
|
+
# validation fails.
|
11
|
+
module CheckHelpers
|
12
|
+
# Check minimum length constraint.
|
13
|
+
#
|
14
|
+
# This method validates that the value meets the minimum length specified
|
15
|
+
# in the constraint. If the value is shorter than the minimum length, a
|
16
|
+
# `ConstraintViolation` is raised.
|
17
|
+
#
|
18
|
+
# @param constraint [Hash] The constraint hash containing `:min_length`.
|
19
|
+
# @param value [String] The value to validate.
|
20
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is shorter than the minimum length.
|
21
|
+
# @return [void]
|
22
|
+
def check_min_length(constraint, value)
|
23
|
+
return unless constraint[:min_length] && value.length < constraint[:min_length]
|
24
|
+
|
25
|
+
raise RubyRoutes::ConstraintViolation, "Value too short (minimum #{constraint[:min_length]} characters)"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Check maximum length constraint.
|
29
|
+
#
|
30
|
+
# This method validates that the value does not exceed the maximum length
|
31
|
+
# specified in the constraint. If the value is longer than the maximum length,
|
32
|
+
# a `ConstraintViolation` is raised.
|
33
|
+
#
|
34
|
+
# @param constraint [Hash] The constraint hash containing `:max_length`.
|
35
|
+
# @param value [String] The value to validate.
|
36
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value exceeds the maximum length.
|
37
|
+
# @return [void]
|
38
|
+
def check_max_length(constraint, value)
|
39
|
+
return unless constraint[:max_length] && value.length > constraint[:max_length]
|
40
|
+
|
41
|
+
raise RubyRoutes::ConstraintViolation, "Value too long (maximum #{constraint[:max_length]} characters)"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check format constraint.
|
45
|
+
#
|
46
|
+
# This method validates that the value matches the format specified in the
|
47
|
+
# constraint. If the value does not match the format, a `ConstraintViolation`
|
48
|
+
# is raised.
|
49
|
+
#
|
50
|
+
# @param constraint [Hash] The constraint hash containing `:format` (a Regexp).
|
51
|
+
# @param value [String] The value to validate.
|
52
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value does not match the required format.
|
53
|
+
# @return [void]
|
54
|
+
def check_format(constraint, value)
|
55
|
+
return unless constraint[:format] && !value.match?(constraint[:format])
|
56
|
+
|
57
|
+
raise RubyRoutes::ConstraintViolation, 'Value does not match required format'
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check in list constraint.
|
61
|
+
#
|
62
|
+
# This method validates that the value is included in the list specified
|
63
|
+
# in the constraint. If the value is not in the list, a `ConstraintViolation`
|
64
|
+
# is raised.
|
65
|
+
#
|
66
|
+
# @param constraint [Hash] The constraint hash containing `:in` (an Array).
|
67
|
+
# @param value [String] The value to validate.
|
68
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not in the allowed list.
|
69
|
+
# @return [void]
|
70
|
+
def check_in_list(constraint, value)
|
71
|
+
return unless constraint[:in] && !constraint[:in].include?(value)
|
72
|
+
|
73
|
+
raise RubyRoutes::ConstraintViolation, 'Value not in allowed list'
|
74
|
+
end
|
75
|
+
|
76
|
+
# Check not in list constraint.
|
77
|
+
#
|
78
|
+
# This method validates that the value is not included in the list specified
|
79
|
+
# in the constraint. If the value is in the forbidden list, a `ConstraintViolation`
|
80
|
+
# is raised.
|
81
|
+
#
|
82
|
+
# @param constraint [Hash] The constraint hash containing `:not_in` (an Array).
|
83
|
+
# @param value [String] The value to validate.
|
84
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is in the forbidden list.
|
85
|
+
# @return [void]
|
86
|
+
def check_not_in_list(constraint, value)
|
87
|
+
return unless constraint[:not_in]&.include?(value)
|
88
|
+
|
89
|
+
raise RubyRoutes::ConstraintViolation, 'Value in forbidden list'
|
90
|
+
end
|
91
|
+
|
92
|
+
# Check range constraint.
|
93
|
+
#
|
94
|
+
# This method validates that the value falls within the range specified
|
95
|
+
# in the constraint. If the value is outside the range, a `ConstraintViolation`
|
96
|
+
# is raised.
|
97
|
+
#
|
98
|
+
# @param constraint [Hash] The constraint hash containing `:range` (a Range).
|
99
|
+
# @param value [String] The value to validate.
|
100
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not in the allowed range.
|
101
|
+
# @return [void]
|
102
|
+
def check_range(constraint, value)
|
103
|
+
return unless constraint[:range] && !constraint[:range].cover?(value.to_i)
|
104
|
+
|
105
|
+
raise RubyRoutes::ConstraintViolation, 'Value not in allowed range'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../constant'
|
4
|
+
|
5
|
+
module RubyRoutes
|
6
|
+
class Route
|
7
|
+
# ConstraintValidator: extracted constraint logic.
|
8
|
+
#
|
9
|
+
# This module provides methods for validating route constraints, including
|
10
|
+
# support for regular expressions, procs, hash-based constraints, and built-in
|
11
|
+
# validation rules. It also handles timeouts and raises appropriate exceptions
|
12
|
+
# for constraint violations.
|
13
|
+
module ConstraintValidator
|
14
|
+
# Validate all constraints for the given parameters.
|
15
|
+
#
|
16
|
+
# This method iterates through all constraints and validates each parameter
|
17
|
+
# against its corresponding rule.
|
18
|
+
#
|
19
|
+
# @param params [Hash] The parameters to validate.
|
20
|
+
# @return [void]
|
21
|
+
def validate_constraints_fast!(params)
|
22
|
+
@constraints.each do |key, rule|
|
23
|
+
param_key = key.to_s
|
24
|
+
next unless params.key?(param_key)
|
25
|
+
|
26
|
+
validate_constraint_for(rule, key, params[param_key])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Dispatch a single constraint check.
|
31
|
+
#
|
32
|
+
# This method validates a single parameter against its constraint rule.
|
33
|
+
#
|
34
|
+
# @param rule [Object] The constraint rule (Regexp, Proc, Symbol, Hash).
|
35
|
+
# @param key [String, Symbol] The parameter key.
|
36
|
+
# @param value [Object] The value to validate.
|
37
|
+
# @return [void]
|
38
|
+
def validate_constraint_for(rule, key, value)
|
39
|
+
case rule
|
40
|
+
when Regexp then validate_regexp_constraint(rule, value)
|
41
|
+
when Proc then validate_proc_constraint(key, rule, value)
|
42
|
+
when Hash then validate_hash_constraint!(rule, value.to_s)
|
43
|
+
else
|
44
|
+
validate_builtin_constraint(rule, value)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Handle built-in symbol/string rules via a simple lookup.
|
49
|
+
#
|
50
|
+
# @param rule [Symbol, String] The built-in constraint rule.
|
51
|
+
# @param value [Object] The value to validate.
|
52
|
+
# @return [void]
|
53
|
+
def validate_builtin_constraint(rule, value)
|
54
|
+
method_sym = RubyRoutes::Constant::BUILTIN_VALIDATORS[rule.to_sym] if rule
|
55
|
+
send(method_sym, value) if method_sym
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validate a regexp constraint.
|
59
|
+
#
|
60
|
+
# This method validates a value against a regular expression constraint.
|
61
|
+
# It raises a timeout error if the validation takes too long.
|
62
|
+
#
|
63
|
+
# @param regexp [Regexp] The regular expression to match.
|
64
|
+
# @param value [Object] The value to validate.
|
65
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value does not match the regexp.
|
66
|
+
def validate_regexp_constraint(regexp, value)
|
67
|
+
Timeout.timeout(0.1) { invalid! unless regexp.match?(value.to_s) }
|
68
|
+
rescue Timeout::Error
|
69
|
+
raise RubyRoutes::ConstraintViolation, 'Regex constraint timed out'
|
70
|
+
end
|
71
|
+
|
72
|
+
# Validate a proc constraint.
|
73
|
+
#
|
74
|
+
# This method validates a value using a proc constraint. It emits a
|
75
|
+
# deprecation warning for proc constraints and handles timeouts and errors.
|
76
|
+
#
|
77
|
+
# @param key [String, Symbol] The parameter key.
|
78
|
+
# @param proc [Proc] The proc to call.
|
79
|
+
# @param value [Object] The value to validate.
|
80
|
+
# @raise [RubyRoutes::ConstraintViolation] If the proc constraint fails or times out.
|
81
|
+
def validate_proc_constraint(key, proc, value)
|
82
|
+
warn_proc_constraint_deprecation(key)
|
83
|
+
Timeout.timeout(0.05) { invalid! unless proc.call(value.to_s) }
|
84
|
+
rescue Timeout::Error
|
85
|
+
raise RubyRoutes::ConstraintViolation, 'Proc constraint timed out'
|
86
|
+
rescue StandardError => e
|
87
|
+
raise RubyRoutes::ConstraintViolation, "Proc constraint failed: #{e.message}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Validate an integer constraint.
|
91
|
+
#
|
92
|
+
# @param value [Object] The value to validate.
|
93
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not an integer.
|
94
|
+
def validate_int_constraint(value)
|
95
|
+
invalid! unless value.to_s.match?(/\A\d+\z/)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Validate a UUID constraint.
|
99
|
+
#
|
100
|
+
# @param value [Object] The value to validate.
|
101
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not a valid UUID.
|
102
|
+
def validate_uuid_constraint(value)
|
103
|
+
validate_uuid!(value)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Validate an email constraint.
|
107
|
+
#
|
108
|
+
# @param value [Object] The value to validate.
|
109
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not a valid email.
|
110
|
+
def validate_email_constraint(value)
|
111
|
+
invalid! unless value.to_s.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Validate a slug constraint.
|
115
|
+
#
|
116
|
+
# @param value [Object] The value to validate.
|
117
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not a valid slug.
|
118
|
+
def validate_slug_constraint(value)
|
119
|
+
invalid! unless value.to_s.match?(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Validate an alpha constraint.
|
123
|
+
#
|
124
|
+
# @param value [Object] The value to validate.
|
125
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not alphabetic.
|
126
|
+
def validate_alpha_constraint(value)
|
127
|
+
invalid! unless value.to_s.match?(/\A[a-zA-Z]+\z/)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Validate an alphanumeric constraint.
|
131
|
+
#
|
132
|
+
# @param value [Object] The value to validate.
|
133
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not alphanumeric.
|
134
|
+
def validate_alphanumeric_constraint(value)
|
135
|
+
invalid! unless value.to_s.match?(/\A[a-zA-Z0-9]+\z/)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Validate a UUID.
|
139
|
+
#
|
140
|
+
# This method validates that a value is a properly formatted UUID.
|
141
|
+
#
|
142
|
+
# @param value [Object] The value to validate.
|
143
|
+
# @raise [RubyRoutes::ConstraintViolation] If the value is not a valid UUID.
|
144
|
+
def validate_uuid!(value)
|
145
|
+
string = value.to_s
|
146
|
+
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)
|
147
|
+
invalid!
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Raise a constraint violation.
|
152
|
+
#
|
153
|
+
# @raise [RubyRoutes::ConstraintViolation] Always raises this exception.
|
154
|
+
def invalid!
|
155
|
+
raise RubyRoutes::ConstraintViolation
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|