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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +240 -163
  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 +86 -36
  7. data/lib/ruby_routes/radix_tree/finder.rb +213 -0
  8. data/lib/ruby_routes/radix_tree/inserter.rb +96 -0
  9. data/lib/ruby_routes/radix_tree.rb +65 -230
  10. data/lib/ruby_routes/route/check_helpers.rb +115 -0
  11. data/lib/ruby_routes/route/constraint_validator.rb +173 -0
  12. data/lib/ruby_routes/route/param_support.rb +200 -0
  13. data/lib/ruby_routes/route/path_builder.rb +84 -0
  14. data/lib/ruby_routes/route/path_generation.rb +87 -0
  15. data/lib/ruby_routes/route/query_helpers.rb +56 -0
  16. data/lib/ruby_routes/route/segment_compiler.rb +166 -0
  17. data/lib/ruby_routes/route/small_lru.rb +93 -18
  18. data/lib/ruby_routes/route/validation_helpers.rb +174 -0
  19. data/lib/ruby_routes/route/warning_helpers.rb +57 -0
  20. data/lib/ruby_routes/route.rb +127 -501
  21. data/lib/ruby_routes/route_set/cache_helpers.rb +76 -0
  22. data/lib/ruby_routes/route_set/collection_helpers.rb +125 -0
  23. data/lib/ruby_routes/route_set.rb +140 -132
  24. data/lib/ruby_routes/router/build_helpers.rb +99 -0
  25. data/lib/ruby_routes/router/builder.rb +97 -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 +127 -0
  29. data/lib/ruby_routes/router.rb +196 -182
  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 +58 -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 +171 -77
  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,26 +1,72 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubyRoutes
2
4
  module Segments
5
+ # WildcardSegment
6
+ #
7
+ # Represents a greedy (splat) segment in a route definition (e.g. "*path").
8
+ # Captures the remainder of the request path (including embedded slashes)
9
+ # starting at its position and stores it under the parameter name
10
+ # (text after the asterisk) or "splat" if none provided.
11
+ #
12
+ # Matching Behavior:
13
+ # - Succeeds only if the current radix node has a +wildcard_child+.
14
+ # - Consumes all remaining segments and stops traversal (second tuple true).
15
+ #
16
+ # Returned tuple from #match:
17
+ # [next_node, stop_traversal_flag=true]
18
+ #
19
+ # @api internal
3
20
  class WildcardSegment < BaseSegment
4
- def initialize(text)
5
- @name = (text[1..-1] || 'splat')
21
+ # Correctly derive parameter name for wildcard splats.
22
+ #
23
+ # "*photos" -> "photos"
24
+ # "*" -> "splat" (previous code produced "" and never fell back)
25
+ #
26
+ # Also ensures @raw_text is assigned by delegating to BaseSegment#initialize.
27
+ # @param raw_segment_text [String] raw token (e.g. "*path" or "*")
28
+ def initialize(raw_segment_text)
29
+ super(raw_segment_text)
30
+ tail = raw_segment_text && raw_segment_text[1..]
31
+ tail = nil if tail == '' # treat empty substring as absent
32
+ @param_name = tail || 'splat'
6
33
  end
7
34
 
8
- def ensure_child(current)
9
- current.wildcard_child ||= Node.new
10
- current = current.wildcard_child
11
- current.param_name = @name
12
- current
35
+ # Ensure a wildcard child node on +parent_node+ and assign param name.
36
+ #
37
+ # @param parent_node [Object]
38
+ # @return [Object] wildcard child node
39
+ def ensure_child(parent_node)
40
+ parent_node.wildcard_child ||= Node.new
41
+ wildcard_child_node = parent_node.wildcard_child
42
+ if wildcard_child_node.param_name.nil?
43
+ wildcard_child_node.param_name = @param_name
44
+ end
45
+ wildcard_child_node
13
46
  end
14
47
 
48
+ # @return [Boolean] always true for wildcard segment
15
49
  def wildcard?
16
50
  true
17
51
  end
18
52
 
