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,47 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # RubyRoutes::Route::SmallLru
5
+ #
6
+ # A tiny fixed‑capacity Least Recently Used (LRU) cache optimized for:
7
+ # - Very small memory footprint
8
+ # - Low per‑operation overhead
9
+ # - Predictable eviction (oldest key removed when capacity exceeded)
10
+ #
11
+ # Implementation notes:
12
+ # - Backed by a Ruby Hash (insertion order preserves LRU ordering when we reinsert on hit)
13
+ # - get promotes an entry by deleting + reinserting (handled by hit strategy)
14
+ # - External pluggable strategies supply side effects for hit / miss (counters, promotions)
15
+ # - Eviction uses Hash#shift (O(1) for Ruby >= 2.5)
16
+ #
17
+ # Thread safety: NOT thread‑safe. Wrap with a Mutex externally if sharing across threads.
18
+ #
19
+ # Public API surface kept intentionally small for hot path use in routing / path caching.
20
+ #
1
21
  module RubyRoutes
2
22
  class Route
3
- # small LRU used for path generation cache
23
+ # SmallLru
24
+ # @!visibility public
4
25
  class SmallLru
5
- attr_reader :hits, :misses, :evictions
26
+ # @return [Integer] maximum number of entries retained
27
+ # @return [Integer] number of cache hits
28
+ # @return [Integer] number of cache misses
29
+ # @return [Integer] number of evictions (when capacity exceeded)
30
+ attr_reader :max_size, :hits, :misses, :evictions
6
31
 
7
- # larger default to reduce eviction likelihood in benchmarks
32
+ # @param max_size [Integer] positive maximum size
33
+ # @raise [ArgumentError] if max_size < 1
8
34
  def initialize(max_size = 1024)
9
- @max_size = max_size
10
- @h = {}
11
- @hits = 0
12
- @misses = 0
13
- @evictions = 0
35
+ max = Integer(max_size)
36
+ raise ArgumentError, 'max_size must be >= 1' if max < 1
14
37
 
15
- @hit_strategy = RubyRoutes::Constant::LRU_HIT_STRATEGY
38
+ @max_size = max
39
+ @hash = {} # { key => value } (insertion order = LRU order)
40
+ @hits = 0
41
+ @misses = 0
42
+ @evictions = 0
43
+ # Strategy objects must respond_to?(:call). They receive (lru, key) or (lru, key, value).
44
+ @hit_strategy = RubyRoutes::Constant::LRU_HIT_STRATEGY
16
45
  @miss_strategy = RubyRoutes::Constant::LRU_MISS_STRATEGY
17
46
  end
18
47
 
48
+ # Fetch a cached value.
49
+ #
50
+ # On hit:
51
+ # - Strategy updates hit count and reorders key (strategy expected to call increment_hits + promote)
52
+ # On miss:
53
+ # - Strategy updates miss count (strategy expected to call increment_misses)
54
+ #
55
+ # @param key [Object]
56
+ # @return [Object, nil] cached value or nil
19
57
  def get(key)
20
- strategy = @h.key?(key) ? @hit_strategy : @miss_strategy
21
- strategy.call(self, key)
58
+ lookup_strategy = @hash.key?(key) ? @hit_strategy : @miss_strategy
59
+ lookup_strategy.call(self, key)
22
60
  end
23
61
 
24
- def set(key, val)
25
- @h.delete(key) if @h.key?(key)
26
- @h[key] = val
27
- if @h.size > @max_size
28
- @h.shift
62
+ # Insert or update an entry.
63
+ # Re-inserts key to become most recently used.
64
+ # Evicts least recently used (Hash#shift) if capacity exceeded.
65
+ #
66
+ # @param key [Object]
67
+ # @param value [Object]
68
+ # @return [Object] value
69
+ def set(key, value)
70
+ @hash.delete(key) if @hash.key?(key) # promote existing
71
+ @hash[key] = value
72
+ if @hash.size > @max_size
73
+ @hash.shift
29
74
  @evictions += 1
30
75
  end
31
- val
76
+ value
32
77
  end
33
78
 
79
+ # @return [Integer] current number of entries
34
80
  def size
35
- @h.size
81
+ @hash.size
36
82
  end
37
83
 
84
+ # @return [Boolean]
85
+ def empty?
86
+ @hash.empty?
87
+ end
88
+
89
+ # @return [Array<Object>] keys in LRU order (oldest first)
90
+ def keys
91
+ @hash.keys
92
+ end
93
+
94
+ # Debug / spec helper (avoid exposing internal Hash directly).
95
+ # @return [Hash] shallow copy of internal store
96
+ def inspect_hash
97
+ @hash.dup
98
+ end
99
+
100
+ # Increment hit counter (intended for strategy objects).
101
+ # @return [void]
38
102
  def increment_hits
39
103
  @hits += 1
40
104
  end
41
105
 
106
+ # Increment miss counter (intended for strategy objects).
107
+ # @return [void]
42
108
  def increment_misses
43
109
  @misses += 1
44
110
  end
111
+
112
+ # Internal helper used by hit strategy to promote key.
113
+ # @param key [Object]
114
+ # @return [void]
115
+ def promote(key)
116
+ val = @hash.delete(key)
117
+ @hash[key] = val if val
118
+ end
119
+ private :promote
45
120
  end
46
121
  end
47
122
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'check_helpers'
4
+
5
+ module RubyRoutes
6
+ class Route
7
+ # ValidationHelpers: extracted validation and defaults logic.
8
+ #
9
+ # This module provides methods for validating routes, required parameters,
10
+ # and hash-form constraints. It also includes caching mechanisms for
11
+ # validation results and utilities for managing hash pools.
12
+ module ValidationHelpers
13
+ include RubyRoutes::Route::CheckHelpers
14
+
15
+ # Initialize validation result cache.
16
+ #
17
+ # This method initializes an LRU (Least Recently Used) cache for storing
18
+ # validation results, with a maximum size of 64 entries.
19
+ #
20
+ # @return [void]
21
+ def initialize_validation_cache
22
+ @validation_cache = SmallLru.new(64)
23
+ end
24
+
25
+ # Validate fundamental route shape.
26
+ #
27
+ # This method ensures that the route has a valid controller, action, and
28
+ # HTTP method. If any of these are invalid, an `InvalidRoute` exception
29
+ # is raised.
30
+ #
31
+ # @raise [InvalidRoute] If the route is invalid.
32
+ # @return [void]
33
+ def validate_route!
34
+ raise InvalidRoute, 'Controller is required' if @controller.nil?
35
+ raise InvalidRoute, 'Action is required' if @action.nil?
36
+ raise InvalidRoute, "Invalid HTTP method: #{@methods}" if @methods.empty?
37
+ end
38
+
39
+ # Validate required parameters once.
40
+ #
41
+ # This method validates that all required parameters are present and not
42
+ # nil. It ensures that validation is performed only once per request.
43
+ #
44
+ # @param params [Hash] The parameters to validate.
45
+ # @raise [RouteNotFound] If required parameters are missing or nil.
46
+ # @return [void]
47
+ def validate_required_once(params)
48
+ return if @required_params.empty? || @required_validated_once
49
+
50
+ missing, nils = validate_required_params(params)
51
+ raise RouteNotFound, "Missing params: #{missing.join(', ')}" unless missing.empty?
52
+ raise RouteNotFound, "Missing or nil params: #{nils.join(', ')}" unless nils.empty?
53
+
54
+ @required_validated_once = true
55
+ end
56
+
57
+ # Validate required parameters.
58
+ #
59
+ # This method checks for missing or nil required parameters.
60
+ #
61
+ # @param params [Hash] The parameters to validate.
62
+ # @return [Array<Array>] An array containing two arrays:
63
+ # - `missing` [Array<String>] The keys of missing parameters.
64
+ # - `nils` [Array<String>] The keys of parameters that are nil.
65
+ def validate_required_params(params)
66
+ return RubyRoutes::Constant::EMPTY_PAIR if @required_params.empty?
67
+
68
+ missing = []
69
+ nils = []
70
+ @required_params.each do |required_key|
71
+ process_required_key(required_key, params, missing, nils)
72
+ end
73
+ [missing, nils]
74
+ end
75
+
76
+ # Per-key validation helper used by `validate_required_params`.
77
+ #
78
+ # This method checks if a specific required key is present and not nil.
79
+ #
80
+ # @param required_key [String] The required parameter key.
81
+ # @param params [Hash] The parameters to validate.
82
+ # @param missing [Array<String>] The array to store missing keys.
83
+ # @param nils [Array<String>] The array to store keys with nil values.
84
+ # @return [void]
85
+ def process_required_key(required_key, params, missing, nils)
86
+ if params.key?(required_key)
87
+ nils << required_key if params[required_key].nil?
88
+ elsif params.key?(symbol_key = required_key.to_sym)
89
+ nils << required_key if params[symbol_key].nil?
90
+ else
91
+ missing << required_key
92
+ end
93
+ end
94
+
95
+ # Cache validation result.
96
+ #
97
+ # This method stores the validation result in the cache if the parameters
98
+ # are frozen and the cache is not full.
99
+ #
100
+ # @param params [Hash] The parameters used for validation.
101
+ # @param result [Object] The validation result to cache.
102
+ # @return [void]
103
+ def cache_validation_result(params, result)
104
+ return unless params.frozen?
105
+ return unless @validation_cache && @validation_cache.size < 64
106
+
107
+ @validation_cache.set(params.hash, result)
108
+ end
109
+
110
+ # Fetch cached validation result.
111
+ #
112
+ # This method retrieves a cached validation result for the given parameters.
113
+ #
114
+ # @param params [Hash] The parameters used for validation.
115
+ # @return [Object, nil] The cached validation result, or `nil` if not found.
116
+ def get_cached_validation(params)
117
+ @validation_cache&.get(params.hash)
118
+ end
119
+
120
+ # Return hash to pool.
121
+ #
122
+ # This method returns a hash to the thread-local hash pool for reuse.
123
+ #
124
+ # @param hash [Hash] The hash to return to the pool.
125
+ # @return [void]
126
+ def return_hash_to_pool(hash)
127
+ pool = Thread.current[:ruby_routes_hash_pool] ||= []
128
+ pool.push(hash) if pool.size < 5
129
+ end
130
+
131
+ # Validate hash-form constraint rules.
132
+ #
133
+ # This method validates a value against a set of hash-form constraints,
134
+ # such as minimum length, maximum length, format, inclusion, exclusion,
135
+ # and range.
136
+ #
137
+ # @param constraint [Hash] The constraint rules.
138
+ # @param value [String] The value to validate.
139
+ # @raise [RubyRoutes::ConstraintViolation] If the value violates any constraint.
140
+ # @return [void]
141
+ def validate_hash_constraint!(constraint, value)
142
+ check_min_length(constraint, value)
143
+ check_max_length(constraint, value)
144
+ check_format(constraint, value)
145
+ check_in_list(constraint, value)
146
+ check_not_in_list(constraint, value)
147
+ check_range(constraint, value)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRoutes
4
+ class Route
5
+ # WarningHelpers: encapsulate deprecation / warning helpers.
6
+ #
7
+ # This module provides methods for emitting deprecation warnings for
8
+ # deprecated features, such as `Proc` constraints, and suggests secure
9
+ # alternatives.
10
+ module WarningHelpers
11
+ # Emit deprecation warning for `Proc` constraints once per parameter.
12
+ #
13
+ # This method ensures that a deprecation warning for a `Proc` constraint
14
+ # is only emitted once per parameter. It tracks parameters for which
15
+ # warnings have already been shown.
16
+ #
17
+ # @param param [String, Symbol] The parameter name for which the warning
18
+ # is being emitted.
19
+ # @return [void]
20
+ def warn_proc_constraint_deprecation(param)
21
+ return if @proc_warnings_shown&.include?(param)
22
+
23
+ @proc_warnings_shown ||= Set.new
24
+ @proc_warnings_shown << param
25
+ warn_proc_warning(param)
26
+ end
27
+
28
+ # Warn about `Proc` constraint deprecation.
29
+ #
30
+ # This method emits a detailed deprecation warning for `Proc` constraints,
31
+ # explaining the security risks and suggesting secure alternatives.
32
+ #
33
+ # @param param [String, Symbol] The parameter name for which the warning
34
+ # is being emitted.
35
+ # @return [void]
36
+ def warn_proc_warning(param)
37
+ warn <<~WARNING
38
+ [DEPRECATION] Proc constraints are deprecated due to security risks.
39
+
40
+ Parameter: #{param}; Route: #{@path}
41
+
42
+ Secure alternatives:
43
+ - Use regex: constraints: { #{param}: /\\A\\d+\\z/ }
44
+ - Use built-in types: constraints: { #{param}: :int }
45
+ - Use hash constraints: constraints: { #{param}: { min_length: 3, format: /\\A[a-z]+\\z/ } }
46
+
47
+ Available built-in types: :int, :uuid, :email, :slug, :alpha, :alphanumeric
48
+
49
+ This warning will become an error in a future version.
50
+ WARNING
51
+ end
52
+ end
53
+ end
54
+ end