ruby_routes 2.3.0 → 2.5.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.
@@ -61,7 +61,21 @@ module RubyRoutes
61
61
  # @param scoped_path [String] The path to prepend the scope's path to.
62
62
  # @return [void]
63
63
  def apply_path_scope(scope, scoped_path)
64
- scoped_path.prepend(scope[:path]) if scope[:path]
64
+ path = scope[:path]&.to_s
65
+ return if path.nil? || path.empty?
66
+
67
+ if path.end_with?('/')
68
+ if scoped_path.start_with?('/')
69
+ scoped_path.prepend(path.chomp('/'))
70
+ else
71
+ scoped_path.prepend(path)
72
+ end
73
+ else
74
+ scoped_path.prepend(scoped_path.start_with?('/') ? path : "#{path}/")
75
+ end
76
+
77
+ # Normalize: Ensure the final path starts with '/'
78
+ scoped_path.prepend('/') unless scoped_path.start_with?('/')
65
79
  end
66
80
 
67
81
  # Apply the module from a scope to the given options.
@@ -73,13 +87,17 @@ module RubyRoutes
73
87
  # @param scoped_options [Hash] The options to update with the module.
74
88
  # @return [void]
75
89
  def apply_module_scope(scope, scoped_options)
76
- return unless scope[:module]
90
+ module_string = scope[:module]&.to_s
91
+ return if module_string.nil? || module_string.empty?
77
92
 
78
- if scoped_options[:to]
79
- controller, action = scoped_options[:to].to_s.split('#', 2)
80
- scoped_options[:to] = "#{scope[:module]}/#{controller}##{action}"
81
- elsif scoped_options[:controller]
82
- scoped_options[:controller].prepend("#{scope[:module]}/")
93
+ if (to_val = scoped_options[:to])
94
+ controller, action = to_val.to_s.split('#', 2)
95
+ return if controller.nil? || controller.empty?
96
+ scoped_options[:to] = action && !action.empty? ? "#{module_string}/#{controller}##{action}" : "#{module_string}/#{controller}"
97
+ elsif (controller = scoped_options[:controller])
98
+ controller_string = controller.to_s
99
+ return if controller_string.empty?
100
+ scoped_options[:controller] = "#{module_string}/#{controller_string}"
83
101
  end
84
102
  end
85
103
 
@@ -91,7 +109,7 @@ module RubyRoutes
91
109
  def apply_defaults_scope(scope, scoped_options)
92
110
  return unless scope[:defaults]
93
111
 
94
- scoped_options[:defaults] = (scoped_options[:defaults] || {}).merge(scope[:defaults])
112
+ scoped_options[:defaults] = scope[:defaults].merge(scoped_options[:defaults] || {})
95
113
  end
96
114
 
97
115
  # Apply the constraints from a scope to the given options.
@@ -102,7 +120,7 @@ module RubyRoutes
102
120
  def apply_constraints_scope(scope, scoped_options)
103
121
  return unless scope[:constraints]
104
122
 
105
- scoped_options[:constraints] = (scoped_options[:constraints] || {}).merge(scope[:constraints])
123
+ scoped_options[:constraints] = scope[:constraints].merge(scoped_options[:constraints] || {})
106
124
  end
107
125
  end
108
126
  end
@@ -3,7 +3,9 @@
3
3
  require_relative 'utility/inflector_utility'
4
4
  require_relative 'utility/route_utility'
5
5
  require_relative 'router/http_helpers'
6
-
6
+ require_relative 'router/resource_helpers'
7
+ require_relative 'constant'
8
+ require_relative 'route_set'
7
9
  module RubyRoutes
8
10
  # RubyRoutes::Router
9
11
  #
@@ -86,11 +88,16 @@ module RubyRoutes
86
88
  return self if @frozen
87
89
 
88
90
  @frozen = true
91
+ if @route_set.respond_to?(:finalize!)
92
+ @route_set.finalize!
93
+ else
94
+ @route_set.freeze
95
+ end
96
+ @route_utils.freeze if @route_utils.respond_to?(:freeze)
89
97
  @scope_stack.freeze
90
98
  @concerns.freeze
91
99
  self
92
100
  end
93
-
94
101
  # Check if the router is frozen.
95
102
  #
96
103
  # @return [Boolean] `true` if the router is frozen, `false` otherwise.
@@ -98,12 +105,17 @@ module RubyRoutes
98
105
  !!@frozen
99
106
  end
100
107
 
101
- # Define a root route.
108
+ # Define the root route.
102
109
  #
103
110
  # @param options [Hash] The options for the root route.
104
111
  # @return [Router] self.
105
112
  def root(options = {})
106
- add_route('/', build_route_options(options, :get))
113
+ ensure_unfrozen!
114
+ if options[:to]
115
+ add_route('/', via: [:get], **options)
116
+ else
117
+ add_route('/', controller: 'root', action: :index, via: [:get], **options)
118
+ end
107
119
  self
