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.
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 +82 -41
  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 +83 -142
  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 +96 -17
  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 +121 -451
  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 +126 -148
  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 +196 -179
  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 +179 -0
  39. data/lib/ruby_routes/utility/method_utility.rb +137 -0
  40. data/lib/ruby_routes/utility/path_utility.rb +89 -0
  41. data/lib/ruby_routes/utility/route_utility.rb +49 -0
  42. data/lib/ruby_routes/version.rb +3 -1
  43. data/lib/ruby_routes.rb +68 -11
  44. metadata +30 -7
@@ -1,26 +1,70 @@
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
+ wildcard_child_node.param_name = @param_name
43
+ wildcard_child_node
13
44
  end
14
45
 
46
+ # @return [Boolean] always true for wildcard segment
15
47
  def wildcard?
16
48
  true
17
49
  end
18
50
 
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]
51
+ # Attempt to match / consume remaining path.
52
+ #
53
+ # @param current_node [Object] current radix node
54
+ # @param _unused_literal [String] (unused)
55
+ # @param segment_index [Integer] index where wildcard appears
56
+ # @param all_path_segments [Array<String>] all request segments
57
+ # @param captured_params [Hash] params hash to populate
58
+ # @return [Array<(Object, Boolean)>] [wildcard_child_node_or_nil, stop_traversal_flag]
59
+ def match(current_node, _unused_literal, segment_index, all_path_segments, captured_params)
60
+ return [nil, false] unless current_node.wildcard_child
61
+
62
+ wildcard_child_node = current_node.wildcard_child
63
+ if captured_params
64
+ remaining_path = all_path_segments[segment_index..].join('/')
65
+ captured_params[wildcard_child_node.param_name.to_s] = remaining_path
66
+ end
67
+ [wildcard_child_node, true]
24
68
  end
25
69
  end
