ruby_routes 2.1.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 +82 -41
- 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 +83 -142
- 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 +96 -17
- 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 +121 -451
- 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 +126 -148
- 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 +196 -179
- 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 +179 -0
- data/lib/ruby_routes/utility/method_utility.rb +137 -0
- data/lib/ruby_routes/utility/path_utility.rb +89 -0
- data/lib/ruby_routes/utility/route_utility.rb +49 -0
- data/lib/ruby_routes/version.rb +3 -1
- data/lib/ruby_routes.rb +68 -11
- metadata +30 -7
@@ -1,43 +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
|
-
#
|
23
|
+
# SmallLru
|
24
|
+
# @!visibility public
|
4
25
|
class SmallLru
|
5
|
-
|
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
|
-
#
|
32
|
+
# @param max_size [Integer] positive maximum size
|
33
|
+
# @raise [ArgumentError] if max_size < 1
|
8
34
|
def initialize(max_size = 1024)
|
9
|
-
|
10
|
-
|
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
|
-
@
|
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
|
-
|
21
|
-
|
58
|
+
lookup_strategy = @hash.key?(key) ? @hit_strategy : @miss_strategy
|
59
|
+
lookup_strategy.call(self, key)
|
22
60
|
end
|
23
61
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
76
|
+
value
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Integer] current number of entries
|
80
|
+
def size
|
81
|
+
@hash.size
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Boolean]
|
85
|
+
def empty?
|
86
|
+
@hash.empty?
|
32
87
|
end
|
33
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]
|
34
102
|
def increment_hits
|
35
103
|
@hits += 1
|
36
104
|
end
|
37
105
|
|
106
|
+
# Increment miss counter (intended for strategy objects).
|
107
|
+
# @return [void]
|
38
108
|
def increment_misses
|
39
109
|
@misses += 1
|
40
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
|
41
120
|
end
|
42
121
|
end
|
43
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
|