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,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyRoutes
|
4
|
+
class RadixTree
|
5
|
+
# Inserter module for adding routes to the RadixTree.
|
6
|
+
# Handles tokenization, node advancement, and endpoint finalization.
|
7
|
+
module Inserter
|
8
|
+
private
|
9
|
+
|
10
|
+
# Inserts a route into the RadixTree for the given path and HTTP methods.
|
11
|
+
#
|
12
|
+
# @param path_string [String] the path to insert
|
13
|
+
# @param http_methods [Array<String>] the HTTP methods for the route
|
14
|
+
# @param route_handler [Object] the handler for the route
|
15
|
+
# @return [Object] the route handler
|
16
|
+
def insert_route(path_string, http_methods, route_handler)
|
17
|
+
return route_handler if path_string.nil? || path_string.empty?
|
18
|
+
|
19
|
+
tokens = split_path(path_string)
|
20
|
+
current_node = @root
|
21
|
+
tokens.each { |token| current_node = advance_node(current_node, token) }
|
22
|
+
finalize_endpoint(current_node, http_methods, route_handler)
|
23
|
+
route_handler
|
24
|
+
end
|
25
|
+
|
26
|
+
# Advances to the next node based on the token type.
|
27
|
+
#
|
28
|
+
# @param current_node [Node] the current node in the tree
|
29
|
+
# @param token [String] the token to process
|
30
|
+
# @return [Node] the next node
|
31
|
+
def advance_node(current_node, token)
|
32
|
+
case token[0]
|
33
|
+
when ':'
|
34
|
+
handle_dynamic(current_node, token)
|
35
|
+
when '*'
|
36
|
+
handle_wildcard(current_node, token)
|
37
|
+
else
|
38
|
+
handle_static(current_node, token)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Handles dynamic parameter tokens (e.g., :id).
|
43
|
+
#
|
44
|
+
# @param current_node [Node] the current node
|
45
|
+
# @param token [String] the dynamic token
|
46
|
+
# @return [Node] the dynamic child node
|
47
|
+
def handle_dynamic(current_node, token)
|
48
|
+
param_name = token[1..]
|
49
|
+
current_node.dynamic_child ||= build_param_node(param_name)
|
50
|
+
current_node.dynamic_child
|
51
|
+
end
|
52
|
+
|
53
|
+
# Handles wildcard tokens (e.g., *splat).
|
54
|
+
#
|
55
|
+
# @param current_node [Node] the current node
|
56
|
+
# @param token [String] the wildcard token
|
57
|
+
# @return [Node] the wildcard child node
|
58
|
+
def handle_wildcard(current_node, token)
|
59
|
+
param_name = token[1..]
|
60
|
+
param_name = 'splat' if param_name.nil? || param_name.empty?
|
61
|
+
current_node.wildcard_child ||= build_param_node(param_name)
|
62
|
+
current_node.wildcard_child
|
63
|
+
end
|
64
|
+
|
65
|
+
# Handles static literal tokens.
|
66
|
+
#
|
67
|
+
# @param current_node [Node] the current node
|
68
|
+
# @param token [String] the static token
|
69
|
+
# @return [Node] the static child node
|
70
|
+
def handle_static(current_node, token)
|
71
|
+
literal_token = token.freeze
|
72
|
+
current_node.static_children[literal_token] ||= Node.new
|
73
|
+
current_node.static_children[literal_token]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Builds a new node for parameter capture.
|
77
|
+
#
|
78
|
+
# @param param_name [String] the parameter name
|
79
|
+
# @return [Node] the new parameter node
|
80
|
+
def build_param_node(param_name)
|
81
|
+
node = Node.new
|
82
|
+
node.param_name = param_name
|
83
|
+
node
|
84
|
+
end
|
85
|
+
|
86
|
+
# Finalizes the endpoint by adding handlers for HTTP methods.
|
87
|
+
#
|
88
|
+
# @param node [Node] the endpoint node
|
89
|
+
# @param http_methods [Array<String>] the HTTP methods
|
90
|
+
# @param route_handler [Object] the route handler
|
91
|
+
def finalize_endpoint(node, http_methods, route_handler)
|
92
|
+
http_methods.each { |http_method| node.add_handler(http_method, route_handler) }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -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,48 @@ module RubyRoutes
|
|
19
50
|
end
|
20
51
|
end
|
21
52
|
|
53
|
+
# Initialize empty tree and split cache.
|
22
54
|
def initialize
|
23
|
-
@root
|
24
|
-
@split_cache
|
25
|
-
@
|
26
|
-
@
|
27
|
-
@
|
55
|
+
@root = 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
|
-
|
41
|
-
end
|
42
|
-
|
43
|
-
# Insert a route into the tree structure, creating nodes as needed.
|
44
|
-
# Supports static segments, dynamic parameters (:param), and wildcards (*splat).
|
45
|
-
def insert_route(path_str, methods, handler)
|
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
|
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
|
96
73
|
end
|
97
74
|
|
98
|
-
#
|
99
|
-
|
100
|
-
|
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
|
-
|
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 == '/'
|
206
88
|
|
207
|
-
|
208
|
-
if
|
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
|
223
|
-
|
224
|
-
@split_cache[path] = segments
|
225
|
-
@split_cache_order << path
|
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
|
-
|
230
|
-
# Validates route constraints against extracted parameters.
|
231
|
-
# Returns true if all constraints pass, false otherwise.
|
232
|
-
def check_constraints(handler, params)
|
233
|
-
return true unless handler.respond_to?(:constraints)
|
234
|
-
|
235
|
-
constraints = handler.constraints
|
236
|
-
return true unless constraints && !constraints.empty?
|
237
|
-
|
238
|
-
# Check each constraint
|
239
|
-
constraints.each do |param, constraint|
|
240
|
-
param_key = param.to_s
|
241
|
-
value = params[param_key]
|
242
|
-
next unless value
|
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
|
257
|
-
end
|
258
|
-
|
259
|
-
true
|
260
|
-
end
|
261
96
|
end
|
262
97
|
end
|
@@ -0,0 +1,115 @@
|
|
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 (min = constraint[:min_length]) && value && value.length < min
|
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 (max = constraint[:max_length]) && value && value.length > max
|
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 (format = constraint[:format]) && value && !value.match?(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
|
+
range = constraint[:range]
|
104
|
+
return unless range
|
105
|
+
begin
|
106
|
+
integer_value = Integer(value) # raises on nil, floats, or junk like "10abc"
|
107
|
+
rescue ArgumentError, TypeError
|
108
|
+
raise RubyRoutes::ConstraintViolation, 'Value not in allowed range'
|
109
|
+
end
|
110
|
+
|
111
|
+
raise RubyRoutes::ConstraintViolation, 'Value not in allowed range' unless range.cover?(integer_value)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|