19
- def match(node, _text, idx, segments, params)
20
- return [nil, false] unless node.wildcard_child
21
- nxt = node.wildcard_child
22
- params[nxt.param_name.to_s] = segments[idx..-1].join('/') if params
23
- [nxt, true]
53
+ # Attempt to match / consume remaining path.
54
+ #
55
+ # @param current_node [Object] current radix node
56
+ # @param _unused_literal [String] (unused)
57
+ # @param segment_index [Integer] index where wildcard appears
58
+ # @param all_path_segments [Array<String>] all request segments
59
+ # @param captured_params [Hash] params hash to populate
60
+ # @return [Array<(Object, Boolean)>] [wildcard_child_node_or_nil, stop_traversal_flag]
61
+ def match(current_node, _unused_literal, segment_index, all_path_segments, captured_params)
62
+ return [nil, false] unless current_node.wildcard_child
63
+
64
+ wildcard_child_node = current_node.wildcard_child
65
+ if captured_params
66
+ remaining_path = all_path_segments[segment_index..].join('/')
67
+ captured_params[wildcard_child_node.param_name.to_s] = remaining_path
68
+ end
69
+ [wildcard_child_node, true]
24
70
  end
25
71
  end
26
72
  end
@@ -1,28 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Minimal String inflection helpers.
4
+ #
5
+ # @note This is a very small, intentionally naive English inflector
6
+ # covering only a few common pluralization patterns used inside the
7
+ # routing DSL (e.g., resources / resource helpers). It is NOT a full
8
+ # replacement for ActiveSupport::Inflector and should not be relied on
9
+ # for general linguistic correctness.
10
+ #
11
+ # @note Supported patterns:
12
+ # - Singularize:
13
+ # * words ending in "ies" -> "y" (companies -> company)
14
+ # * words ending in "s" -> strip trailing "s" (users -> user)
15
+ # - Pluralize:
16
+ # * words ending in "y" -> replace with "ies" (company -> companies)
17
+ # * words ending in sh/ch/x -> append "es" (box -> boxes)
18
+ # * words ending in "z" -> append "zes" (quiz -> quizzes) (simplified)
19
+ # * words ending in "s" -> unchanged
20
+ # * default -> append "s"
21
+ #
22
+ # @note Limitations:
23
+ # - Does not handle irregular forms (person/people, child/children, etc.).
24
+ # - Simplified handling of "z" endings (adds "zes" instead of "zzes").
25
+ # - Case‑sensitive (expects lowercase ASCII).
26
+ #
27
+ # @api internal
1
28
  class String
29
+ # Convert a plural form to a simplistic singular.
30
+ #
31
+ # @example Singularize a word
32
+ # "companies".singularize # => "company"
33
+ # "users".singularize # => "user"
34
+ # "box".singularize # => "box" (unchanged)
35
+ #
36
+ # @return [String] Singularized form (may be the same object if no change is needed).
2
37
  def singularize
3
38
  case self
4
39
  when /ies$/
5
- self.sub(/ies$/, 'y')
40
+ sub(/ies$/, 'y')
6
41
  when /s$/
7
- self.sub(/s$/, '')
42
+ sub(/s$/, '')
8
43
  else
9
44
  self
10
45
  end
11
46
  end
12
47
 
48
+ # Convert a singular form to a simplistic plural.
49
+ #
50
+ # @example Pluralize a word
51
+ # "company".pluralize # => "companies"
52
+ # "box".pluralize # => "boxes"
53
+ # "quiz".pluralize # => "quizzes"
54
+ # "user".pluralize # => "users"
55
+ #
56
+ # @return [String] Pluralized form.
13
57
  def pluralize
14
- case self
15
- when /y$/
16
- self.sub(/y$/, 'ies')
17
- when /sh$/, /ch$/, /x$/
18
- self + 'es'
19
- when /z$/
20
- self + 'zes'
21
- when /s$/
22
- # Words ending in 's' are already plural
23
- self
24
- else
25
- self + 's'
26
- end
58
+ return self if end_with?('s')
59
+ return sub(/y$/, 'ies') if end_with?('y')
60
+ return "#{self}es" if match?(/sh$|ch$|x$/)
61
+ return "#{self}zes" if end_with?('z')
62
+
63
+ "#{self}s"
27
64
  end