108
120
  end
109
121
 
@@ -116,7 +128,9 @@ module RubyRoutes
116
128
  # @param nested_block [Proc] The block for nested routes.
117
129
  # @return [Router] self.
118
130
  def resources(resource_name, options = {}, &nested_block)
131
+ ensure_unfrozen!
119
132
  define_resource_routes(resource_name, options, &nested_block)
133
+ self
120
134
  end
121
135
 
122
136
  # Define a singular resource.
@@ -125,6 +139,7 @@ module RubyRoutes
125
139
  # @param options [Hash] The options for the resource.
126
140
  # @return [Router] self.
127
141
  def resource(resource_name, options = {})
142
+ ensure_unfrozen!
128
143
  singular = RubyRoutes::Utility::InflectorUtility.singularize(resource_name.to_s)
129
144
  controller = options[:controller] || singular
130
145
  define_singular_routes(singular, controller, options)
@@ -139,6 +154,7 @@ module RubyRoutes
139
154
  # @param block [Proc] The block for nested routes.
140
155
  # @return [Router] self.
141
156
  def namespace(namespace_name, options = {}, &block)
157
+ ensure_unfrozen!
142
158
  push_scope({ path: "/#{namespace_name}", module: namespace_name }.merge(options)) do
143
159
  instance_eval(&block) if block
144
160
  end
@@ -150,6 +166,7 @@ module RubyRoutes
150
166
  # @param block [Proc] The block for nested routes.
151
167
  # @return [Router] self.
152
168
  def scope(options_or_path = {}, &block)
169
+ ensure_unfrozen!
153
170
  scope_entry = options_or_path.is_a?(String) ? { path: options_or_path } : options_or_path
154
171
  push_scope(scope_entry) { instance_eval(&block) if block }
155
172
  end
@@ -160,6 +177,7 @@ module RubyRoutes
160
177
  # @param block [Proc] The block for nested routes.
161
178
  # @return [Router] self.
162
179
  def constraints(constraints_hash = {}, &block)
180
+ ensure_unfrozen!
163
181
  push_scope(constraints: constraints_hash) { instance_eval(&block) if block }
164
182
  end
165
183
 
@@ -169,6 +187,7 @@ module RubyRoutes
169
187
  # @param block [Proc] The block for nested routes.
170
188
  # @return [Router] self.
171
189
  def defaults(defaults_hash = {}, &block)
190
+ ensure_unfrozen!
172
191
  push_scope(defaults: defaults_hash) { instance_eval(&block) if block }
173
192
  end
174
193
 
@@ -190,6 +209,7 @@ module RubyRoutes
190
209
  # @param block [Proc] The block for additional routes.
191
210
  # @return [void]
192
211
  def concerns(*concern_names, &block)
212
+ ensure_unfrozen!
193
213
  concern_names.each do |name|
194
214
  concern_block = @concerns[name]
195
215
  raise "Concern '#{name}' not found" unless concern_block
@@ -39,7 +39,9 @@ module RubyRoutes
39
39
  def ensure_child(parent_node)
40
40
  parent_node.wildcard_child ||= Node.new
41
41
  wildcard_child_node = parent_node.wildcard_child
42
- wildcard_child_node.param_name = @param_name
42
+ if wildcard_child_node.param_name.nil?
43
+ wildcard_child_node.param_name = @param_name
44
+ end
43
45
  wildcard_child_node
44
46
  end
45
47
 
@@ -14,9 +14,7 @@ module RubyRoutes
14
14
  # Design goals:
15
15
  # - Zero garbage on hot cache hits.
16
16
  # - Bounded memory (REQUEST_KEY_CAPACITY ring).
17
- # - Thread safety not required (intended for single request thread use).
18
- #
19
- # @module RubyRoutes::Utility::KeyBuilderUtility
17
+ # - Thread-safe for concurrent access across multiple threads.
20
18
  module KeyBuilderUtility
21
19
  # @!visibility private
22
20
  # { "GET" => { "/users" => "GET:/users" } }
@@ -28,12 +26,21 @@ module RubyRoutes
28
26
  @ring_index = 0
29
27
  # @!visibility private
30
28
  @entry_count = 0
29
+ # @!visibility private
30
+ @mutex = Mutex.new
31
31
 
32
32
  class << self
33
- # Expose pool for diagnostics (read‑only).
33
+ # Clear all cached request keys.
34
34
  #
35
- # @return [Hash] A shallow copy of the request key pool for diagnostics.
36
- attr_reader :request_key_pool
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
42
+ end
43
+ end
37
44
 
38
45
  # Fetch (or create) a frozen "METHOD:PATH" composite key.
39
46
  #
@@ -46,12 +53,14 @@ module RubyRoutes
46
53
  # @param request_path [String] The request path (e.g., "/users").
47
54
  # @return [String] A frozen canonical key.
48
55
  def fetch_request_key(http_method, request_path)
