ruby_routes 2.0.0 → 2.2.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/lib/ruby_routes/constant.rb +1 -0
- data/lib/ruby_routes/node.rb +40 -41
- data/lib/ruby_routes/radix_tree.rb +194 -100
- data/lib/ruby_routes/route/small_lru.rb +4 -0
- data/lib/ruby_routes/route.rb +160 -111
- data/lib/ruby_routes/route_set.rb +96 -106
- data/lib/ruby_routes/router.rb +14 -9
- data/lib/ruby_routes/string_extensions.rb +3 -1
- data/lib/ruby_routes/url_helpers.rb +3 -2
- data/lib/ruby_routes/utility/key_builder_utility.rb +102 -0
- data/lib/ruby_routes/utility/path_utility.rb +42 -0
- data/lib/ruby_routes/utility/route_utility.rb +21 -0
- data/lib/ruby_routes/version.rb +1 -1
- metadata +18 -1
@@ -1,94 +1,103 @@
|
|
1
|
+
require_relative 'utility/key_builder_utility'
|
2
|
+
|
1
3
|
module RubyRoutes
|
2
4
|
class RouteSet
|
3
5
|
attr_reader :routes
|
4
6
|
|
7
|
+
include RubyRoutes::Utility::KeyBuilderUtility
|
8
|
+
|
5
9
|
def initialize
|
6
|
-
@tree = RubyRoutes::RadixTree.new
|
7
|
-
@named_routes = {}
|
8
10
|
@routes = []
|
9
|
-
|
11
|
+
@named_routes = {}
|
10
12
|
@recognition_cache = {}
|
13
|
+
@recognition_cache_max = 2048
|
11
14
|
@cache_hits = 0
|
12
15
|
@cache_misses = 0
|
13
|
-
@
|
16
|
+
@radix_tree = RadixTree.new
|
14
17
|
end
|
15
18
|
|
16
|
-
def
|
19
|
+
def add_to_collection(route)
|
17
20
|
@routes << route
|
18
|
-
@
|
21
|
+
@radix_tree.add(route.path, route.methods, route)
|
19
22
|
@named_routes[route.name] = route if route.named?
|
20
|
-
# Clear recognition cache when routes change
|
21
|
-
@recognition_cache.clear if @recognition_cache.size > 100
|
22
|
-
route
|
23
23
|
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
25
|
+
alias_method :add_route, :add_to_collection
|
26
|
+
|
27
|
+
def find_route(method, path)
|
28
|
+
route, _ = @radix_tree.find(path, method)
|
29
|
+
route
|
30
30
|
end
|
31
31
|
|
32
32
|
def find_named_route(name)
|
33
33
|
route = @named_routes[name]
|
34
|
-
|
35
|
-
|
34
|
+
raise RouteNotFound.new("No route named '#{name}'") unless route
|
35
|
+
route
|
36
36
|
end
|
37
37
|
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
FAST_METHOD_MAP = {
|
39
|
+
get: 'GET', post: 'POST', put: 'PUT', patch: 'PATCH',
|
40
|
+
delete: 'DELETE', head: 'HEAD', options: 'OPTIONS'
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
def normalize_method_input(method)
|
44
|
+
case method
|
45
|
+
when Symbol
|
46
|
+
FAST_METHOD_MAP[method] || method.to_s.upcase
|
47
|
+
when String
|
48
|
+
# Fast path: assume already correct; fallback only for common lowercase
|
49
|
+
return method if method.length <= 6 && method == method.upcase
|
50
|
+
FAST_METHOD_MAP[method.downcase.to_sym] || method.upcase
|
51
|
+
else
|
52
|
+
s = method.to_s
|
53
|
+
FAST_METHOD_MAP[s.downcase.to_sym] || s.upcase
|
54
|
+
end
|
55
|
+
end
|
56
|
+
private :normalize_method_input
|
41
57
|
|
42
|
-
|
43
|
-
|
58
|
+
def match(method, path)
|
59
|
+
m = normalize_method_input(method)
|
60
|
+
raw = path.to_s
|
61
|
+
cache_key = cache_key_for_request(m, raw)
|
44
62
|
|
45
|
-
#
|
46
|
-
if (
|
63
|
+
# Single cache lookup with proper hit accounting
|
64
|
+
if (hit = @recognition_cache[cache_key])
|
47
65
|
@cache_hits += 1
|
48
|
-
return
|
66
|
+
return hit
|
49
67
|
end
|
50
68
|
|
51
69
|
@cache_misses += 1
|
52
70
|
|
53
|
-
|
54
|
-
params = get_thread_local_params
|
55
|
-
handler, _ = @tree.find(request_path, method_up, params)
|
56
|
-
return nil unless handler
|
71
|
+
path_without_query, _qs = raw.split('?', 2)
|
57
72
|
|
58
|
-
|
73
|
+
# Use normalized method (m) for trie lookup
|
74
|
+
route, params = @radix_tree.find(path_without_query, m)
|
75
|
+
return nil unless route
|
59
76
|
|
60
|
-
|
61
|
-
merge_defaults(route, params) if route.defaults && !route.defaults.empty?
|
77
|
+
merge_query_params(route, raw, params)
|
62
78
|
|
63
|
-
|
64
|
-
|
65
|
-
merge_query_params(route, request_path, params)
|
79
|
+
if route.respond_to?(:defaults) && route.defaults
|
80
|
+
route.defaults.each { |k,v| params[k.to_s] = v unless params.key?(k.to_s) }
|
66
81
|
end
|
67
82
|
|
68
|
-
# Create return hash and cache the complete result
|
69
|
-
result_params = params.dup
|
70
83
|
result = {
|
71
84
|
route: route,
|
72
|
-
params:
|
85
|
+
params: params,
|
73
86
|
controller: route.controller,
|
74
87
|
action: route.action
|
75
|
-
}
|
76
|
-
|
88
|
+
}
|
89
|
+
|
77
90
|
insert_cache_entry(cache_key, result)
|
78
91
|
result
|
79
92
|
end
|
80
93
|
|
81
|
-
def recognize_path(path, method =
|
94
|
+
def recognize_path(path, method = 'GET')
|
82
95
|
match(method, path)
|
83
96
|
end
|
84
97
|
|
85
98
|
def generate_path(name, params = {})
|
86
|
-
route =
|
87
|
-
|
88
|
-
route.generate_path(params)
|
89
|
-
else
|
90
|
-
raise RouteNotFound, "No route named '#{name}'"
|
91
|
-
end
|
99
|
+
route = find_named_route(name)
|
100
|
+
route.generate_path(params)
|
92
101
|
end
|
93
102
|
|
94
103
|
def generate_path_from_route(route, params = {})
|
@@ -99,95 +108,76 @@ module RubyRoutes
|
|
99
108
|
@routes.clear
|
100
109
|
@named_routes.clear
|
101
110
|
@recognition_cache.clear
|
102
|
-
@
|
103
|
-
@
|
111
|
+
@cache_hits = 0
|
112
|
+
@cache_misses = 0
|
113
|
+
# Create a new radix tree since we can't clear it
|
114
|
+
@radix_tree = RadixTree.new
|
104
115
|
end
|
105
116
|
|
106
117
|
def size
|
107
118
|
@routes.size
|
108
119
|
end
|
109
|
-
alias_method :length, :size
|
110
120
|
|
111
121
|
def empty?
|
112
122
|
@routes.empty?
|
113
123
|
end
|
114
124
|
|
115
|
-
def each(&block)
|
116
|
-
return enum_for(:each) unless block_given?
|
117
|
-
@routes.each(&block)
|
118
|
-
end
|
119
|
-
|
120
|
-
def include?(route)
|
121
|
-
@routes.include?(route)
|
122
|
-
end
|
123
|
-
|
124
|
-
# Performance monitoring
|
125
125
|
def cache_stats
|
126
|
-
|
127
|
-
hit_rate = total > 0 ? (@cache_hits.to_f / total * 100).round(2) : 0
|
126
|
+
lookups = @cache_hits + @cache_misses
|
128
127
|
{
|
129
128
|
hits: @cache_hits,
|
130
129
|
misses: @cache_misses,
|
131
|
-
hit_rate:
|
130
|
+
hit_rate: lookups.zero? ? 0.0 : (@cache_hits.to_f / lookups * 100.0),
|
132
131
|
size: @recognition_cache.size
|
133
132
|
}
|
134
133
|
end
|
135
134
|
|
136
|
-
|
137
|
-
|
138
|
-
# Method lookup table to avoid repeated upcasing with interned strings
|
139
|
-
def method_lookup(method)
|
140
|
-
@method_cache ||= Hash.new { |h, k| h[k] = k.to_s.upcase.freeze }
|
141
|
-
@method_cache[method]
|
142
|
-
end
|
143
|
-
|
144
|
-
# Optimized cache key building - avoid string interpolation
|
145
|
-
def build_cache_key(method, path)
|
146
|
-
# Use string interpolation which is faster than buffer + dup + freeze
|
147
|
-
# String interpolation creates a new string directly without intermediate allocations
|
148
|
-
"#{method}:#{path}".freeze
|
135
|
+
def each(&block)
|
136
|
+
@routes.each(&block)
|
149
137
|
end
|
150
138
|
|
151
|
-
|
152
|
-
|
153
|
-
# Use single thread-local hash that gets cleared, avoiding pool management overhead
|
154
|
-
hash = Thread.current[:ruby_routes_params_hash] ||= {}
|
155
|
-
hash.clear
|
156
|
-
hash
|
139
|
+
def include?(route)
|
140
|
+
@routes.include?(route)
|
157
141
|
end
|
158
142
|
|
159
|
-
|
160
|
-
# No-op since we're using a single reusable hash per thread
|
161
|
-
end
|
143
|
+
private
|
162
144
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
145
|
+
def insert_cache_entry(key, value)
|
146
|
+
# unchanged cache insert (key already frozen & reusable)
|
147
|
+
if @recognition_cache.size >= @recognition_cache_max
|
148
|
+
@recognition_cache.keys.first(@recognition_cache_max / 4).each { |k| @recognition_cache.delete(k) }
|
149
|
+
end
|
150
|
+
@recognition_cache[key] = value
|
151
|
+
end
|
152
|
+
|
153
|
+
# Add the missing method for merging query params
|
154
|
+
def merge_query_params(route, path, params)
|
155
|
+
# Check for query string
|
156
|
+
if path.to_s.include?('?')
|
157
|
+
if route.respond_to?(:parse_query_params)
|
158
|
+
query_params = route.parse_query_params(path)
|
159
|
+
params.merge!(query_params) if query_params
|
160
|
+
elsif route.respond_to?(:query_params)
|
161
|
+
query_params = route.query_params(path)
|
162
|
+
params.merge!(query_params) if query_params
|
163
|
+
end
|
167
164
|
end
|
168
165
|
end
|
169
166
|
|
170
|
-
#
|
171
|
-
def
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
params.merge!(qp) unless qp.empty?
|
167
|
+
# Add thread-local params pool methods
|
168
|
+
def get_thread_local_params
|
169
|
+
thread_params = Thread.current[:ruby_routes_params_pool] ||= []
|
170
|
+
if thread_params.empty?
|
171
|
+
{}
|
172
|
+
else
|
173
|
+
thread_params.pop.clear
|
178
174
|
end
|
179
175
|
end
|
180
176
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
# Simple eviction: clear cache when it gets too large
|
186
|
-
if @recognition_cache.size > @recognition_cache_max
|
187
|
-
# Keep most recently used half
|
188
|
-
keys_to_delete = @recognition_cache.keys[0...(@recognition_cache_max / 2)]
|
189
|
-
keys_to_delete.each { |k| @recognition_cache.delete(k) }
|
190
|
-
end
|
177
|
+
def return_params_to_pool(params)
|
178
|
+
params.clear
|
179
|
+
thread_pool = Thread.current[:ruby_routes_params_pool] ||= []
|
180
|
+
thread_pool << params if thread_pool.size < 10 # Limit pool size
|
191
181
|
end
|
192
182
|
end
|
193
183
|
end
|
data/lib/ruby_routes/router.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
require_relative 'utility/route_utility'
|
2
|
+
|
1
3
|
module RubyRoutes
|
2
4
|
class Router
|
3
5
|
attr_reader :route_set
|
4
6
|
|
5
7
|
def initialize(&block)
|
6
8
|
@route_set = RouteSet.new
|
9
|
+
@route_utils = RubyRoutes::Utility::RouteUtility.new(@route_set)
|
7
10
|
@scope_stack = []
|
8
11
|
@concerns = {}
|
9
12
|
instance_eval(&block) if block_given?
|
@@ -102,7 +105,14 @@ module RubyRoutes
|
|
102
105
|
end
|
103
106
|
|
104
107
|
# Scope support
|
105
|
-
def scope(
|
108
|
+
def scope(options_or_path = {}, &block)
|
109
|
+
# Handle the case where the first argument is a string (path)
|
110
|
+
options = if options_or_path.is_a?(String)
|
111
|
+
{ path: options_or_path }
|
112
|
+
else
|
113
|
+
options_or_path
|
114
|
+
end
|
115
|
+
|
106
116
|
@scope_stack.push(options)
|
107
117
|
|
108
118
|
if block_given?
|
@@ -165,14 +175,9 @@ module RubyRoutes
|
|
165
175
|
|
166
176
|
private
|
167
177
|
|
168
|
-
def add_route(path, options
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
# Create and add the route
|
173
|
-
route = Route.new(scoped_options[:path], scoped_options)
|
174
|
-
@route_set.add_route(route)
|
175
|
-
route
|
178
|
+
def add_route(path, options={})
|
179
|
+
scoped = apply_scope(path, options)
|
180
|
+
@route_utils.define(scoped[:path], scoped)
|
176
181
|
end
|
177
182
|
|
178
183
|
def apply_scope(path, options)
|
@@ -4,6 +4,7 @@ module RubyRoutes
|
|
4
4
|
module UrlHelpers
|
5
5
|
def self.included(base)
|
6
6
|
base.extend(ClassMethods)
|
7
|
+
base.include(base.url_helpers)
|
7
8
|
end
|
8
9
|
|
9
10
|
module ClassMethods
|
@@ -49,11 +50,11 @@ module RubyRoutes
|
|
49
50
|
# HTML forms only support GET and POST
|
50
51
|
# For other methods, use POST with _method hidden field
|
51
52
|
form_method = (method == 'get') ? 'get' : 'post'
|
52
|
-
|
53
|
+
|
53
54
|
safe_path = CGI.escapeHTML(path.to_s)
|
54
55
|
safe_form_method = CGI.escapeHTML(form_method)
|
55
56
|
html = "<form action=\"#{safe_path}\" method=\"#{safe_form_method}\">"
|
56
|
-
|
57
|
+
|
57
58
|
# Add _method hidden field for non-GET/POST methods
|
58
59
|
if method != 'get' && method != 'post'
|
59
60
|
safe_method = CGI.escapeHTML(method)
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
module Utility
|
3
|
+
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
|
15
|
+
|
16
|
+
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
|
24
|
+
end
|
25
|
+
|
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
|
33
|
+
|
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
|
47
|
+
end
|
48
|
+
|
49
|
+
key
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
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
|
67
|
+
end
|
68
|
+
buf.dup
|
69
|
+
end
|
70
|
+
|
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)
|
74
|
+
end
|
75
|
+
|
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.
|
78
|
+
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
|
93
|
+
else
|
94
|
+
buf << v.to_s
|
95
|
+
end
|
96
|
+
i += 1
|
97
|
+
end
|
98
|
+
buf.dup
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
module Utility
|
3
|
+
module PathUtility
|
4
|
+
ROOT_PATH = '/'.freeze
|
5
|
+
|
6
|
+
def normalize_path(path)
|
7
|
+
path = path.to_s
|
8
|
+
# Add leading slash if missing
|
9
|
+
path = '/' + path unless path.start_with?('/')
|
10
|
+
# Remove trailing slash if present (unless root)
|
11
|
+
path = path[0..-2] if path.length > 1 && path.end_with?('/')
|
12
|
+
path
|
13
|
+
end
|
14
|
+
|
15
|
+
def split_path(path)
|
16
|
+
# Remove query string before splitting
|
17
|
+
path = path.split('?', 2).first
|
18
|
+
path = path[1..-1] if path.start_with?('/')
|
19
|
+
path = path[0...-1] if path.end_with?('/') && path != ROOT_PATH
|
20
|
+
path.empty? ? [] : path.split('/')
|
21
|
+
end
|
22
|
+
|
23
|
+
def join_path_parts(parts)
|
24
|
+
# Pre-calculate the size to avoid buffer resizing
|
25
|
+
size = parts.sum { |p| p.length + 1 } # +1 for slash
|
26
|
+
|
27
|
+
# Use string buffer for better performance
|
28
|
+
result = String.new(capacity: size)
|
29
|
+
result << '/'
|
30
|
+
|
31
|
+
# Join with explicit concatenation rather than array join
|
32
|
+
last_idx = parts.size - 1
|
33
|
+
parts.each_with_index do |part, i|
|
34
|
+
result << part
|
35
|
+
result << '/' unless i == last_idx
|
36
|
+
end
|
37
|
+
|
38
|
+
result
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
module Utility
|
3
|
+
class RouteUtility
|
4
|
+
def initialize(route_set)
|
5
|
+
@route_set = route_set
|
6
|
+
end
|
7
|
+
|
8
|
+
# DSL wants to merge scope, RouteSet wants to add a pre‐built Route,
|
9
|
+
# so we offer two entry points:
|
10
|
+
def define(path, options = {})
|
11
|
+
route = Route.new(path, options)
|
12
|
+
register(route)
|
13
|
+
end
|
14
|
+
|
15
|
+
def register(route)
|
16
|
+
@route_set.add_to_collection(route)
|
17
|
+
route
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/ruby_routes/version.rb
CHANGED
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.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yosef Benny Widyokarsono
|
@@ -37,6 +37,20 @@ dependencies:
|
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '13.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rack
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.2'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.2'
|
40
54
|
- !ruby/object:Gem::Dependency
|
41
55
|
name: simplecov
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -78,6 +92,9 @@ files:
|
|
78
92
|
- lib/ruby_routes/segments/wildcard_segment.rb
|
79
93
|
- lib/ruby_routes/string_extensions.rb
|
80
94
|
- lib/ruby_routes/url_helpers.rb
|
95
|
+
- lib/ruby_routes/utility/key_builder_utility.rb
|
96
|
+
- lib/ruby_routes/utility/path_utility.rb
|
97
|
+
- lib/ruby_routes/utility/route_utility.rb
|
81
98
|
- lib/ruby_routes/version.rb
|
82
99
|
homepage: https://github.com/yosefbennywidyo/ruby_routes
|
83
100
|
licenses:
|