28
65
  end
@@ -1,17 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cgi'
2
4
 
3
5
  module RubyRoutes
6
+ # UrlHelpers
7
+ #
8
+ # Mixin that provides named route helper methods (e.g., `user_path`),
9
+ # HTML link/button helpers, and simple redirect data structures.
10
+ #
11
+ # Inclusion pattern:
12
+ # class Application
13
+ # include RubyRoutes::UrlHelpers
14
+ # def route_set; ROUTES end
15
+ # end
16
+ #
17
+ # When a Route is named, router code should call:
18
+ # add_url_helper(:user_path, route_instance)
19
+ #
20
+ # This defines:
21
+ # user_path(params = {}) -> "/users/1"
22
+ #
23
+ # Public instance methods:
24
+ # - `#path_to(name, params)` → String path
25
+ # - `#url_to(name, params)` → Absolute URL (host hard‑coded: localhost)
26
+ # - `#link_to(name, text, params)` → HTML anchor tag
27
+ # - `#button_to(name, text, params)` → HTML form button
28
+ # - `#redirect_to(name, params)` → { status:, location: }
29
+ #
30
+ # Requirements:
31
+ # - Including class must define `#route_set` returning a `RouteSet`.
32
+ #
33
+ # @api public
4
34
  module UrlHelpers
35
+ # Hook for when the module is included.
36
+ #
37
+ # @param base [Class] The class including the module.
38
+ # @return [void]
5
39
  def self.included(base)
6
40
  base.extend(ClassMethods)
7
41
  base.include(base.url_helpers)
8
42
  end
9
43
 
44
+ # Class‑level DSL for defining dynamic helper methods.
10
45
  module ClassMethods
46
+ # Module storing dynamically defined helper methods (memoized).
47
+ #
48
+ # @return [Module] The module containing dynamically defined helpers.
11
49
  def url_helpers
12
50
  @url_helpers ||= Module.new
13
51
  end
14
52
 
53
+ # Define a named route helper method.
54
+ #
55
+ # @param name [Symbol] The helper method name (e.g., `:user_path`).
56
+ # @param route [RubyRoutes::Route] The route instance.
57
+ # @return [void]
15
58
  def add_url_helper(name, route)
16
59
  url_helpers.define_method(name) do |*args|
17
60
  params = args.first || {}
@@ -20,56 +63,95 @@ module RubyRoutes
20
63
  end
21
64
  end
22
65
 
66
+ # Access the dynamically generated helpers module (instance side).
67
+ #
68
+ # @return [Module] The module containing dynamically defined helpers.
23
69
  def url_helpers
24
70
  self.class.url_helpers
25
71
  end
26
72
 
73
+ # Resolve a named route to a path.
74
+ #
75
+ # @param name [Symbol, String] The route name.
76
+ # @param params [Hash] The parameter substitutions.
77
+ # @return [String] The generated path.
27
78
  def path_to(name, params = {})
28
79
  route = route_set.find_named_route(name)
29
80
  route_set.generate_path_from_route(route, params)
30
81
  end
31
82
 
83
+ # Resolve a named route to a full (hard‑coded host) URL.
84
+ #
85
+ # @param name [Symbol, String] The route name.
86
+ # @param params [Hash] The parameter substitutions.
87
+ # @return [String] The absolute URL.
32
88
  def url_to(name, params = {})
33
89
  path = path_to(name, params)
34
90
  "http://localhost#{path}"
35
91
  end
36
92
 
93
+ # Build an HTML anchor tag for a named route.
94
+ #
95
+ # @param name [Symbol, String] The route name.
96
+ # @param text [String] The link text.
97
+ # @param params [Hash] The parameter substitutions.
98
+ # @return [String] The HTML-safe anchor tag.
37
99
  def link_to(name, text, params = {})
38
100
  path = path_to(name, params)
39
- safe_path = CGI.escapeHTML(path.to_s)
40
- safe_text = CGI.escapeHTML(text.to_s)
41
- "<a href=\"#{safe_path}\">#{safe_text}</a>"
101
+ escaped_path = CGI.escapeHTML(path.to_s)
102
+ escaped_text = CGI.escapeHTML(text.to_s)
103
+ "<a href=\"#{escaped_path}\">#{escaped_text}</a>"
42
104
  end
43
105
 
106
+ # Build a minimal HTML form acting as a button submission to a named route.
107
+ #
108
+ # Supports non-GET/POST methods via a hidden `_method` field (Rails style).
109
+ #
110
+ # @param name [Symbol, String] The route name.
111
+ # @param text [String] The button label.
112
+ # @param params [Hash] The parameter substitutions, including optional `:method`.
113
+ # @option params [Symbol, String] :method (:post) The HTTP method.
114
+ # @return [String] The HTML form markup.
44
115
  def button_to(name, text, params = {})
45
- local_params = params ? params.dup : {}
46
- method = local_params.delete(:method) || :post
116
+ params_copy = params ? params.dup : {}
117
+ method = params_copy.delete(:method) || :post
47
118
  method = method.to_s.downcase
48
- path = path_to(name, local_params)
119
+ path = path_to(name, params_copy)
120
+ form_method = method == 'get' ? 'get' : 'post'
121
+ build_form_html(path, form_method, method, text)
122
+ end
123
+
124
+ # Build a simple redirect structure (framework adapter can translate).
125
+ #
126
+ # @param name [Symbol, String] The route name.
127
+ # @param params [Hash] The parameter substitutions.
128
+ # @return [Hash] A hash containing the redirect status and location.
129
+ def redirect_to(name, params = {})
130
+ path = path_to(name, params)
131
+ { status: 302, location: path }
132
+ end
49
133
 
50
- # HTML forms only support GET and POST
51
- # For other methods, use POST with _method hidden field
52
- form_method = (method == 'get') ? 'get' : 'post'
134
+ private
53
135
 
54
- safe_path = CGI.escapeHTML(path.to_s)
55
- safe_form_method = CGI.escapeHTML(form_method)
56
- html = "<form action=\"#{safe_path}\" method=\"#{safe_form_method}\">"
136
+ # Build the form HTML.
137
+ #
138
+ # @param path [String] The form action path.
139
+ # @param form_method [String] The form method (e.g., "get" or "post").
140
+ # @param method [String] The HTTP method (e.g., "put" or "delete").
141
+ # @param text [String] The button label.
142
+ # @return [String] The HTML form markup.
143
+ def build_form_html(path, form_method, method, text)
144
+ escaped_path = CGI.escapeHTML(path.to_s)
145
+ escaped_form_method = CGI.escapeHTML(form_method)
146
+ form_html = "<form action=\"#{escaped_path}\" method=\"#{escaped_form_method}\">"
57
147
 
58
- # Add _method hidden field for non-GET/POST methods
59
148
  if method != 'get' && method != 'post'
60
- safe_method = CGI.escapeHTML(method)
61
- html += "<input type=\"hidden\" name=\"_method\" value=\"#{safe_method}\">"
149
+ escaped_method = CGI.escapeHTML(method)
150
+ form_html += "<input type=\"hidden\" name=\"_method\" value=\"#{escaped_method}\">"
62
151
  end
63
152
 
64
- safe_text = CGI.escapeHTML(text.to_s)
65
- html += "<button type=\"submit\">#{safe_text}</button>"
66
- html += "</form>"
67
- html
68
- end
69
-
70
- def redirect_to(name, params = {})
71
- path = path_to(name, params)
72
- { status: 302, location: path }
153
+ escaped_text = CGI.escapeHTML(text.to_s)
154
+ form_html + "<button type=\"submit\">#{escaped_text}</button></form>"
73
155
  end
74
156
  end