49
- method_key, path_key = prepare_keys(http_method, request_path)
56
+ @mutex.synchronize do
57
+ method_key, path_key = prepare_keys(http_method, request_path)
50
58
 
51
- bucket = @request_key_pool[method_key] ||= {}
52
- return bucket[path_key] if bucket[path_key]
59
+ bucket = @request_key_pool[method_key] ||= {}
60
+ return bucket[path_key] if bucket[path_key]
53
61
 
54
- handle_cache_miss(bucket, method_key, path_key)
62
+ handle_cache_miss(bucket, method_key, path_key)
63
+ end
55
64
  end
56
65
 
57
66
  private
@@ -116,7 +125,7 @@ module RubyRoutes
116
125
 
117
126
  # Build a generic delimited key from components (non‑hot path).
118
127
  #
119
- # Uses a thread‑local mutable buffer to avoid transient objects.
128
+ # Simple join; acceptable for non‑hot paths.
120
129
  #
121
130
  # @param components [Array<#to_s>] The components to join into a key.
122
131
  # @param delimiter [String] The separator (default is ':').
@@ -159,8 +168,16 @@ module RubyRoutes
159
168
  # @param buffer [String] The buffer to build the key into.
160
169
  # @return [void]
161
170
  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('|')
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
177
+ else
178
+ buffer << '|' << value
179
+ end
180
+ end
164
181
  end
165
182
 
166
183
  # Format a parameter value for inclusion in the key.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../constant'
4
+ require_relative '../route/small_lru'
4
5
 
5
6
  module RubyRoutes
6
7
  module Utility
@@ -38,15 +39,10 @@ module RubyRoutes
38
39
  }.freeze
39
40
 
40
41
  # Cache for non‑predefined or previously seen method tokens.
41
- # Keys: original `String` or `Symbol`
42
- # Values: frozen uppercase `String`
42
+ # Now uses SmallLru for LRU eviction instead of simple clearing.
43
43
  #
44
- # Note: This is intentionally mutable for caching purposes.
45
- #
46
- # @return [Hash{(String,Symbol) => String}]
47
- # rubocop:disable Style/MutableConstant
48
- METHOD_CACHE = {} # Intentionally mutable for caching
49
- # rubocop:enable Style/MutableConstant
44
+ # @return [SmallLru]
45
+ METHOD_CACHE = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::METHOD_CACHE_MAX_SIZE)
50
46
 
51
47
  # Normalize an HTTP method‑like input to a canonical uppercase `String`.
52
48
  #
@@ -78,10 +74,11 @@ module RubyRoutes
78
74
  # @param method_input [String] The HTTP method input.
79
75
  # @return [String] The normalized HTTP method.
80
76
  def normalize_string_method(method_input)
81
- key = method_input
82
- return key if already_upper_ascii?(key)
77
+ return method_input if already_upper_ascii?(method_input)
83
78
 
84
- METHOD_CACHE[key] ||= ascii_upcase(key).freeze
79
+ # Use SmallLru for LRU eviction, freeze key to prevent mutation
80
+ key = method_input.dup.freeze
81
+ METHOD_CACHE.get(key) || METHOD_CACHE.set(key, ascii_upcase(method_input.dup).freeze)
85
82
  end
86
83
 
87
84
  # Normalize a `Symbol` HTTP method.
@@ -89,7 +86,10 @@ module RubyRoutes
89
86
  # @param method_input [Symbol] The HTTP method input.
90
87
  # @return [String] The normalized HTTP method.
91
88
  def normalize_symbol_method(method_input)
92
- SYMBOL_MAP[method_input] || (METHOD_CACHE[method_input] ||= ascii_upcase(method_input.to_s).freeze)
89
+ SYMBOL_MAP[method_input] || begin
90
+ key = method_input.to_s.freeze
91
+ METHOD_CACHE.get(key) || METHOD_CACHE.set(key, ascii_upcase(method_input.to_s).freeze)
92
+ end
93
93
  end
94
94
 
95
95
  # Normalize an arbitrary HTTP method input.
@@ -98,10 +98,11 @@ module RubyRoutes
98
98
  # @return [String] The normalized HTTP method.
99
99
  def normalize_other_method(method_input)
100
100
  coerced = method_input.to_s
101
- key = coerced
102
- return key if already_upper_ascii?(key)
101
+ return coerced if already_upper_ascii?(coerced)
103
102
 
104
- METHOD_CACHE[key] ||= ascii_upcase(key).freeze
103
+ # Use SmallLru for LRU eviction, freeze key to prevent mutation
104
+ key = coerced.dup.freeze
105
+ METHOD_CACHE.get(key) || METHOD_CACHE.set(key, ascii_upcase(coerced.dup).freeze)
105
106
  end
106
107
 
107
108
  # Determine if a `String` consists solely of uppercase ASCII (`A–Z`) or non‑letters.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyRoutes
4
- VERSION = '2.3.0'
4
+ VERSION = '2.5.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_routes
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yosef Benny Widyokarsono