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
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../constant'
4
+
5
+ module RubyRoutes
6
+ module Utility
7
+ # MethodUtility
8
+ #
9
+ # High‑performance HTTP method normalization avoiding core
10
+ # `String#upcase` allocations. Supports `String`, `Symbol`, and
11
+ # arbitrary objects (coerced via `#to_s`).
12
+ #
13
+ # Features:
14
+ # - Zero cost for already‑uppercase ASCII strings (fast scan).
15
+ # - Manual ASCII uppercasing (single pass) for `a–z` only.
16
+ # - Symbol fast path via interned constant map (`SYMBOL_MAP`).
17
+ # - Cache (`METHOD_CACHE`) for uncommon verbs or dynamic inputs.
18
+ #
19
+ # Thread Safety:
20
+ # - `METHOD_CACHE` is a shared `Hash`; occasional benign race
21
+ # (double compute of the same key) is acceptable. If strict
22
+ # thread safety is required, wrap in a `Mutex` (not done to
23
+ # preserve performance).
24
+ #
25
+ # @api internal
26
+ module MethodUtility
27
+ # Pre‑interned canonical uppercase strings for common verbs.
28
+ #
29
+ # @return [Hash{Symbol => String}]
30
+ SYMBOL_MAP = {
31
+ get: RubyRoutes::Constant::HTTP_GET,
32
+ post: RubyRoutes::Constant::HTTP_POST,
33
+ put: RubyRoutes::Constant::HTTP_PUT,
34
+ patch: RubyRoutes::Constant::HTTP_PATCH,
35
+ delete: RubyRoutes::Constant::HTTP_DELETE,
36
+ head: RubyRoutes::Constant::HTTP_HEAD,
37
+ options: RubyRoutes::Constant::HTTP_OPTIONS
38
+ }.freeze
39
+
40
+ # Cache for non‑predefined or previously seen method tokens.
41
+ # Keys: original `String` or `Symbol`
42
+ # Values: frozen uppercase `String`
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
50
+
51
+ # Normalize an HTTP method‑like input to a canonical uppercase `String`.
52
+ #
53
+ # Fast paths:
54
+ # - Uppercase ASCII `String`: returned as‑is (no dup/freeze).
55
+ # - `Symbol` in `SYMBOL_MAP`: constant returned.
56
+ #
57
+ # Slow path:
58
+ # - Manual ASCII uppercasing (only `a–z`) + cached.
59
+ #
60
+ # @param method_input [String, Symbol, #to_s] The HTTP method input.
61
+ # @return [String] Canonical uppercase representation.
62
+ # (Cached/transformed values are frozen; uppercase fast-path may return the original `String`.)
63
+ def normalize_http_method(method_input)
64
+ case method_input
65
+ when String
66
+ normalize_string_method(method_input)
67
+ when Symbol
68
+ normalize_symbol_method(method_input)
69
+ else
70
+ normalize_other_method(method_input)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ # Normalize a `String` HTTP method.
77
+ #
78
+ # @param method_input [String] The HTTP method input.
79
+ # @return [String] The normalized HTTP method.
80
+ def normalize_string_method(method_input)
81
+ key = method_input
82
+ return key if already_upper_ascii?(key)
83
+
84
+ METHOD_CACHE[key] ||= ascii_upcase(key).freeze
85
+ end
86
+
87
+ # Normalize a `Symbol` HTTP method.
88
+ #
89
+ # @param method_input [Symbol] The HTTP method input.
90
+ # @return [String] The normalized HTTP method.
91
+ def normalize_symbol_method(method_input)
92
+ SYMBOL_MAP[method_input] || (METHOD_CACHE[method_input] ||= ascii_upcase(method_input.to_s).freeze)
93
+ end
94
+
95
+ # Normalize an arbitrary HTTP method input.
96
+ #
97
+ # @param method_input [#to_s] The HTTP method input.
98
+ # @return [String] The normalized HTTP method.
99
+ def normalize_other_method(method_input)
100
+ coerced = method_input.to_s
101
+ key = coerced
102
+ return key if already_upper_ascii?(key)
103
+
104
+ METHOD_CACHE[key] ||= ascii_upcase(key).freeze
105
+ end
106
+
107
+ # Determine if a `String` consists solely of uppercase ASCII (`A–Z`) or non‑letters.
108
+ #
109
+ # @param candidate [String] The string to check.
110
+ # @return [Boolean] `true` if the string is already uppercase ASCII, `false` otherwise.
111
+ def already_upper_ascii?(candidate)
112
+ candidate.each_byte { |char_code| return false if char_code >= 97 && char_code <= 122 } # a-z
113
+ true
114
+ end
115
+
116
+ # Convert only ASCII lowercase letters (`a–z`) to uppercase in a single pass.
117
+ #
118
+ # Returns the original `String` when no changes are needed to avoid allocation.
119
+ #
120
+ # @param original [String] The original string.
121
+ # @return [String] Either the original or a newly packed transformed string.
122
+ def ascii_upcase(original)
123
+ byte_array = original.bytes
124
+ any_lowercase_transformed = false
125
+ byte_array.each_with_index do |char_code, idx|
126
+ if char_code >= 97 && char_code <= 122
127
+ byte_array[idx] = char_code - 32
128
+ any_lowercase_transformed = true
129
+ end
130
+ end
131
+ return original unless any_lowercase_transformed
132
+
133
+ byte_array.pack('C*')
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRoutes
4
+ module Utility
5
+ # PathUtility
6
+ #
7
+ # Low‑allocation helpers for normalizing and manipulating URL path
8
+ # strings used during route definition, recognition, and generation.
9
+ #
10
+ # @note Design goals:
11
+ # - Idempotent normalization: ensure a single leading slash and trim one
12
+ # trailing slash (except for root).
13
+ # - Internal duplicate slashes are tolerated by `normalize_path`; `split_path`
14
+ # collapses empty segments; `join_path_parts` never produces duplicate
15
+ # separators
16
+ #
17
+ # @note Thread safety: stateless (all methods pure).
18
+ #
19
+ # @api internal
20
+ module PathUtility
21
+ # Normalize a raw path string.
22
+ #
23
+ # Rules:
24
+ # - Ensures a leading slash.
25
+ # - Removes a trailing slash (except when the path is exactly "/").
26
+ #
27
+ # @param raw_path [String, #to_s]
28
+ # @return [String] normalized path (may be the same object if unchanged)
29
+ #
30
+ # @example
31
+ # normalize_path('users/') # => "/users"
32
+ # normalize_path('/users') # => "/users"
33
+ # normalize_path('/') # => "/"
34
+ def normalize_path(raw_path)
35
+ normalized_path = raw_path.to_s
36
+ normalized_path = "/#{normalized_path}" unless normalized_path.start_with?('/')
37
+ normalized_path = normalized_path[0..-2] if normalized_path.length > 1 && normalized_path.end_with?('/')
38
+ normalized_path
39
+ end
40
+
41
+ # Normalize HTTP method to uppercase String (fast path).
42
+ #
43
+ # @param method [String, Symbol]
44
+ # @return [String]
45
+ def normalize_method(method)
46
+ method.to_s.upcase
47
+ end
48
+
49
+ # Split a path into its component segments (excluding leading/trailing slash
50
+ # and any query string). Returns an empty Array for root.
51
+ #
52
+ # @param raw_path [String]
53
+ # @return [Array<String>] segments without empty elements
54
+ #
55
+ # @example
56
+ # split_path('/users/123?x=1') # => ["users", "123"]
57
+ # split_path('/') # => []
58
+ def split_path(raw_path)
59
+ path_without_query = raw_path.to_s.split(/[?#]/, 2).first
60
+ return [] if path_without_query.nil? || path_without_query.empty?
61
+
62
+ path_without_query.split('/').reject(&:empty?)
63
+ end
64
+
65
+ # Join path parts into a normalized absolute path.
66
+ #
67
+ # Allocation minimization:
68
+ # - Precomputes total size to preallocate destination String.
69
+ # - Appends with manual slash insertion.
70
+ #
71
+ # @param path_parts [Array<String>]
72
+ # @return [String] absolute path beginning with '/'
73
+ #
74
+ # @example
75
+ # join_path_parts(%w[users 123]) # => "/users/123"
76
+ def join_path_parts(path_parts)
77
+ estimated_path_size = path_parts.sum { |path_part| path_part.length + 1 } # +1 per slash
78
+ path_buffer = String.new(capacity: estimated_path_size)
79
+ path_buffer << '/'
80
+ last_part_index = path_parts.size - 1
81
+ path_parts.each_with_index do |path_part, part_index|
82
+ path_buffer << path_part
83
+ path_buffer << '/' unless part_index == last_part_index
84
+ end
85
+ path_buffer
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRoutes
4
+ module Utility
5
+ # RouteUtility
6
+ #
7
+ # Internal DSL helper used by Router / RouteSet to construct and
8
+ # register Route objects. It abstracts the two primary entry points:
9
+ #
10
+ # - #define: Build a new Route from a path + options hash and add it.
11
+ # - #register: Add an already-instantiated Route.
12
+ #
13
+ # This separation lets higher-level DSL code remain concise while
14
+ # keeping RouteSet mutation logic centralized.
15
+ #
16
+ # Thread safety: Not thread-safe; expected to be called during
17
+ # application boot / configuration phase.
18
+ #
19
+ # @api internal
20
+ class RouteUtility
21
+ # @param route_set [RubyRoutes::RouteSet]
22
+ def initialize(route_set)
23
+ @route_set = route_set
24
+ end
25
+
26
+ # Build and register a new Route.
27
+ #
28
+ # @param path [String]
29
+ # @param options [Hash] route definition options
30
+ # @return [RubyRoutes::Route]
31
+ #
32
+ # @example
33
+ # util.define('/users/:id', via: :get, to: 'users#show', as: :user)
34
+ def define(path, options = {})
35
+ route = Route.new(path, options)
36
+ register(route)
37
+ end
38
+
39
+ # Register an existing Route instance with the RouteSet.
40
+ #
41
+ # @param route [RubyRoutes::Route]
42
+ # @return [RubyRoutes::Route] the same route (for chaining)
43
+ def register(route)
44
+ @route_set.add_to_collection(route)
45
+ route
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubyRoutes
2
- VERSION = "2.1.0"
4
+ VERSION = '2.3.0'
3
5
  end
data/lib/ruby_routes.rb CHANGED
@@ -1,25 +1,82 @@
1
- require_relative "ruby_routes/version"
2
- require_relative "ruby_routes/string_extensions"
3
- require_relative "ruby_routes/route"
4
- require_relative "ruby_routes/route_set"
5
- require_relative "ruby_routes/url_helpers"
6
- require_relative "ruby_routes/router"
7
- require_relative "ruby_routes/radix_tree"
8
- require_relative "ruby_routes/node"
1
+ # frozen_string_literal: true
9
2
 
3
+ require_relative 'ruby_routes/version'
4
+ require_relative 'ruby_routes/constant'
5
+ require_relative 'ruby_routes/string_extensions'
6
+ require_relative 'ruby_routes/route'
7
+ require_relative 'ruby_routes/route_set'
8
+ require_relative 'ruby_routes/url_helpers'
9
+ require_relative 'ruby_routes/router'
10
+ require_relative 'ruby_routes/radix_tree'
11
+ require_relative 'ruby_routes/node'
12
+ require_relative 'ruby_routes/router/builder'
13
+
14
+ # RubyRoutes
15
+ #
16
+ # RubyRoutes: High-performance, thread-safe routing DSL for Ruby applications.
17
+ # Provides Rails-like route definitions, RESTful resources, constraints, path generation, and advanced caching.
18
+ #
19
+ # See README.md for usage, API, migration, and security notes.
20
+ #
21
+ # Responsibilities:
22
+ # - Autoload and require all core components:
23
+ # - version, string_extensions, route, route_set, url_helpers, router, radix_tree, node, router/builder
24
+ # - Provide `.draw` helper to build a finalized, immutable router.
25
+ # - Expose version and convenience APIs for router creation.
26
+ #
27
+ # Thread-safety & Immutability:
28
+ # - All loaded structures (constants, frozen route sets, caches) are safe for concurrent use.
29
+ # - Modifications (route additions) are not thread-safe; always use `RubyRoutes.draw` in an initializer or at boot.
30
+ # - After build/finalize, all internals are deeply frozen for safety.
31
+ #
32
+ # Security & Migration:
33
+ # - Proc constraints are deprecated; see `MIGRATION_GUIDE.md` for secure alternatives.
34
+ # - `RouteSet#match` returns frozen params; callers must `.dup` if mutation is needed.
35
+ # - See `SECURITY_FIXES.md` for details on security improvements.
36
+ #
37
+ # Performance:
38
+ # - Optimized for low memory usage, high cache hit rates, and zero memory leaks.
39
+ # - See `README.md` for benchmark results.
40
+ #
41
+ # @api public
10
42
  module RubyRoutes
43
+ # Base error class for RubyRoutes-specific exceptions.
11
44
  class Error < StandardError; end
45
+
46
+ # Raised when a route cannot be found.
12
47
  class RouteNotFound < Error; end
48
+
49
+ # Raised when a route is invalid.
13
50
  class InvalidRoute < Error; end
51
+
52
+ # Raised when a constraint validation fails.
14
53
  class ConstraintViolation < Error; end
15
54
 
16
- # Create a new router instance
55
+ # Create a new router instance.
56
+ #
57
+ # @example Define routes using a block
58
+ # router = RubyRoutes.new do
59
+ # get '/health', to: 'system#health'
60
+ # resources :users
61
+ # end
62
+ #
63
+ # @param block [Proc] The block defining the routes.
64
+ # @return [RubyRoutes::Router] A new router instance.
17
65
  def self.new(&block)
18
66
  RubyRoutes::Router.new(&block)
19
67
  end
20
68
 
21
- # Define the routes using a block
69
+ # Define the routes using a block and return a finalized router.
70
+ #
71
+ # @example Define and finalize routes
72
+ # router = RubyRoutes.draw do
73
+ # get '/health', to: 'system#health'
74
+ # resources :users
75
+ # end
76
+ #
77
+ # @param block [Proc] The block defining the routes.
78
+ # @return [RubyRoutes::Router] A finalized router instance.
22
79
  def self.draw(&block)
23
- RubyRoutes::Router.new(&block)
80
+ RubyRoutes::Router.build(&block)
24
81
  end
25
82
  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.1.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yosef Benny Widyokarsono
@@ -10,19 +10,19 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rspec
13
+ name: rack
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '3.12'
18
+ version: '2.2'
19
19
  type: :development
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '3.12'
25
+ version: '2.2'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rake
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -38,19 +38,19 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '13.0'
40
40
  - !ruby/object:Gem::Dependency
41
- name: rack
41
+ name: rspec
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.2'
46
+ version: '3.12'
47
47
  type: :development
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '2.2'
53
+ version: '3.12'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: simplecov
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -81,10 +81,28 @@ files:
81
81
  - lib/ruby_routes/lru_strategies/miss_strategy.rb
82
82
  - lib/ruby_routes/node.rb
83
83
  - lib/ruby_routes/radix_tree.rb
84
+ - lib/ruby_routes/radix_tree/finder.rb
85
+ - lib/ruby_routes/radix_tree/inserter.rb
84
86
  - lib/ruby_routes/route.rb
87
+ - lib/ruby_routes/route/check_helpers.rb
88
+ - lib/ruby_routes/route/constraint_validator.rb
89
+ - lib/ruby_routes/route/param_support.rb
90
+ - lib/ruby_routes/route/path_builder.rb
91
+ - lib/ruby_routes/route/path_generation.rb
92
+ - lib/ruby_routes/route/query_helpers.rb
93
+ - lib/ruby_routes/route/segment_compiler.rb
85
94
  - lib/ruby_routes/route/small_lru.rb
95
+ - lib/ruby_routes/route/validation_helpers.rb
96
+ - lib/ruby_routes/route/warning_helpers.rb
86
97
  - lib/ruby_routes/route_set.rb
98
+ - lib/ruby_routes/route_set/cache_helpers.rb
99
+ - lib/ruby_routes/route_set/collection_helpers.rb
87
100
  - lib/ruby_routes/router.rb
101
+ - lib/ruby_routes/router/build_helpers.rb
102
+ - lib/ruby_routes/router/builder.rb
103
+ - lib/ruby_routes/router/http_helpers.rb
104
+ - lib/ruby_routes/router/resource_helpers.rb
105
+ - lib/ruby_routes/router/scope_helpers.rb
88
106
  - lib/ruby_routes/segment.rb
89
107
  - lib/ruby_routes/segments/base_segment.rb
90
108
  - lib/ruby_routes/segments/dynamic_segment.rb
@@ -92,6 +110,11 @@ files:
92
110
  - lib/ruby_routes/segments/wildcard_segment.rb
93
111
  - lib/ruby_routes/string_extensions.rb
94
112
  - lib/ruby_routes/url_helpers.rb
113
+ - lib/ruby_routes/utility/inflector_utility.rb
114
+ - lib/ruby_routes/utility/key_builder_utility.rb
115
+ - lib/ruby_routes/utility/method_utility.rb
116
+ - lib/ruby_routes/utility/path_utility.rb
117
+ - lib/ruby_routes/utility/route_utility.rb
95
118
  - lib/ruby_routes/version.rb
96
119
  homepage: https://github.com/yosefbennywidyo/ruby_routes
97
120
  licenses: