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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +232 -162
  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 +75 -33
  7. data/lib/ruby_routes/radix_tree/finder.rb +164 -0
  8. data/lib/ruby_routes/radix_tree/inserter.rb +98 -0
  9. data/lib/ruby_routes/radix_tree.rb +79 -227
  10. data/lib/ruby_routes/route/check_helpers.rb +109 -0
  11. data/lib/ruby_routes/route/constraint_validator.rb +159 -0
  12. data/lib/ruby_routes/route/param_support.rb +202 -0
  13. data/lib/ruby_routes/route/path_builder.rb +86 -0
  14. data/lib/ruby_routes/route/path_generation.rb +102 -0
  15. data/lib/ruby_routes/route/query_helpers.rb +56 -0
  16. data/lib/ruby_routes/route/segment_compiler.rb +163 -0
  17. data/lib/ruby_routes/route/small_lru.rb +93 -18
  18. data/lib/ruby_routes/route/validation_helpers.rb +151 -0
  19. data/lib/ruby_routes/route/warning_helpers.rb +54 -0
  20. data/lib/ruby_routes/route.rb +124 -501
  21. data/lib/ruby_routes/route_set/cache_helpers.rb +174 -0
  22. data/lib/ruby_routes/route_set/collection_helpers.rb +127 -0
  23. data/lib/ruby_routes/route_set.rb +120 -133
  24. data/lib/ruby_routes/router/build_helpers.rb +100 -0
  25. data/lib/ruby_routes/router/builder.rb +96 -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 +109 -0
  29. data/lib/ruby_routes/router.rb +193 -181
  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 +56 -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 +161 -84
  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
@@ -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 provides an optimized tree structure for fast route matching.
6
- # Supports static segments, dynamic parameters (:param), and wildcards (*splat).
7
- # Features longest prefix matching and improved LRU caching for performance.
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
- # Allow RadixTree.new(path, options...) to act as a convenience factory
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
- @root = Node.new
24
- @split_cache = {}
25
- @split_cache_order = []
26
- @split_cache_max = 2048
27
- @empty_segments = [].freeze
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 radix tree with specified path, HTTP methods, and handler.
31
- # Returns the handler for method chaining.
32
- def add(path, methods, handler)
33
- # Normalize path
34
- normalized_path = normalize_path(path)
35
-
36
- # Insert into the tree
37
- insert_route(normalized_path, methods, handler)
38
-
39
- # Return handler for chaining
40
- 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
41
73
  end
42
74
 
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
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
- # Improved LRU Path Segment Cache: Accessed keys are moved to the end of the
203
- # order array to ensure proper LRU eviction behavior
204
- def split_path_cached(path)
205
- return @empty_segments if path == '/'
206
-
207
- # Check if path is in cache
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
- @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
96
 
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
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