75
157
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRoutes
4
+ module Utility
5
+ # InflectorUtility
6
+ #
7
+ # Minimal internal pluralize/singularize (same logic as before).
8
+ # Not a full linguistic inflector; avoid exposing publicly.
9
+ module InflectorUtility
10
+ module_function
11
+
12
+ def singularize(str)
13
+ return '' if str.nil?
14
+
15
+ case str
16
+ when /ies$/ then str.sub(/ies$/, 'y')
17
+ when /s$/ then str.sub(/s$/, '')
18
+ else str
19
+ end
20
+ end
21
+
22
+ def pluralize(str)
23
+ return '' if str.nil?
24
+
25
+ case str
26
+ when /y$/ then str.sub(/y$/, 'ies')
27
+ when /(sh|ch|x)$/ then "#{str}es"
28
+ when /z$/ then "#{str}zes"
29
+ when /s$/ then str
30
+ else "#{str}s"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,101 +1,195 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubyRoutes
2
4
  module Utility
5
+ # KeyBuilderUtility
6
+ #
7
+ # High‑performance helpers for building and reusing cache keys:
8
+ # 1. Request recognition keys ("METHOD:PATH") via a per‑method nested
9
+ # hash plus a fixed‑size ring buffer for eviction.
10
+ # 2. Parameter combination keys for path generation (ordered values of
11
+ # required params, pipe + slash delimited) with thread‑local String
12
+ # buffers to avoid intermediate allocations.
13
+ #
14
+ # Design goals:
15
+ # - Zero garbage on hot cache hits.
16
+ # - Bounded memory (REQUEST_KEY_CAPACITY ring).
17
+ # - Thread-safe for concurrent access across multiple threads.
3
18
  module KeyBuilderUtility
4
- # ------------------------------------------------------------------
5
- # Fast reusable key storage for "METHOD:PATH" strings.
6
- # O(1) insert + O(1) eviction using a fixed-size ring buffer.
7
- # Only allocates a new String on the *first* time a (method,path) pair appears.
8
- # ------------------------------------------------------------------
9
- REQUEST_KEY_CAPACITY = 4096
10
-
11
- @pool = {} # { method_string => { path_string => frozen_key_string } }
12
- @ring = Array.new(REQUEST_KEY_CAPACITY) # ring entries: [method_string, path_string]
13
- @ring_pos = 0
14
- @entry_cnt = 0
19
+ # @!visibility private
20
+ # { "GET" => { "/users" => "GET:/users" } }
21
+ @request_key_pool = {}
22
+ # @!visibility private
23
+ # Circular buffer holding [method_string, path_string] tuples to evict.
24
+ @request_key_ring = Array.new(RubyRoutes::Constant::REQUEST_KEY_CAPACITY)
25
+ # @!visibility private
26
+ @ring_index = 0
27
+ # @!visibility private
28
+ @entry_count = 0
29
+ # @!visibility private
30
+ @mutex = Mutex.new
15
31
 
16
32
  class << self
17
- attr_reader :pool
18
- def fetch_request_key(method, path)
19
- # Method & path must be strings already (callers ensure)
20
- if (paths = @pool[method])
21
- if (key = paths[path])
22
- return key
23
- end
33
+ # Clear all cached request keys.
34
+ #
35
+ # @return [void]
36
+ def clear!
37
+ @mutex.synchronize do
38
+ @request_key_pool.clear
39
+ @request_key_ring.fill(nil)
40
+ @entry_count = 0
41
+ @ring_index = 0
24
42
  end
43
+ end
25
44
 
26
- # MISS: build & freeze once
27
- key = "#{method}:#{path}".freeze
28
- if paths
29
- paths[path] = key
30
- else
31
- @pool[method] = { path => key }
32
- end
45
+ # Fetch (or create) a frozen "METHOD:PATH" composite key.
46
+ #
47
+ # On miss:
48
+ # - Builds the String once.
49
+ # - Records it in the nested pool.
50
+ # - Tracks insertion in a fixed ring; when full, overwrites oldest.
51
+ #
52
+ # @param http_method [String] The HTTP method (e.g., "GET").
53
+ # @param request_path [String] The request path (e.g., "/users").
54
+ # @return [String] A frozen canonical key.
55
+ def fetch_request_key(http_method, request_path)
56
+ @mutex.synchronize do
57
+ method_key, path_key = prepare_keys(http_method, request_path)
33
58
 
34
- # Evict if ring full (overwrite oldest slot)
35
- if @entry_cnt < REQUEST_KEY_CAPACITY
36
- @ring[@entry_cnt] = [method, path]
37
- @entry_cnt += 1
38
- else
39
- ev_m, ev_p = @ring[@ring_pos]
40
- bucket = @pool[ev_m]
41
- if bucket&.delete(ev_p) && bucket.empty?
42
- @pool.delete(ev_m)
43
- end
44
- @ring[@ring_pos] = [method, path]
45
- @ring_pos += 1
46
- @ring_pos = 0 if @ring_pos == REQUEST_KEY_CAPACITY
59
+ bucket = @request_key_pool[method_key] ||= {}
60
+ return bucket[path_key] if bucket[path_key]
61
+
62
+ handle_cache_miss(bucket, method_key, path_key)
47
63
  end
64
+ end
48
65
 
49
- key
66
+ private
67
+
68
+ # Prepare keys by freezing them if necessary.
69
+ #
70
+ # @param http_method [String] The HTTP method.
71
+ # @param request_path [String] The request path.
72
+ # @return [Array<String>] An array containing the frozen method and path keys.
73
+ def prepare_keys(http_method, request_path)
74
+ method_key = http_method.frozen? ? http_method : http_method.dup.freeze
75
+ path_key = request_path.frozen? ? request_path : request_path.dup.freeze
76
+ [method_key, path_key]
77
+ end
78
+
79
+ # Handle a cache miss by creating a composite key and updating the ring buffer.
80
+ #
81
+ # @param bucket [Hash] The bucket for the method key.
82
+ # @param method_key [String] The HTTP method key.
83
+ # @param path_key [String] The path key.
84
+ # @return [String] The composite key.
85
+ def handle_cache_miss(bucket, method_key, path_key)
86
+ composite = "#{method_key}:#{path_key}".freeze
87
+ bucket[path_key] = composite
88
+ handle_ring_buffer(method_key, path_key)
89
+ composite
90
+ end
91
+
92
+ # Handle the ring buffer for eviction.
93
+ #
94
+ # @param method_key [String] The HTTP method key.
95
+ # @param path_key [String] The path key.
96
+ # @return [void]
97
+ def handle_ring_buffer(method_key, path_key)
98
+ evict_old_entry if @entry_count >= RubyRoutes::Constant::REQUEST_KEY_CAPACITY
99
+ add_to_ring_buffer(method_key, path_key)
50
100
  end
51
- end
52
101
 
53
- # ------------------------------------------------------------------
54
- # Public helpers mixed into instances
55
- # ------------------------------------------------------------------
56
-
57
- # Generic key (rarely hot): joins parts with delim; single allocation.
58
- def build_key(parts, delim = ':')
59
- return ''.freeze if parts.empty?
60
- buf = Thread.current[:ruby_routes_key_buf] ||= String.new
61
- buf.clear
62
- i = 0
63
- while i < parts.length
64
- buf << delim unless i.zero?
65
- buf << parts[i].to_s
66
- i += 1
102
+ # Add a key to the ring buffer.
103
+ #
104
+ # @param method_key [String] The HTTP method key.
105
+ # @param path_key [String] The path key.
106
+ # @return [void]
107
+ def add_to_ring_buffer(method_key, path_key)
108
+ @request_key_ring[@ring_index] = [method_key, path_key]
109
+ @ring_index = (@ring_index + 1) % RubyRoutes::Constant::REQUEST_KEY_CAPACITY
110
+ @entry_count += 1 if @entry_count < RubyRoutes::Constant::REQUEST_KEY_CAPACITY
67
111
  end