26
70
  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
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRoutes
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 safety not required (intended for single request thread use).
18
+ #
19
+ # @module RubyRoutes::Utility::KeyBuilderUtility
20
+ module KeyBuilderUtility
21
+ # @!visibility private
22
+ # { "GET" => { "/users" => "GET:/users" } }
23
+ @request_key_pool = {}
24
+ # @!visibility private
25
+ # Circular buffer holding [method_string, path_string] tuples to evict.
26
+ @request_key_ring = Array.new(RubyRoutes::Constant::REQUEST_KEY_CAPACITY)
27
+ # @!visibility private
28
+ @ring_index = 0
29
+ # @!visibility private
30
+ @entry_count = 0
31
+
32
+ class << self
33
+ # Expose pool for diagnostics (read‑only).
34
+ #
35
+ # @return [Hash] A shallow copy of the request key pool for diagnostics.
36
+ attr_reader :request_key_pool
37
+
38
+ # Fetch (or create) a frozen "METHOD:PATH" composite key.
39
+ #
40
+ # On miss:
41
+ # - Builds the String once.
42
+ # - Records it in the nested pool.
43
+ # - Tracks insertion in a fixed ring; when full, overwrites oldest.
44
+ #
45
+ # @param http_method [String] The HTTP method (e.g., "GET").
46
+ # @param request_path [String] The request path (e.g., "/users").
47
+ # @return [String] A frozen canonical key.
48
+ def fetch_request_key(http_method, request_path)
49
+ method_key, path_key = prepare_keys(http_method, request_path)
50
+
51
+ bucket = @request_key_pool[method_key] ||= {}
52
+ return bucket[path_key] if bucket[path_key]
53
+
54
+ handle_cache_miss(bucket, method_key, path_key)
55
+ end
56
+
57
+ private
58
+
59
+ # Prepare keys by freezing them if necessary.
60
+ #
61
+ # @param http_method [String] The HTTP method.
62
+ # @param request_path [String] The request path.
63
+ # @return [Array<String>] An array containing the frozen method and path keys.
64
+ def prepare_keys(http_method, request_path)
65
+ method_key = http_method.frozen? ? http_method : http_method.dup.freeze
66
+ path_key = request_path.frozen? ? request_path : request_path.dup.freeze
67
+ [method_key, path_key]
68
+ end
69
+
70
+ # Handle a cache miss by creating a composite key and updating the ring buffer.
71
+ #
72
+ # @param bucket [Hash] The bucket for the method key.
73
+ # @param method_key [String] The HTTP method key.
74
+ # @param path_key [String] The path key.
75
+ # @return [String] The composite key.
76
+ def handle_cache_miss(bucket, method_key, path_key)
77
+ composite = "#{method_key}:#{path_key}".freeze
78
+ bucket[path_key] = composite
79
+ handle_ring_buffer(method_key, path_key)
80
+ composite
81
+ end
82
+
83
+ # Handle the ring buffer for eviction.
84
+ #
85
+ # @param method_key [String] The HTTP method key.
86
+ # @param path_key [String] The path key.
87
+ # @return [void]
88
+ def handle_ring_buffer(method_key, path_key)
89
+ evict_old_entry if @entry_count >= RubyRoutes::Constant::REQUEST_KEY_CAPACITY
90
+ add_to_ring_buffer(method_key, path_key)
91
+ end
92
+
93
+ # Add a key to the ring buffer.
94
+ #
95
+ # @param method_key [String] The HTTP method key.
96
+ # @param path_key [String] The path key.
97
+ # @return [void]
98
+ def add_to_ring_buffer(method_key, path_key)
99
+ @request_key_ring[@ring_index] = [method_key, path_key]
100
+ @ring_index = (@ring_index + 1) % RubyRoutes::Constant::REQUEST_KEY_CAPACITY
101
+ @entry_count += 1 if @entry_count < RubyRoutes::Constant::REQUEST_KEY_CAPACITY
102
+ end
103
+
104
+ # Evict the oldest entry from the ring buffer.
105
+ #
106
+ # @return [void]
107
+ def evict_old_entry
108
+ old_method, old_path = @request_key_ring[@ring_index]
109
+ old_method_bucket = @request_key_pool[old_method]
110
+ return unless old_method_bucket
111
+
112
+ old_method_bucket.delete(old_path)
113
+ @request_key_pool.delete(old_method) if old_method_bucket.empty?
114
+ end
115
+ end
116
+
117
+ # Build a generic delimited key from components (non‑hot path).
118
+ #
119
+ # Uses a thread‑local mutable buffer to avoid transient objects.
120
+ #
121
+ # @param components [Array<#to_s>] The components to join into a key.
122
+ # @param delimiter [String] The separator (default is ':').
123
+ # @return [String] A frozen key string.
124
+ def build_key(components, delimiter = ':')
125
+ return RubyRoutes::Constant::EMPTY_STRING if components.empty?
126
+
127
+ components.map(&:to_s).join(delimiter).freeze
128
+ end
129
+
130
+ # Return (intern/reuse) a composite request key.
131
+ #
132
+ # @param http_method [String] The HTTP method.
133
+ # @param path [String] The request path.
134
+ # @return [String] A frozen "METHOD:PATH" key.
135
+ def cache_key_for_request(http_method, path)
136
+ KeyBuilderUtility.fetch_request_key(http_method, path.to_s)
137
+ end
138
+
139
+ # Build a cache key from required params and a merged hash in order.
140
+ #
141
+ # Format: "val1|val2/subA/subB|val3".
142
+ #
143
+ # @param required_params [Array<String>] The required parameter keys.
144
+ # @param merged [Hash{String=>Object}] The merged parameters.
145
+ # @return [String] A frozen key (empty if none required).
146
+ def cache_key_for_params(required_params, merged)
147
+ return RubyRoutes::Constant::EMPTY_STRING if required_params.nil? || required_params.empty?
148
+
149
+ buffer = Thread.current[:ruby_routes_param_key_buf] ||= String.new
150
+ buffer.clear
151
+ build_param_key_buffer(required_params, merged, buffer)
152
+ buffer.dup.freeze
153
+ end
154
+
155
+ # Build the parameter key buffer.
156
+ #
157
+ # @param required_params [Array<String>] The required parameter keys.
158
+ # @param merged [Hash] The merged parameters.
159
+ # @param buffer [String] The buffer to build the key into.
160
+ # @return [void]
161
+ def build_param_key_buffer(required_params, merged, buffer)
162
+ key_components = required_params.map { |param| format_param_value(merged[param]) }
163
+ buffer << key_components.join('|')
164
+ end
165
+
166
+ # Format a parameter value for inclusion in the key.
167
+ #
168
+ # @param param_value [Object] The parameter value.
169
+ # @return [String] The formatted parameter value.
170
+ def format_param_value(param_value)
171
+ if param_value.is_a?(Array)
172
+ param_value.join('/')
173
+ else
174
+ param_value.to_s
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end