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.
- checksums.yaml +4 -4
- data/README.md +240 -163
- 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 +86 -36
- data/lib/ruby_routes/radix_tree/finder.rb +213 -0
- data/lib/ruby_routes/radix_tree/inserter.rb +96 -0
- data/lib/ruby_routes/radix_tree.rb +65 -230
- data/lib/ruby_routes/route/check_helpers.rb +115 -0
- data/lib/ruby_routes/route/constraint_validator.rb +173 -0
- data/lib/ruby_routes/route/param_support.rb +200 -0
- data/lib/ruby_routes/route/path_builder.rb +84 -0
- data/lib/ruby_routes/route/path_generation.rb +87 -0
- data/lib/ruby_routes/route/query_helpers.rb +56 -0
- data/lib/ruby_routes/route/segment_compiler.rb +166 -0
- data/lib/ruby_routes/route/small_lru.rb +93 -18
- data/lib/ruby_routes/route/validation_helpers.rb +174 -0
- data/lib/ruby_routes/route/warning_helpers.rb +57 -0
- data/lib/ruby_routes/route.rb +127 -501
- data/lib/ruby_routes/route_set/cache_helpers.rb +76 -0
- data/lib/ruby_routes/route_set/collection_helpers.rb +125 -0
- data/lib/ruby_routes/route_set.rb +140 -132
- data/lib/ruby_routes/router/build_helpers.rb +99 -0
- data/lib/ruby_routes/router/builder.rb +97 -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 +127 -0
- data/lib/ruby_routes/router.rb +196 -182
- 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 +58 -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 +171 -77
- data/lib/ruby_routes/utility/method_utility.rb +137 -0
- data/lib/ruby_routes/utility/path_utility.rb +75 -28
- data/lib/ruby_routes/utility/route_utility.rb +30 -2
- data/lib/ruby_routes/version.rb +3 -1
- data/lib/ruby_routes.rb +68 -11
- 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
|
-
|
5
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
40
|
+
sub(/ies$/, 'y')
|
6
41
|
when /s$/
|
7
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
40
|
-
|
41
|
-
"<a href=\"#{
|
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
|
-
|
46
|
-
method =
|
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,
|
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
|
-
|
51
|
-
# For other methods, use POST with _method hidden field
|
52
|
-
form_method = (method == 'get') ? 'get' : 'post'
|
134
|
+
private
|
53
135
|
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
61
|
-
|
149
|
+
escaped_method = CGI.escapeHTML(method)
|
150
|
+
form_html += "<input type=\"hidden\" name=\"_method\" value=\"#{escaped_method}\">"
|
62
151
|
end
|
63
152
|
|
64
|
-
|
65
|
-
|
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
|
-
#
|
6
|
-
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
@
|
12
|
-
|
13
|
-
@
|
14
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
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
|
-
#
|
72
|
-
|
73
|
-
|
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
|
-
#
|
77
|
-
#
|
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
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
178
|
+
buffer << '|' << value
|
95
179
|
end
|
96
|
-
i += 1
|
97
180
|
end
|
98
|
-
|
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
|