68
- buf.dup
112
+
113
+ # Evict the oldest entry from the ring buffer.
114
+ #
115
+ # @return [void]
116
+ def evict_old_entry
117
+ old_method, old_path = @request_key_ring[@ring_index]
118
+ old_method_bucket = @request_key_pool[old_method]
119
+ return unless old_method_bucket
120
+
121
+ old_method_bucket.delete(old_path)
122
+ @request_key_pool.delete(old_method) if old_method_bucket.empty?
123
+ end
124
+ end
125
+
126
+ # Build a generic delimited key from components (non‑hot path).
127
+ #
128
+ # Simple join; acceptable for non‑hot paths.
129
+ #
130
+ # @param components [Array<#to_s>] The components to join into a key.
131
+ # @param delimiter [String] The separator (default is ':').
132
+ # @return [String] A frozen key string.
133
+ def build_key(components, delimiter = ':')
134
+ return RubyRoutes::Constant::EMPTY_STRING if components.empty?
135
+
136
+ components.map(&:to_s).join(delimiter).freeze
69
137
  end
70
138
 
71
- # HOT: request cache key (reused frozen interned string)
72
- def cache_key_for_request(method, path)
73
- KeyBuilderUtility.fetch_request_key(method, path.to_s)
139
+ # Return (intern/reuse) a composite request key.
140
+ #
141
+ # @param http_method [String] The HTTP method.
142
+ # @param path [String] The request path.
143
+ # @return [String] A frozen "METHOD:PATH" key.
144
+ def cache_key_for_request(http_method, path)
145
+ KeyBuilderUtility.fetch_request_key(http_method, path.to_s)
74
146
  end
75
147
 
76
- # HOT: params key produces a short-lived String (dup, not re-frozen each time).
77
- # Callers usually put it into an LRU that duplicates again, so keep it lean.
148
+ # Build a cache key from required params and a merged hash in order.
149
+ #
150
+ # Format: "val1|val2/subA/subB|val3".
151
+ #
152
+ # @param required_params [Array<String>] The required parameter keys.
153
+ # @param merged [Hash{String=>Object}] The merged parameters.
154
+ # @return [String] A frozen key (empty if none required).
78
155
  def cache_key_for_params(required_params, merged)
79
- return ''.freeze if required_params.nil? || required_params.empty?
80
- buf = Thread.current[:ruby_routes_param_key_buf] ||= String.new
81
- buf.clear
82
- i = 0
83
- while i < required_params.length
84
- buf << '|' unless i.zero?
85
- v = merged[required_params[i]]
86
- if v.is_a?(Array)
87
- j = 0
88
- while j < v.length
89
- buf << '/' unless j.zero?
90
- buf << v[j].to_s
91
- j += 1
92
- end
156
+ return RubyRoutes::Constant::EMPTY_STRING if required_params.nil? || required_params.empty?
157
+
158
+ buffer = Thread.current[:ruby_routes_param_key_buf] ||= String.new
159
+ buffer.clear
160
+ build_param_key_buffer(required_params, merged, buffer)
161
+ buffer.dup.freeze
162
+ end
163
+
164
+ # Build the parameter key buffer.
165
+ #
166
+ # @param required_params [Array<String>] The required parameter keys.
167
+ # @param merged [Hash] The merged parameters.
168
+ # @param buffer [String] The buffer to build the key into.
169
+ # @return [void]
170
+ def build_param_key_buffer(required_params, merged, buffer)
171
+ first = true
172
+ required_params.each do |param|
173
+ value = format_param_value(merged[param])
174
+ if first
175
+ buffer << value
176
+ first = false
93
177
  else
94
- buf << v.to_s
178
+ buffer << '|' << value
95
179
  end
96
- i += 1
97
180
  end
98
- buf.dup
181
+ end
182
+
183
+ # Format a parameter value for inclusion in the key.
184
+ #
185
+ # @param param_value [Object] The parameter value.
186
+ # @return [String] The formatted parameter value.
187
+ def format_param_value(param_value)
188
+ if param_value.is_a?(Array)
189
+ param_value.join('/')
190
+ else
191
+ param_value.to_s
192
+ end
99
193
  end
100
194
  end
101
195
  end