padrino-core 0.18.3 → 0.18.5
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/padrino-core/application/routing.rb +19 -8
- data/lib/padrino-core/path_router/compiler.rb +73 -7
- data/lib/padrino-core/path_router/matcher.rb +37 -3
- data/lib/padrino-core/path_router/route.rb +19 -1
- data/lib/padrino-core/path_router.rb +41 -6
- data/lib/padrino-core/version.rb +1 -1
- data/test/test_filters.rb +43 -0
- data/test/test_routing.rb +370 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be05253b26516488a094d26e9867de40bf05e08fef5d5522bd0cd0d17cd31c9b
|
|
4
|
+
data.tar.gz: 7c6cd5ead1fd9ffac74c8c2dad9d74d64156af2c9ae04bf33bbbc5a0e6a8d177
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62df53f97d43739feaf1a71c3b09f27f39f28972ad242f321a60f41327ec3fb2dc854b75f7b109fe02cb3389a1ee10e13d20f6e9a01ce424dd02ed06c5309581
|
|
7
|
+
data.tar.gz: fcac6597b21ff08a3c7c20d35ddfeaeb1b7e9ac5dbfd5e83ab9bd0a9d1c705f3c3474da8daaa12d6519c9e9c1227bbcf66057f801719748db3e74fa859ce3e1b
|
|
@@ -38,6 +38,7 @@ module Padrino
|
|
|
38
38
|
|
|
39
39
|
def initialize(value, options = {})
|
|
40
40
|
super(value.to_s)
|
|
41
|
+
options = options.dup
|
|
41
42
|
@map = options.delete(:map)
|
|
42
43
|
@optional = options.delete(:optional)
|
|
43
44
|
@options = options
|
|
@@ -339,10 +340,10 @@ module Padrino
|
|
|
339
340
|
# @see http://padrinorb.com/guides/controllers/route-filters/
|
|
340
341
|
#
|
|
341
342
|
def construct_filter(*args, &block)
|
|
342
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
343
|
+
options = args.last.is_a?(Hash) ? args.pop.dup : {}
|
|
343
344
|
if (except = options.delete(:except))
|
|
344
345
|
raise 'You cannot use :except with other options specified' unless args.empty? && options.empty?
|
|
345
|
-
except = Array(except)
|
|
346
|
+
except = Array(except).dup
|
|
346
347
|
options = except.last.is_a?(Hash) ? except.pop : {}
|
|
347
348
|
end
|
|
348
349
|
Filter.new(!except, @_controller, options, Array(except || args), &block)
|
|
@@ -376,8 +377,7 @@ module Padrino
|
|
|
376
377
|
return super() unless name
|
|
377
378
|
defaults = { optional: false, map: name.to_s }
|
|
378
379
|
options = defaults.merge(options)
|
|
379
|
-
@_parent = Array(@_parent)
|
|
380
|
-
@_parent << Parent.new(name, options)
|
|
380
|
+
@_parent = Array(@_parent) + [Parent.new(name, options)]
|
|
381
381
|
end
|
|
382
382
|
|
|
383
383
|
##
|
|
@@ -462,7 +462,7 @@ module Padrino
|
|
|
462
462
|
# url(:index, fragment: 'comments')
|
|
463
463
|
#
|
|
464
464
|
def url(*args)
|
|
465
|
-
params = args.last.is_a?(Hash) ? args.pop : {}
|
|
465
|
+
params = args.last.is_a?(Hash) ? args.pop.dup : {}
|
|
466
466
|
fragment = params.delete(:fragment) || params.delete(:anchor)
|
|
467
467
|
path = make_path_with_params(args, value_to_param(params))
|
|
468
468
|
rebase_url(fragment ? "#{path}##{fragment}" : path)
|
|
@@ -497,8 +497,8 @@ module Padrino
|
|
|
497
497
|
def rebase_url(url)
|
|
498
498
|
if url.start_with?('/')
|
|
499
499
|
new_url = String.new
|
|
500
|
-
new_url <<
|
|
501
|
-
new_url <<
|
|
500
|
+
new_url << conformed_uri(:@_conformed_base_uri, ENV['RACK_BASE_URI']) if ENV['RACK_BASE_URI']
|
|
501
|
+
new_url << conformed_uri(:@_conformed_uri_root, uri_root) if defined?(uri_root)
|
|
502
502
|
new_url << url
|
|
503
503
|
else
|
|
504
504
|
url.empty? ? '/' : url
|
|
@@ -542,7 +542,7 @@ module Padrino
|
|
|
542
542
|
# Saves controller options, yields the block, restores controller options.
|
|
543
543
|
def with_new_options(*args)
|
|
544
544
|
original = {}
|
|
545
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
545
|
+
options = args.last.is_a?(Hash) ? args.pop.dup : {}
|
|
546
546
|
|
|
547
547
|
CONTROLLER_OPTIONS.each { |key| replace_instance_variable(original, "@_#{key}", options.delete(key)) }
|
|
548
548
|
replace_instance_variable(original, :@_controller, args)
|
|
@@ -594,6 +594,17 @@ module Padrino
|
|
|
594
594
|
uri_string.gsub(%r{^(?!/)(.*)}, '/\1').gsub(%r{/+$}, '')
|
|
595
595
|
end
|
|
596
596
|
|
|
597
|
+
# conform_uri, remembered in +ivar+ for the last value it was given (the base URI and
|
|
598
|
+
# uri_root rarely change). The memo is a frozen [value, result] pair, replaced whole.
|
|
599
|
+
def conformed_uri(ivar, uri_string)
|
|
600
|
+
memo = instance_variable_get(ivar)
|
|
601
|
+
return memo[1] if memo && memo[0] == uri_string
|
|
602
|
+
|
|
603
|
+
memo = [uri_string.dup.freeze, conform_uri(uri_string).freeze].freeze
|
|
604
|
+
instance_variable_set(ivar, memo)
|
|
605
|
+
memo[1]
|
|
606
|
+
end
|
|
607
|
+
|
|
597
608
|
##
|
|
598
609
|
# Rewrite default routes.
|
|
599
610
|
#
|
|
@@ -22,6 +22,7 @@ module Padrino
|
|
|
22
22
|
route.index = index
|
|
23
23
|
route.regexp = /\A(?<_#{index}>#{route.matcher.to_regexp})\z/
|
|
24
24
|
end
|
|
25
|
+
index_by_prefix!
|
|
25
26
|
@compiled = true
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -46,11 +47,15 @@ module Padrino
|
|
|
46
47
|
# Calls routes by using request.
|
|
47
48
|
#
|
|
48
49
|
def call_by_request(request)
|
|
50
|
+
path_info = pattern = nil
|
|
49
51
|
rotation do |offset|
|
|
50
|
-
|
|
52
|
+
# A route's filters may change the path before passing; decode it again only then.
|
|
53
|
+
unless path_info && path_info == request.path_info
|
|
54
|
+
path_info = request.path_info.dup
|
|
55
|
+
pattern = decode_pattern(path_info)
|
|
56
|
+
end
|
|
51
57
|
if (route = match?(offset, pattern))
|
|
52
|
-
|
|
53
|
-
yield(route, params) if route.verb == request.request_method
|
|
58
|
+
yield(route, route.params_for(pattern, request.params)) if route.verb == request.request_method
|
|
54
59
|
route
|
|
55
60
|
end
|
|
56
61
|
end
|
|
@@ -72,9 +77,64 @@ module Padrino
|
|
|
72
77
|
def match?(offset, path)
|
|
73
78
|
return unless path
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
trimmed = path[0..-2] if path.end_with?('/')
|
|
81
|
+
routes = @routes
|
|
82
|
+
# The index holds positions in routes as they were compiled, so it is used only while
|
|
83
|
+
# routes still holds the same routes in the same order. After any other edit to routes,
|
|
84
|
+
# match? looks through them one by one.
|
|
85
|
+
if (prefix_index = @prefix_index) && path.length >= PREFIX_SIZE && routes == prefix_index[2]
|
|
86
|
+
by_prefix, unprefixed, = prefix_index
|
|
87
|
+
candidates = by_prefix.fetch(path[0, PREFIX_SIZE], unprefixed)
|
|
88
|
+
from = candidates.bsearch_index { |index| index >= offset }
|
|
89
|
+
return unless from
|
|
90
|
+
|
|
91
|
+
from.upto(candidates.size - 1) do |at|
|
|
92
|
+
route = routes[candidates[at]]
|
|
93
|
+
regexp = route.regexp
|
|
94
|
+
return route if regexp.match?(path) || (trimmed && regexp.match?(trimmed))
|
|
95
|
+
end
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
while (route = routes[offset])
|
|
99
|
+
regexp = route.regexp
|
|
100
|
+
# A route added after compiling has no regexp and matches nothing.
|
|
101
|
+
return route if regexp && (regexp.match?(path) || (trimmed && regexp.match?(trimmed)))
|
|
102
|
+
offset += 1
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# How many leading characters of a path pick the routes to try (index_by_prefix!).
|
|
107
|
+
PREFIX_SIZE = 3
|
|
108
|
+
# The start of a Sinatra pattern that can only match itself: plain characters, up to the
|
|
109
|
+
# first one that may be special (':', '*', '(', '?', '{', '\\', '+' ...).
|
|
110
|
+
LITERAL_START = %r{\A[A-Za-z0-9_\-/.~]*}
|
|
111
|
+
|
|
112
|
+
##
|
|
113
|
+
# Sorts the routes by the first PREFIX_SIZE characters a path they match must start with,
|
|
114
|
+
# so that match? only tries the routes that can match it. A route whose pattern doesn't
|
|
115
|
+
# fix that many (a Regexp, a capture or group early, an optional character, a '|') is tried
|
|
116
|
+
# for every path.
|
|
117
|
+
#
|
|
118
|
+
def index_by_prefix!
|
|
119
|
+
prefixed = Hash.new { |hash, key| hash[key] = [] }
|
|
120
|
+
unprefixed = []
|
|
121
|
+
@routes.each_with_index do |route, index|
|
|
122
|
+
(prefix = literal_prefix(route)) ? prefixed[prefix] << index : unprefixed << index
|
|
77
123
|
end
|
|
124
|
+
by_prefix = prefixed.transform_values { |indexes| (indexes + unprefixed).sort.freeze }.freeze
|
|
125
|
+
# One frozen value, set in one assignment: match? reads it once, so a thread matching while
|
|
126
|
+
# another compiles the same engine sees the lists and routes from the same compile.
|
|
127
|
+
@prefix_index = [by_prefix, unprefixed.freeze, @routes.dup.freeze].freeze
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The first PREFIX_SIZE characters every path the route matches starts with, or nil.
|
|
131
|
+
def literal_prefix(route)
|
|
132
|
+
path = route.original_path
|
|
133
|
+
return unless path.is_a?(String) && !path.include?('|')
|
|
134
|
+
|
|
135
|
+
literal = path[LITERAL_START]
|
|
136
|
+
literal = literal[0..-2] if path[literal.length] == '?'
|
|
137
|
+
literal[0, PREFIX_SIZE] if literal.length >= PREFIX_SIZE
|
|
78
138
|
end
|
|
79
139
|
|
|
80
140
|
##
|
|
@@ -82,11 +142,12 @@ module Padrino
|
|
|
82
142
|
#
|
|
83
143
|
def rotation(offset = 0)
|
|
84
144
|
compile! unless compiled?
|
|
85
|
-
|
|
86
|
-
|
|
145
|
+
candidacies = []
|
|
146
|
+
while (route = yield(offset))
|
|
87
147
|
candidacies << route
|
|
88
148
|
offset = route.index.next
|
|
89
149
|
end
|
|
150
|
+
candidacies
|
|
90
151
|
end
|
|
91
152
|
|
|
92
153
|
##
|
|
@@ -97,6 +158,11 @@ module Padrino
|
|
|
97
158
|
# (a '%0A' left by '%250A'), matches no route either.
|
|
98
159
|
#
|
|
99
160
|
def decode_pattern(pattern)
|
|
161
|
+
# Without an escape, decoding an ASCII path only changes its encoding.
|
|
162
|
+
if pattern.ascii_only? && !pattern.include?('%') && Encoding.default_external.ascii_compatible?
|
|
163
|
+
return pattern.include?("\n") ? nil : pattern.dup.force_encoding(Encoding::UTF_8)
|
|
164
|
+
end
|
|
165
|
+
|
|
100
166
|
pattern = pattern.encode(Encoding.default_external)
|
|
101
167
|
return if pattern.match?(/%(?!\h\h)/)
|
|
102
168
|
|
|
@@ -5,6 +5,8 @@ module Padrino
|
|
|
5
5
|
class Matcher
|
|
6
6
|
# To count group of regexp
|
|
7
7
|
GROUP_REGEXP = /\((?!\?:|\?!|\?<=|\?<!|\?=).+?\)/
|
|
8
|
+
# Most key lists a matcher remembers for expand_path.
|
|
9
|
+
MAX_EXACT_KEYS = 32
|
|
8
10
|
|
|
9
11
|
##
|
|
10
12
|
# Constructs an instance of PathRouter::Matcher.
|
|
@@ -45,9 +47,14 @@ module Padrino
|
|
|
45
47
|
def expand(params)
|
|
46
48
|
params = @default_values.merge(params) if @default_values.is_a?(Hash)
|
|
47
49
|
params, query = params.each_with_object([{}, {}]) do |(key, val), parts|
|
|
48
|
-
|
|
50
|
+
if names.include?(key.to_s)
|
|
51
|
+
# Mustermann escapes an Integer through a slow path; its digits need no escaping.
|
|
52
|
+
parts[0][key] = val.instance_of?(Integer) ? val.to_s : val
|
|
53
|
+
else
|
|
54
|
+
parts[1][key] = val
|
|
55
|
+
end
|
|
49
56
|
end
|
|
50
|
-
expanded_path =
|
|
57
|
+
expanded_path = expand_path(params)
|
|
51
58
|
expanded_path += "?#{Padrino::Utils.build_uri_query(query)}" unless query.empty?
|
|
52
59
|
expanded_path
|
|
53
60
|
end
|
|
@@ -106,7 +113,7 @@ module Padrino
|
|
|
106
113
|
# @see Regexp#names
|
|
107
114
|
#
|
|
108
115
|
def names
|
|
109
|
-
handler.names
|
|
116
|
+
@names ||= handler.names.dup.freeze
|
|
110
117
|
end
|
|
111
118
|
|
|
112
119
|
##
|
|
@@ -122,6 +129,33 @@ module Padrino
|
|
|
122
129
|
|
|
123
130
|
private
|
|
124
131
|
|
|
132
|
+
##
|
|
133
|
+
# Mustermann's expand(:append, params). When the params fill one of the
|
|
134
|
+
# pattern's forms exactly, :append has nothing to append, and expand(:raise)
|
|
135
|
+
# gives the same path faster. Whether they do depends only on the keys,
|
|
136
|
+
# so it is found out once per list of keys, for params whose values are
|
|
137
|
+
# all Strings or Symbols (Integers arrive as Strings). Any other value
|
|
138
|
+
# (nil, an Array, an object) goes through :append and leaves the memo alone.
|
|
139
|
+
#
|
|
140
|
+
def expand_path(params)
|
|
141
|
+
return handler.expand(:append, params) unless params.all? { |_, value| value.instance_of?(String) || value.instance_of?(Symbol) }
|
|
142
|
+
|
|
143
|
+
keys = params.keys
|
|
144
|
+
exact = (@exact_keys ||= {}.freeze)[keys]
|
|
145
|
+
return handler.expand(:raise, params) if exact
|
|
146
|
+
|
|
147
|
+
path = handler.expand(:append, params)
|
|
148
|
+
if exact.nil? && @exact_keys.size < MAX_EXACT_KEYS
|
|
149
|
+
exact = begin
|
|
150
|
+
handler.expand(:raise, params) == path
|
|
151
|
+
rescue Mustermann::ExpandError, ArgumentError
|
|
152
|
+
false
|
|
153
|
+
end
|
|
154
|
+
@exact_keys = @exact_keys.merge(keys.freeze => exact).freeze
|
|
155
|
+
end
|
|
156
|
+
path
|
|
157
|
+
end
|
|
158
|
+
|
|
125
159
|
##
|
|
126
160
|
# Creates a hash with indifferent access.
|
|
127
161
|
#
|
|
@@ -4,7 +4,17 @@ module Padrino
|
|
|
4
4
|
##
|
|
5
5
|
# The accessors are useful to access from PathRouter::Router
|
|
6
6
|
#
|
|
7
|
-
attr_accessor :
|
|
7
|
+
attr_accessor :capture, :order, :options, :index, :regexp
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# The route's name (url generation finds routes by it).
|
|
11
|
+
#
|
|
12
|
+
attr_reader :name
|
|
13
|
+
|
|
14
|
+
def name=(name)
|
|
15
|
+
@name = name
|
|
16
|
+
@router&.named_routes_changed!
|
|
17
|
+
end
|
|
8
18
|
|
|
9
19
|
##
|
|
10
20
|
# A reader for compile option
|
|
@@ -16,6 +26,14 @@ module Padrino
|
|
|
16
26
|
#
|
|
17
27
|
attr_writer :router
|
|
18
28
|
|
|
29
|
+
##
|
|
30
|
+
# Sets the route's router unless it has one (a route pushed onto Router#routes directly).
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
33
|
+
def adopt(router)
|
|
34
|
+
@router ||= router
|
|
35
|
+
end
|
|
36
|
+
|
|
19
37
|
##
|
|
20
38
|
# The accessors will be used in other classes
|
|
21
39
|
#
|
|
@@ -32,6 +32,7 @@ module Padrino
|
|
|
32
32
|
route = Route.new(path, verb, options, &block)
|
|
33
33
|
route.router = self
|
|
34
34
|
@routes << route
|
|
35
|
+
@named_routes = nil
|
|
35
36
|
route
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -56,13 +57,17 @@ module Padrino
|
|
|
56
57
|
#
|
|
57
58
|
def path(name, *args)
|
|
58
59
|
params = args.last.is_a?(Hash) ? args.pop : {}
|
|
59
|
-
candidates =
|
|
60
|
-
raise InvalidRouteException
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
candidates = named_routes[name]
|
|
61
|
+
raise InvalidRouteException unless candidates
|
|
62
|
+
route = candidates.first
|
|
63
|
+
# Candidates that all take the same params (a GET and its HEAD) rank in their order.
|
|
64
|
+
unless candidates.all? { |candidate| candidate.matcher.names == route.matcher.names }
|
|
65
|
+
keys = params.keys.map(&:to_s)
|
|
63
66
|
# Tries to find the route that matches more, but with fewer names, in stable order
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
route = candidates.each_with_index.min_by do |candidate, i|
|
|
68
|
+
[(keys - candidate.matcher.names).length, candidate.matcher.names.size, i]
|
|
69
|
+
end.first
|
|
70
|
+
end
|
|
66
71
|
matcher = route.matcher
|
|
67
72
|
params_for_expand = params.dup
|
|
68
73
|
if !args.empty? && matcher.mustermann?
|
|
@@ -86,6 +91,7 @@ module Padrino
|
|
|
86
91
|
# Resets all routes, current order and preparation.
|
|
87
92
|
#
|
|
88
93
|
def reset!
|
|
94
|
+
@named_routes = nil
|
|
89
95
|
@routes = []
|
|
90
96
|
@current_order = 0
|
|
91
97
|
@prepared = nil
|
|
@@ -107,10 +113,39 @@ module Padrino
|
|
|
107
113
|
@prepared = true
|
|
108
114
|
return if @current_order.zero?
|
|
109
115
|
@routes.sort_by!(&:order)
|
|
116
|
+
@named_routes = nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
##
|
|
120
|
+
# Forgets the routes by name, after a route was added, sorted or renamed.
|
|
121
|
+
#
|
|
122
|
+
# @api private
|
|
123
|
+
def named_routes_changed!
|
|
124
|
+
@named_routes = nil
|
|
110
125
|
end
|
|
111
126
|
|
|
112
127
|
private
|
|
113
128
|
|
|
129
|
+
##
|
|
130
|
+
# The routes by name, in the order of +routes+, rebuilt when +routes+ changes size so
|
|
131
|
+
# that a route added to or deleted from +routes+ directly is seen.
|
|
132
|
+
# ceiling: a direct edit that keeps the size (one route swapped for another) isn't seen,
|
|
133
|
+
# nor is a rename of a route this router shares with, or took from, another router;
|
|
134
|
+
# rebuild on every call if that has to work.
|
|
135
|
+
#
|
|
136
|
+
def named_routes
|
|
137
|
+
size = @routes.size
|
|
138
|
+
memo = @named_routes
|
|
139
|
+
return memo[1] if memo && memo[0] == size
|
|
140
|
+
|
|
141
|
+
# A route pushed onto +routes+ directly has no router; give it this one, so that
|
|
142
|
+
# renaming it clears this index.
|
|
143
|
+
@routes.each { |route| route.adopt(self) }
|
|
144
|
+
by_name = @routes.group_by(&:name).freeze
|
|
145
|
+
@named_routes = [size, by_name].freeze
|
|
146
|
+
by_name
|
|
147
|
+
end
|
|
148
|
+
|
|
114
149
|
##
|
|
115
150
|
# Returns true if the router has been prepared.
|
|
116
151
|
#
|
data/lib/padrino-core/version.rb
CHANGED
data/test/test_filters.rb
CHANGED
|
@@ -416,6 +416,49 @@ describe 'Filters' do
|
|
|
416
416
|
assert_equal 'before', body
|
|
417
417
|
end
|
|
418
418
|
|
|
419
|
+
it 'should apply one except options hash to each filter it is passed to' do
|
|
420
|
+
opts = { except: :index }
|
|
421
|
+
mock_app do
|
|
422
|
+
before(opts) { @first = 'first' }
|
|
423
|
+
before(opts) { @second = 'second' }
|
|
424
|
+
get(:index) { "#{@first}#{@second}" }
|
|
425
|
+
get(:main) { "#{@first}#{@second}" }
|
|
426
|
+
end
|
|
427
|
+
get '/'
|
|
428
|
+
assert_equal '', body
|
|
429
|
+
get '/main'
|
|
430
|
+
assert_equal 'firstsecond', body
|
|
431
|
+
assert_equal({ except: :index }, opts)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
it 'should apply one except list with options to each filter it is passed to' do
|
|
435
|
+
except = [:index, { agent: /IE/ }]
|
|
436
|
+
mock_app do
|
|
437
|
+
before(except: except) { @first = 'first' }
|
|
438
|
+
before(except: except) { @second = 'second' }
|
|
439
|
+
get(:index) { "#{@first}#{@second}" }
|
|
440
|
+
get(:main) { "#{@first}#{@second}" }
|
|
441
|
+
end
|
|
442
|
+
get '/main', {}, { 'HTTP_USER_AGENT' => 'IE' }
|
|
443
|
+
assert_equal '', body
|
|
444
|
+
get '/main'
|
|
445
|
+
assert_equal 'firstsecond', body
|
|
446
|
+
assert_equal [:index, { agent: /IE/ }], except
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
it 'should accept frozen except options' do
|
|
450
|
+
mock_app do
|
|
451
|
+
before({ except: :index }.freeze) { @first = 'first' }
|
|
452
|
+
before(except: [:index, { agent: /IE/ }.freeze].freeze) { @second = 'second' }
|
|
453
|
+
get(:index) { "#{@first}#{@second}" }
|
|
454
|
+
get(:main) { "#{@first}#{@second}" }
|
|
455
|
+
end
|
|
456
|
+
get '/'
|
|
457
|
+
assert_equal '', body
|
|
458
|
+
get '/main'
|
|
459
|
+
assert_equal 'firstsecond', body
|
|
460
|
+
end
|
|
461
|
+
|
|
419
462
|
it 'should be able to filter excluding based on a symbol when specify the multiple routes and use nested controller' do
|
|
420
463
|
mock_app do
|
|
421
464
|
controller :test, :nested do
|
data/test/test_routing.rb
CHANGED
|
@@ -434,6 +434,17 @@ describe 'Routing' do
|
|
|
434
434
|
end
|
|
435
435
|
end
|
|
436
436
|
|
|
437
|
+
it 'should give 404 for a path with a raw newline' do
|
|
438
|
+
mock_app do
|
|
439
|
+
get('/names/:name') { 'name' }
|
|
440
|
+
end
|
|
441
|
+
["/names/a\nb", "/names/a\n"].each do |path|
|
|
442
|
+
env = Rack::MockRequest.env_for('/')
|
|
443
|
+
env['PATH_INFO'] = path
|
|
444
|
+
assert_equal 404, @app.call(env)[0], path.inspect
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
437
448
|
it 'should cascade a path with a control character to the next app at the same mount' do
|
|
438
449
|
first = mock_app do
|
|
439
450
|
before('/admin') { halt 401, 'first' }
|
|
@@ -2440,6 +2451,89 @@ describe 'Routing' do
|
|
|
2440
2451
|
assert_equal 1, counter
|
|
2441
2452
|
end
|
|
2442
2453
|
|
|
2454
|
+
describe 'with options hashes and lists reused or frozen' do
|
|
2455
|
+
def mock_app_needing_ok(&block)
|
|
2456
|
+
mock_app do
|
|
2457
|
+
set(:needs_ok) { |on| condition { halt 403, 'no ok' unless params[:ok] } if on }
|
|
2458
|
+
instance_eval(&block)
|
|
2459
|
+
end
|
|
2460
|
+
end
|
|
2461
|
+
|
|
2462
|
+
it 'should apply the options to each controller and leave the hash unchanged' do
|
|
2463
|
+
opts = { conditions: { needs_ok: true }, map: '/area' }
|
|
2464
|
+
mock_app_needing_ok do
|
|
2465
|
+
controller(:a, opts) { get(:one) { 'one' } }
|
|
2466
|
+
controllers(:b, opts) { get(:two) { 'two' } }
|
|
2467
|
+
end
|
|
2468
|
+
%w[one two].each do |name|
|
|
2469
|
+
get "/area/#{name}"
|
|
2470
|
+
assert_equal 'no ok', body
|
|
2471
|
+
get "/area/#{name}", ok: 1
|
|
2472
|
+
assert_equal name, body
|
|
2473
|
+
end
|
|
2474
|
+
assert_equal({ conditions: { needs_ok: true }, map: '/area' }, opts)
|
|
2475
|
+
end
|
|
2476
|
+
|
|
2477
|
+
it 'should accept a frozen options hash' do
|
|
2478
|
+
opts = { conditions: { needs_ok: true } }.freeze
|
|
2479
|
+
mock_app_needing_ok do
|
|
2480
|
+
controller(:a, opts) { get(:index) { 'a' } }
|
|
2481
|
+
controller(:b, opts) { get(:index) { 'b' } }
|
|
2482
|
+
end
|
|
2483
|
+
%w[a b].each do |name|
|
|
2484
|
+
get "/#{name}"
|
|
2485
|
+
assert_equal 'no ok', body
|
|
2486
|
+
get "/#{name}", ok: 1
|
|
2487
|
+
assert_equal name, body
|
|
2488
|
+
end
|
|
2489
|
+
end
|
|
2490
|
+
|
|
2491
|
+
it 'should accept a frozen parent list and a parent declared inside the controller' do
|
|
2492
|
+
mock_app do
|
|
2493
|
+
controller(:product, parent: [:shop].freeze) do
|
|
2494
|
+
parent :category
|
|
2495
|
+
get(:show) { 'product' }
|
|
2496
|
+
end
|
|
2497
|
+
end
|
|
2498
|
+
get '/shop/1/category/2/product/show'
|
|
2499
|
+
assert_equal 'product', body
|
|
2500
|
+
end
|
|
2501
|
+
|
|
2502
|
+
it 'should leave the options given to a parent unchanged' do
|
|
2503
|
+
opts = { map: 'store', optional: true }.freeze
|
|
2504
|
+
first = Padrino::Routing::Parent.new(:a, opts)
|
|
2505
|
+
second = Padrino::Routing::Parent.new(:b, opts)
|
|
2506
|
+
assert_equal ['store', true], [first.map, first.optional]
|
|
2507
|
+
assert_equal ['store', true], [second.map, second.optional]
|
|
2508
|
+
end
|
|
2509
|
+
|
|
2510
|
+
it 'should keep the fragment of a params hash used for several urls' do
|
|
2511
|
+
mock_app { get(:show, with: :id) { 'show' } }
|
|
2512
|
+
params = { id: 1, fragment: 'top' }
|
|
2513
|
+
assert_equal '/show/1#top', @app.url(:show, params)
|
|
2514
|
+
assert_equal '/show/1#top', @app.url(:show, params)
|
|
2515
|
+
assert_equal '/show/1', @app.url(:show, { id: 1 }.freeze)
|
|
2516
|
+
end
|
|
2517
|
+
|
|
2518
|
+
it 'should not add a parent declared in a controller to the parents it was given' do
|
|
2519
|
+
parents = [:shop]
|
|
2520
|
+
mock_app do
|
|
2521
|
+
controller(:product, parent: parents) do
|
|
2522
|
+
parent :category
|
|
2523
|
+
get(:show) { 'product' }
|
|
2524
|
+
end
|
|
2525
|
+
controller(:item, parent: parents) { get(:show) { 'item' } }
|
|
2526
|
+
end
|
|
2527
|
+
get '/shop/1/category/2/product/show'
|
|
2528
|
+
assert_equal 'product', body
|
|
2529
|
+
get '/shop/1/item/show'
|
|
2530
|
+
assert_equal 'item', body
|
|
2531
|
+
get '/shop/1/category/2/item/show'
|
|
2532
|
+
assert_equal 404, status
|
|
2533
|
+
assert_equal [:shop], parents
|
|
2534
|
+
end
|
|
2535
|
+
end
|
|
2536
|
+
|
|
2443
2537
|
it 'should parse params with class level provides' do
|
|
2444
2538
|
mock_app do
|
|
2445
2539
|
controllers :posts, provides: %i[html js] do
|
|
@@ -3136,4 +3230,280 @@ describe 'Routing' do
|
|
|
3136
3230
|
assert_equal 'GET /posts/:id', outer_env['sinatra.route']
|
|
3137
3231
|
end
|
|
3138
3232
|
end
|
|
3233
|
+
|
|
3234
|
+
describe 'route matching' do
|
|
3235
|
+
it 'matches the path a route rewrote before passing' do
|
|
3236
|
+
mock_app do
|
|
3237
|
+
get(:rewrite, map: '/rewrite/:id') do
|
|
3238
|
+
request.path_info = '/target'
|
|
3239
|
+
pass
|
|
3240
|
+
end
|
|
3241
|
+
get(:in_place, map: '/in_place/:id') do
|
|
3242
|
+
request.path_info.replace('/target')
|
|
3243
|
+
pass
|
|
3244
|
+
end
|
|
3245
|
+
get(:target, map: '/target') { 'target' }
|
|
3246
|
+
end
|
|
3247
|
+
get '/rewrite/1'
|
|
3248
|
+
assert_equal 200, status
|
|
3249
|
+
assert_equal 'target', body
|
|
3250
|
+
get '/in_place/1'
|
|
3251
|
+
assert_equal 200, status
|
|
3252
|
+
assert_equal 'target', body
|
|
3253
|
+
end
|
|
3254
|
+
|
|
3255
|
+
it 'keeps serving its routes when a route is added after a request' do
|
|
3256
|
+
mock_app do
|
|
3257
|
+
get('/first') { 'first' }
|
|
3258
|
+
end
|
|
3259
|
+
get '/first'
|
|
3260
|
+
@app.get('/second') { 'second' }
|
|
3261
|
+
get '/first'
|
|
3262
|
+
assert_equal 'first', body
|
|
3263
|
+
get '/m'
|
|
3264
|
+
assert_equal 404, status
|
|
3265
|
+
get '/missing'
|
|
3266
|
+
assert_equal 404, status
|
|
3267
|
+
end
|
|
3268
|
+
|
|
3269
|
+
it 'keeps serving its routes when routes are removed or inserted after a request' do
|
|
3270
|
+
mock_app do
|
|
3271
|
+
get(:gone, map: '/gone') { 'gone' }
|
|
3272
|
+
get(:abc, map: '/abc') { 'abc' }
|
|
3273
|
+
get(:cap, map: '/:x') { 'cap' }
|
|
3274
|
+
end
|
|
3275
|
+
get '/abc'
|
|
3276
|
+
@app.router.routes.reject! { |route| route.name == :gone }
|
|
3277
|
+
get '/abc'
|
|
3278
|
+
assert_equal 'abc', body
|
|
3279
|
+
get '/zz'
|
|
3280
|
+
assert_equal 'cap', body
|
|
3281
|
+
@app.router.routes.unshift(Padrino::PathRouter::Route.new('/new', :get))
|
|
3282
|
+
get '/abc'
|
|
3283
|
+
assert_equal 'abc', body
|
|
3284
|
+
get '/gone'
|
|
3285
|
+
assert_equal 'cap', body
|
|
3286
|
+
end
|
|
3287
|
+
|
|
3288
|
+
it 'keeps serving its routes when routes are reordered after a request' do
|
|
3289
|
+
mock_app do
|
|
3290
|
+
get('/abc') { 'abc' }
|
|
3291
|
+
get('/abd') { 'abd' }
|
|
3292
|
+
get('/zzz') { 'zzz' }
|
|
3293
|
+
end
|
|
3294
|
+
get '/abc'
|
|
3295
|
+
routes = @app.router.routes
|
|
3296
|
+
first = routes.index { |route| route.original_path == '/abc' && route.verb == 'GET' }
|
|
3297
|
+
last = routes.rindex { |route| route.original_path == '/zzz' && route.verb == 'GET' }
|
|
3298
|
+
routes[first], routes[last] = routes[last], routes[first]
|
|
3299
|
+
get '/abc'
|
|
3300
|
+
assert_equal 200, status
|
|
3301
|
+
assert_equal 'abc', body
|
|
3302
|
+
get '/zzz'
|
|
3303
|
+
assert_equal 'zzz', body
|
|
3304
|
+
end
|
|
3305
|
+
|
|
3306
|
+
it 'matches routes whose pattern starts with an optional or variable part' do
|
|
3307
|
+
mock_app do
|
|
3308
|
+
get('/colou?r') { 'color' }
|
|
3309
|
+
get('/xy?z') { 'optional' }
|
|
3310
|
+
get('/one|/two') { 'union' }
|
|
3311
|
+
get('/:lang/start') { 'capture' }
|
|
3312
|
+
get('/*/files') { 'splat' }
|
|
3313
|
+
get('/ab') { 'short' }
|
|
3314
|
+
get(%r{/rx[0-9]+}) { 'regexp' }
|
|
3315
|
+
end
|
|
3316
|
+
{ '/color' => 'color', '/colour' => 'color', '/xz' => 'optional', '/xyz' => 'optional', '/two' => 'union', '/de/start' => 'capture',
|
|
3317
|
+
'/a/b/files' => 'splat', '/ab' => 'short', '/ab/' => 'short', '/rx12' => 'regexp' }.each do |path, expected|
|
|
3318
|
+
get path
|
|
3319
|
+
assert_equal expected, body, path
|
|
3320
|
+
end
|
|
3321
|
+
end
|
|
3322
|
+
|
|
3323
|
+
it 'picks the routes to try by the decoded path' do
|
|
3324
|
+
mock_app do
|
|
3325
|
+
get('/abc/first') { 'first' }
|
|
3326
|
+
get('/ABC/x') { 'upper' }
|
|
3327
|
+
get('/a/') { 'slash' }
|
|
3328
|
+
end
|
|
3329
|
+
get '/%61bc/first'
|
|
3330
|
+
assert_equal 200, status
|
|
3331
|
+
assert_equal 'first', body
|
|
3332
|
+
get '/ABC/x'
|
|
3333
|
+
assert_equal 200, status
|
|
3334
|
+
assert_equal 'upper', body
|
|
3335
|
+
get '/a/'
|
|
3336
|
+
assert_equal 200, status
|
|
3337
|
+
assert_equal 'slash', body
|
|
3338
|
+
end
|
|
3339
|
+
|
|
3340
|
+
it 'lets a StopIteration raised in a route reach the error handlers' do
|
|
3341
|
+
mock_app do
|
|
3342
|
+
set :raise_errors, false
|
|
3343
|
+
set :show_exceptions, false
|
|
3344
|
+
get('/boom') { [].each.next }
|
|
3345
|
+
error(StopIteration) { 'handled' }
|
|
3346
|
+
end
|
|
3347
|
+
get '/boom'
|
|
3348
|
+
assert_equal 500, status
|
|
3349
|
+
assert_equal 'handled', body
|
|
3350
|
+
end
|
|
3351
|
+
|
|
3352
|
+
it 'serves a route when an earlier route for another verb matches the path' do
|
|
3353
|
+
mock_app do
|
|
3354
|
+
set :raise_errors, false
|
|
3355
|
+
set :show_exceptions, false
|
|
3356
|
+
post('(/:lang)?') { 'post' }
|
|
3357
|
+
get('/') { 'root' }
|
|
3358
|
+
end
|
|
3359
|
+
get '/'
|
|
3360
|
+
assert_equal 200, status
|
|
3361
|
+
assert_equal 'root', body
|
|
3362
|
+
end
|
|
3363
|
+
|
|
3364
|
+
it 'tries routes in the order they were defined, whatever their pattern starts with' do
|
|
3365
|
+
mock_app do
|
|
3366
|
+
get('/abc/first') { 'literal' }
|
|
3367
|
+
get('/:name/first') { 'capture' }
|
|
3368
|
+
get('/:name/second') { 'capture' }
|
|
3369
|
+
get('/abc/second') { 'literal' }
|
|
3370
|
+
end
|
|
3371
|
+
get '/abc/first'
|
|
3372
|
+
assert_equal 'literal', body
|
|
3373
|
+
get '/abc/second'
|
|
3374
|
+
assert_equal 'capture', body
|
|
3375
|
+
end
|
|
3376
|
+
end
|
|
3377
|
+
|
|
3378
|
+
describe 'url generation after routes change' do
|
|
3379
|
+
it 'finds a renamed route by its new name' do
|
|
3380
|
+
mock_app do
|
|
3381
|
+
get(:old, map: '/page') { 'page' }
|
|
3382
|
+
end
|
|
3383
|
+
assert_equal '/page', @app.url(:old)
|
|
3384
|
+
@app.routes.each { |route| route.name = :new if route.name == :old }
|
|
3385
|
+
assert_equal '/page', @app.url(:new)
|
|
3386
|
+
assert_raises(Padrino::Routing::UnrecognizedException) { @app.url(:old) }
|
|
3387
|
+
end
|
|
3388
|
+
|
|
3389
|
+
it 'uses the routes defined after reset_routes!' do
|
|
3390
|
+
mock_app do
|
|
3391
|
+
get(:page, map: '/old') { 'old' }
|
|
3392
|
+
end
|
|
3393
|
+
assert_equal '/old', @app.url(:page)
|
|
3394
|
+
@app.reset_routes!
|
|
3395
|
+
assert_raises(Padrino::Routing::UnrecognizedException) { @app.url(:page) }
|
|
3396
|
+
@app.get(:page, map: '/new') { 'new' }
|
|
3397
|
+
assert_equal '/new', @app.url(:page)
|
|
3398
|
+
end
|
|
3399
|
+
|
|
3400
|
+
it 'finds no route deleted from the routes' do
|
|
3401
|
+
mock_app do
|
|
3402
|
+
get(:gone, map: '/gone') { 'gone' }
|
|
3403
|
+
end
|
|
3404
|
+
assert_equal '/gone', @app.url(:gone)
|
|
3405
|
+
@app.router.routes.reject! { |route| route.name == :gone }
|
|
3406
|
+
assert_raises(Padrino::Routing::UnrecognizedException) { @app.url(:gone) }
|
|
3407
|
+
end
|
|
3408
|
+
|
|
3409
|
+
it 'finds a route pushed onto the routes by its new name after a rename' do
|
|
3410
|
+
mock_app do
|
|
3411
|
+
get(:index) { 'index' }
|
|
3412
|
+
end
|
|
3413
|
+
route = Padrino::PathRouter::Route.new('/page', :get, name: :old)
|
|
3414
|
+
@app.router.routes << route
|
|
3415
|
+
assert_equal '/page', @app.url(:old)
|
|
3416
|
+
route.name = :new
|
|
3417
|
+
assert_equal '/page', @app.url(:new)
|
|
3418
|
+
assert_raises(Padrino::Routing::UnrecognizedException) { @app.url(:old) }
|
|
3419
|
+
end
|
|
3420
|
+
|
|
3421
|
+
it 'keeps the router a route was added to when another router indexes it' do
|
|
3422
|
+
owner = Padrino::PathRouter.new
|
|
3423
|
+
route = owner.add(:get, '/shared', name: :old)
|
|
3424
|
+
owner.path(:old)
|
|
3425
|
+
other = Padrino::PathRouter.new
|
|
3426
|
+
other.routes << route
|
|
3427
|
+
other.path(:old)
|
|
3428
|
+
route.name = :new
|
|
3429
|
+
assert_equal '/shared', owner.path(:new)
|
|
3430
|
+
end
|
|
3431
|
+
|
|
3432
|
+
it 'finds a route added to the router with a name' do
|
|
3433
|
+
router = Padrino::PathRouter.new
|
|
3434
|
+
router.add(:get, '/one', name: :one)
|
|
3435
|
+
assert_equal '/one', router.path(:one)
|
|
3436
|
+
router.add(:get, '/two', name: :two)
|
|
3437
|
+
assert_equal '/two', router.path(:two)
|
|
3438
|
+
router.routes.pop
|
|
3439
|
+
router.add(:get, '/three', name: :three)
|
|
3440
|
+
assert_equal '/three', router.path(:three)
|
|
3441
|
+
end
|
|
3442
|
+
|
|
3443
|
+
it 'finds the routes added to the router after reset!' do
|
|
3444
|
+
router = Padrino::PathRouter.new
|
|
3445
|
+
router.add(:get, '/old', name: :page)
|
|
3446
|
+
assert_equal '/old', router.path(:page)
|
|
3447
|
+
router.reset!
|
|
3448
|
+
router.routes << Padrino::PathRouter::Route.new('/new', :get, name: :page)
|
|
3449
|
+
assert_equal '/new', router.path(:page)
|
|
3450
|
+
end
|
|
3451
|
+
|
|
3452
|
+
it 'picks the first route of a name in the order requests try them' do
|
|
3453
|
+
mock_app do
|
|
3454
|
+
get(:page, map: '/low', priority: :low) { 'low' }
|
|
3455
|
+
get(:page, map: '/normal') { 'normal' }
|
|
3456
|
+
end
|
|
3457
|
+
@app.url(:page)
|
|
3458
|
+
get '/normal'
|
|
3459
|
+
assert_equal '/normal', @app.url(:page)
|
|
3460
|
+
end
|
|
3461
|
+
|
|
3462
|
+
it 'picks the route of a name that takes the params given' do
|
|
3463
|
+
mock_app do
|
|
3464
|
+
get(:show, map: '/show/:id') { 'id' }
|
|
3465
|
+
get(:show, map: '/show/:id/:slug') { 'slug' }
|
|
3466
|
+
end
|
|
3467
|
+
2.times do
|
|
3468
|
+
assert_equal '/show/1', @app.url(:show, id: 1)
|
|
3469
|
+
assert_equal '/show/1/x', @app.url(:show, id: 1, slug: 'x')
|
|
3470
|
+
assert_equal '/show/1?ref=a', @app.url(:show, id: 1, ref: 'a')
|
|
3471
|
+
end
|
|
3472
|
+
end
|
|
3473
|
+
|
|
3474
|
+
it 'expands the same url for each form of the params' do
|
|
3475
|
+
mock_app do
|
|
3476
|
+
get(:item, map: '/item/:id(.:format)?') { 'item' }
|
|
3477
|
+
get(:opt, map: '/opt(/:a(/:b))?') { 'opt' }
|
|
3478
|
+
end
|
|
3479
|
+
2.times do
|
|
3480
|
+
assert_equal '/item/7', @app.url(:item, id: 7)
|
|
3481
|
+
assert_equal '/item/7', @app.url(:item, 'id' => 7)
|
|
3482
|
+
assert_equal '/item/-7', @app.url(:item, id: -7)
|
|
3483
|
+
assert_equal '/item/7.json', @app.url(:item, id: '7', format: :json)
|
|
3484
|
+
assert_equal '/item/a%20b', @app.url(:item, id: 'a b')
|
|
3485
|
+
assert_equal '/opt/1/2', @app.url(:opt, a: 1, b: 2)
|
|
3486
|
+
assert_equal '/opt?b=2', @app.url(:opt, b: 2)
|
|
3487
|
+
assert_equal '/opt?b=2', @app.url(:opt, a: nil, b: 2)
|
|
3488
|
+
assert_equal '/opt/1/2', @app.routes.find { |route| route.name == :opt }.path(a: 1, b: 2)
|
|
3489
|
+
assert_equal '/opt?b=2', @app.routes.find { |route| route.name == :opt }.path(a: nil, b: 2)
|
|
3490
|
+
end
|
|
3491
|
+
end
|
|
3492
|
+
|
|
3493
|
+
it 'expands values other than Strings, Symbols and Integers as before' do
|
|
3494
|
+
list = Class.new(Array)
|
|
3495
|
+
value = Class.new { def to_s; 'v1'; end }.new
|
|
3496
|
+
mock_app do
|
|
3497
|
+
get(:files, map: '/files/*') { 'files' }
|
|
3498
|
+
get(:opt, map: '/opt(/:a(/:b))?') { 'opt' }
|
|
3499
|
+
end
|
|
3500
|
+
2.times do
|
|
3501
|
+
assert_equal '/files/x', @app.url(:files, splat: 'x')
|
|
3502
|
+
assert_equal '/files/x', @app.url(:files, splat: list['x'])
|
|
3503
|
+
assert_equal '/opt/1/2', @app.url(:opt, a: 1, b: 2)
|
|
3504
|
+
assert_equal '/opt/v1/2', @app.url(:opt, a: value, b: 2)
|
|
3505
|
+
assert_equal '/opt/false/2', @app.url(:opt, a: false, b: 2)
|
|
3506
|
+
end
|
|
3507
|
+
end
|
|
3508
|
+
end
|
|
3139
3509
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: padrino-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.18.
|
|
4
|
+
version: 0.18.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Padrino Team
|
|
@@ -11,7 +11,7 @@ authors:
|
|
|
11
11
|
autorequire:
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date: 2026-11-
|
|
14
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: ostruct
|
|
@@ -33,14 +33,14 @@ dependencies:
|
|
|
33
33
|
requirements:
|
|
34
34
|
- - '='
|
|
35
35
|
- !ruby/object:Gem::Version
|
|
36
|
-
version: 0.18.
|
|
36
|
+
version: 0.18.5
|
|
37
37
|
type: :runtime
|
|
38
38
|
prerelease: false
|
|
39
39
|
version_requirements: !ruby/object:Gem::Requirement
|
|
40
40
|
requirements:
|
|
41
41
|
- - '='
|
|
42
42
|
- !ruby/object:Gem::Version
|
|
43
|
-
version: 0.18.
|
|
43
|
+
version: 0.18.5
|
|
44
44
|
- !ruby/object:Gem::Dependency
|
|
45
45
|
name: rackup
|
|
46
46
|
requirement: !ruby/object:Gem::Requirement
|