rack-mount 0.0.1 → 0.6.13
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.
- data/README.rdoc +12 -4
- data/lib/rack/mount/analysis/frequency.rb +15 -6
- data/lib/rack/mount/analysis/histogram.rb +55 -6
- data/lib/rack/mount/analysis/splitting.rb +85 -71
- data/lib/rack/mount/code_generation.rb +117 -0
- data/lib/rack/mount/generatable_regexp.rb +95 -48
- data/lib/rack/mount/multimap.rb +2 -43
- data/lib/rack/mount/prefix.rb +12 -7
- data/lib/rack/mount/regexp_with_named_groups.rb +27 -7
- data/lib/rack/mount/route.rb +79 -18
- data/lib/rack/mount/route_set.rb +330 -22
- data/lib/rack/mount/strexp/parser.rb +0 -0
- data/lib/rack/mount/strexp/parser.y +34 -0
- data/lib/rack/mount/strexp/tokenizer.rb +83 -0
- data/lib/rack/mount/strexp/tokenizer.rex +12 -0
- data/lib/rack/mount/strexp.rb +54 -79
- data/lib/rack/mount/utils.rb +65 -174
- data/lib/rack/mount/vendor/multimap/multimap.rb +142 -39
- data/lib/rack/mount/vendor/multimap/multiset.rb +33 -1
- data/lib/rack/mount/vendor/multimap/nested_multimap.rb +18 -16
- data/lib/rack/mount/vendor/regin/regin/alternation.rb +40 -0
- data/lib/rack/mount/vendor/regin/regin/anchor.rb +4 -0
- data/lib/rack/mount/vendor/regin/regin/atom.rb +54 -0
- data/lib/rack/mount/vendor/regin/regin/character.rb +51 -0
- data/lib/rack/mount/vendor/regin/regin/character_class.rb +50 -0
- data/lib/rack/mount/vendor/regin/regin/collection.rb +77 -0
- data/lib/rack/mount/vendor/regin/regin/expression.rb +126 -0
- data/lib/rack/mount/vendor/regin/regin/group.rb +85 -0
- data/lib/rack/mount/vendor/regin/regin/options.rb +55 -0
- data/lib/rack/mount/vendor/regin/regin/parser.rb +521 -0
- data/lib/rack/mount/vendor/regin/regin/tokenizer.rb +0 -0
- data/lib/rack/mount/vendor/regin/regin/version.rb +3 -0
- data/lib/rack/mount/vendor/regin/regin.rb +75 -0
- data/lib/rack/mount/version.rb +3 -0
- data/lib/rack/mount.rb +13 -16
- metadata +73 -29
- data/lib/rack/mount/const.rb +0 -45
- data/lib/rack/mount/exceptions.rb +0 -3
- data/lib/rack/mount/generation/route.rb +0 -57
- data/lib/rack/mount/generation/route_set.rb +0 -163
- data/lib/rack/mount/meta_method.rb +0 -104
- data/lib/rack/mount/mixover.rb +0 -47
- data/lib/rack/mount/recognition/code_generation.rb +0 -99
- data/lib/rack/mount/recognition/route.rb +0 -59
- data/lib/rack/mount/recognition/route_set.rb +0 -88
data/lib/rack/mount/utils.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
require '
|
|
1
|
+
begin
|
|
2
|
+
require 'regin'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__), 'vendor/regin'))
|
|
5
|
+
require 'regin'
|
|
6
|
+
end
|
|
7
|
+
|
|
3
8
|
require 'uri'
|
|
4
9
|
|
|
5
10
|
module Rack::Mount
|
|
@@ -9,6 +14,19 @@ module Rack::Mount
|
|
|
9
14
|
# more appropriate contexts.
|
|
10
15
|
#++
|
|
11
16
|
module Utils
|
|
17
|
+
def silence_debug
|
|
18
|
+
old_debug, $DEBUG = $DEBUG, nil
|
|
19
|
+
yield
|
|
20
|
+
ensure
|
|
21
|
+
$DEBUG = old_debug
|
|
22
|
+
end
|
|
23
|
+
module_function :silence_debug
|
|
24
|
+
|
|
25
|
+
def debug(msg)
|
|
26
|
+
warn "Rack::Mount #{msg}" if $DEBUG
|
|
27
|
+
end
|
|
28
|
+
module_function :debug
|
|
29
|
+
|
|
12
30
|
# Normalizes URI path.
|
|
13
31
|
#
|
|
14
32
|
# Strips off trailing slash and ensures there is a leading slash.
|
|
@@ -19,25 +37,26 @@ module Rack::Mount
|
|
|
19
37
|
# normalize_path("") # => "/"
|
|
20
38
|
def normalize_path(path)
|
|
21
39
|
path = "/#{path}"
|
|
22
|
-
path.squeeze!(
|
|
23
|
-
path.sub!(%r{/+\Z},
|
|
24
|
-
path =
|
|
40
|
+
path.squeeze!('/')
|
|
41
|
+
path.sub!(%r{/+\Z}, '')
|
|
42
|
+
path = '/' if path == ''
|
|
25
43
|
path
|
|
26
44
|
end
|
|
27
45
|
module_function :normalize_path
|
|
28
46
|
|
|
29
47
|
# Removes trailing nils from array.
|
|
30
48
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
# pop_trailing_blanks!([1, 2, 3]) # => [1, 2, 3]
|
|
50
|
+
# pop_trailing_blanks!([1, 2, 3, nil, ""]) # => [1, 2, 3]
|
|
51
|
+
# pop_trailing_blanks!([nil]) # => []
|
|
52
|
+
# pop_trailing_blanks!([""]) # => []
|
|
53
|
+
def pop_trailing_blanks!(ary)
|
|
54
|
+
while ary.length > 0 && (ary.last.nil? || ary.last == '')
|
|
36
55
|
ary.pop
|
|
37
56
|
end
|
|
38
57
|
ary
|
|
39
58
|
end
|
|
40
|
-
module_function :
|
|
59
|
+
module_function :pop_trailing_blanks!
|
|
41
60
|
|
|
42
61
|
RESERVED_PCHAR = ':@&=+$,;%'
|
|
43
62
|
SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
|
|
@@ -47,14 +66,16 @@ module Rack::Mount
|
|
|
47
66
|
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
|
|
48
67
|
end
|
|
49
68
|
|
|
69
|
+
Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
|
70
|
+
|
|
50
71
|
def escape_uri(uri)
|
|
51
|
-
|
|
72
|
+
Parser.escape(uri.to_s, UNSAFE_PCHAR)
|
|
52
73
|
end
|
|
53
74
|
module_function :escape_uri
|
|
54
75
|
|
|
55
76
|
if ''.respond_to?(:force_encoding)
|
|
56
77
|
def unescape_uri(uri)
|
|
57
|
-
|
|
78
|
+
Parser.unescape(uri).force_encoding('utf-8')
|
|
58
79
|
end
|
|
59
80
|
else
|
|
60
81
|
def unescape_uri(uri)
|
|
@@ -72,33 +93,17 @@ module Rack::Mount
|
|
|
72
93
|
}.join("&")
|
|
73
94
|
when Hash
|
|
74
95
|
value.map { |k, v|
|
|
75
|
-
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
|
96
|
+
build_nested_query(v, prefix ? "#{prefix}[#{Rack::Utils.escape(k)}]" : Rack::Utils.escape(k))
|
|
76
97
|
}.join("&")
|
|
77
98
|
when String
|
|
78
99
|
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
|
79
|
-
"#{
|
|
80
|
-
when NilClass
|
|
81
|
-
Rack::Utils.escape(prefix)
|
|
100
|
+
"#{prefix}=#{Rack::Utils.escape(value)}"
|
|
82
101
|
else
|
|
83
|
-
|
|
84
|
-
build_nested_query(value.to_param.to_s, prefix)
|
|
85
|
-
else
|
|
86
|
-
Rack::Utils.escape(prefix)
|
|
87
|
-
end
|
|
102
|
+
prefix
|
|
88
103
|
end
|
|
89
104
|
end
|
|
90
105
|
module_function :build_nested_query
|
|
91
106
|
|
|
92
|
-
def normalize_extended_expression(regexp)
|
|
93
|
-
return regexp unless regexp.options & Regexp::EXTENDED != 0
|
|
94
|
-
source = regexp.source
|
|
95
|
-
source.gsub!(/#.+$/, '')
|
|
96
|
-
source.gsub!(/\s+/, '')
|
|
97
|
-
source.gsub!(/\\\//, '/')
|
|
98
|
-
Regexp.compile(source)
|
|
99
|
-
end
|
|
100
|
-
module_function :normalize_extended_expression
|
|
101
|
-
|
|
102
107
|
# Determines whether the regexp must match the entire string.
|
|
103
108
|
#
|
|
104
109
|
# regexp_anchored?(/^foo$/) # => true
|
|
@@ -106,166 +111,52 @@ module Rack::Mount
|
|
|
106
111
|
# regexp_anchored?(/^foo/) # => false
|
|
107
112
|
# regexp_anchored?(/foo$/) # => false
|
|
108
113
|
def regexp_anchored?(regexp)
|
|
109
|
-
|
|
114
|
+
regexp.source =~ /\A(\\A|\^).*(\\Z|\$)\Z/m ? true : false
|
|
110
115
|
end
|
|
111
116
|
module_function :regexp_anchored?
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# returned.
|
|
116
|
-
#
|
|
117
|
-
# extract_static_regexp(/^foo$/) # => "foo"
|
|
118
|
-
# extract_static_regexp(/^foo\.bar$/) # => "foo.bar"
|
|
119
|
-
# extract_static_regexp(/^foo|bar$/) # => /^foo|bar$/
|
|
120
|
-
def extract_static_regexp(regexp, options = nil)
|
|
121
|
-
if regexp.is_a?(String)
|
|
122
|
-
regexp = Regexp.compile("\\A#{regexp}\\Z", options)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Just return if regexp is case-insensitive
|
|
126
|
-
return regexp if regexp.casefold?
|
|
127
|
-
|
|
118
|
+
def normalize_extended_expression(regexp)
|
|
119
|
+
return regexp unless regexp.options & Regexp::EXTENDED != 0
|
|
128
120
|
source = regexp.source
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
Regexp.compile("\\A(#{source})\\Z") =~ unescaped_source
|
|
134
|
-
return unescaped_source
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
regexp
|
|
138
|
-
end
|
|
139
|
-
module_function :extract_static_regexp
|
|
140
|
-
|
|
141
|
-
if Const::SUPPORTS_NAMED_CAPTURES
|
|
142
|
-
NAMED_CAPTURE_REGEXP = /\?<([^>]+)>/
|
|
143
|
-
else
|
|
144
|
-
NAMED_CAPTURE_REGEXP = /\?:<([^>]+)>/
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Strips shim named capture syntax and returns a clean Regexp and
|
|
148
|
-
# an ordered array of the named captures.
|
|
149
|
-
#
|
|
150
|
-
# extract_named_captures(/[a-z]+/) # => /[a-z]+/, []
|
|
151
|
-
# extract_named_captures(/(?:<foo>[a-z]+)/) # => /([a-z]+)/, ['foo']
|
|
152
|
-
# extract_named_captures(/([a-z]+)(?:<foo>[a-z]+)/)
|
|
153
|
-
# # => /([a-z]+)([a-z]+)/, [nil, 'foo']
|
|
154
|
-
def extract_named_captures(regexp)
|
|
155
|
-
options = regexp.is_a?(Regexp) ? regexp.options : nil
|
|
156
|
-
source = Regexp.compile(regexp).source
|
|
157
|
-
names, scanner = [], StringScanner.new(source)
|
|
158
|
-
|
|
159
|
-
while scanner.skip_until(/\(/)
|
|
160
|
-
if scanner.scan(NAMED_CAPTURE_REGEXP)
|
|
161
|
-
names << scanner[1]
|
|
162
|
-
else
|
|
163
|
-
names << nil
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
names = [] unless names.any?
|
|
168
|
-
source.gsub!(NAMED_CAPTURE_REGEXP, Const::EMPTY_STRING)
|
|
169
|
-
return Regexp.compile(source, options), names
|
|
121
|
+
source.gsub!(/#.+$/, '')
|
|
122
|
+
source.gsub!(/\s+/, '')
|
|
123
|
+
source.gsub!(/\\\//, '/')
|
|
124
|
+
Regexp.compile(source)
|
|
170
125
|
end
|
|
171
|
-
module_function :
|
|
172
|
-
|
|
173
|
-
class Capture < Array #:nodoc:
|
|
174
|
-
attr_reader :name, :optional
|
|
175
|
-
alias_method :optional?, :optional
|
|
176
|
-
|
|
177
|
-
def initialize(*args)
|
|
178
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
179
|
-
|
|
180
|
-
@name = options.delete(:name)
|
|
181
|
-
@name = @name.to_s if @name
|
|
182
|
-
|
|
183
|
-
@optional = options.delete(:optional) || false
|
|
184
|
-
|
|
185
|
-
super(args)
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def ==(obj)
|
|
189
|
-
obj.is_a?(Capture) && @name == obj.name && @optional == obj.optional && super
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def optionalize!
|
|
193
|
-
@optional = true
|
|
194
|
-
self
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def named?
|
|
198
|
-
name && name != Const::EMPTY_STRING
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def to_s
|
|
202
|
-
source = "(#{join})"
|
|
203
|
-
source << '?' if optional?
|
|
204
|
-
source
|
|
205
|
-
end
|
|
126
|
+
module_function :normalize_extended_expression
|
|
206
127
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
end
|
|
128
|
+
def parse_regexp(regexp)
|
|
129
|
+
cache = @@_parse_regexp_cache ||= {}
|
|
210
130
|
|
|
211
|
-
|
|
212
|
-
|
|
131
|
+
if expression = cache[regexp]
|
|
132
|
+
return expression
|
|
213
133
|
end
|
|
214
|
-
end
|
|
215
134
|
|
|
216
|
-
def extract_regexp_parts(regexp) #:nodoc:
|
|
217
135
|
unless regexp.is_a?(RegexpWithNamedGroups)
|
|
218
136
|
regexp = RegexpWithNamedGroups.new(regexp)
|
|
219
137
|
end
|
|
220
138
|
|
|
221
|
-
|
|
222
|
-
regexp, names = extract_named_captures(regexp)
|
|
223
|
-
else
|
|
224
|
-
names = regexp.names
|
|
225
|
-
end
|
|
226
|
-
source = regexp.source
|
|
227
|
-
|
|
228
|
-
source =~ /^(\\A|\^)/ ? source.gsub!(/^(\\A|\^)/, Const::EMPTY_STRING) :
|
|
229
|
-
raise(ArgumentError, "#{source} needs to match the start of the string")
|
|
230
|
-
|
|
231
|
-
scanner = StringScanner.new(source)
|
|
232
|
-
stack = [[]]
|
|
233
|
-
|
|
234
|
-
capture_index = 0
|
|
235
|
-
until scanner.eos?
|
|
236
|
-
char = scanner.getch
|
|
237
|
-
cur = stack.last
|
|
238
|
-
|
|
239
|
-
escaped = cur.last.is_a?(String) && cur.last[-1, 1] == '\\'
|
|
139
|
+
expression = Regin.parse(regexp)
|
|
240
140
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
capture_index += 1
|
|
251
|
-
cur.push(capture)
|
|
252
|
-
stack.push(capture)
|
|
253
|
-
elsif char == ')'
|
|
254
|
-
capture = stack.pop
|
|
255
|
-
if scanner.peek(1) == '?'
|
|
256
|
-
scanner.pos += 1
|
|
257
|
-
capture.optionalize!
|
|
141
|
+
unless Regin.regexp_supports_named_captures?
|
|
142
|
+
tag_captures = Proc.new do |group|
|
|
143
|
+
case group
|
|
144
|
+
when Regin::Group
|
|
145
|
+
# TODO: dup instead of mutating
|
|
146
|
+
group.instance_variable_set('@name', regexp.names[group.index]) if group.index
|
|
147
|
+
tag_captures.call(group.expression)
|
|
148
|
+
when Regin::Expression
|
|
149
|
+
group.each { |child| tag_captures.call(child) }
|
|
258
150
|
end
|
|
259
|
-
elsif char == '$'
|
|
260
|
-
cur.push(Const::NULL)
|
|
261
|
-
else
|
|
262
|
-
cur.push('') unless cur.last.is_a?(String)
|
|
263
|
-
cur.last << char
|
|
264
151
|
end
|
|
152
|
+
tag_captures.call(expression)
|
|
265
153
|
end
|
|
266
154
|
|
|
267
|
-
|
|
155
|
+
cache[regexp] = expression.freeze
|
|
156
|
+
expression
|
|
157
|
+
rescue Racc::ParseError, Regin::Parser::ScanError
|
|
158
|
+
[]
|
|
268
159
|
end
|
|
269
|
-
module_function :
|
|
160
|
+
module_function :parse_regexp
|
|
270
161
|
end
|
|
271
162
|
end
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
+
require 'forwardable'
|
|
1
2
|
require 'multiset'
|
|
2
3
|
|
|
3
4
|
# Multimap is a generalization of a map or associative array
|
|
4
5
|
# abstract data type in which more than one value may be associated
|
|
5
6
|
# with and returned for a given key.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
#
|
|
8
|
+
# == Example
|
|
9
|
+
#
|
|
10
|
+
# require 'multimap'
|
|
11
|
+
# map = Multimap.new
|
|
12
|
+
# map["a"] = 100
|
|
13
|
+
# map["b"] = 200
|
|
14
|
+
# map["a"] = 300
|
|
15
|
+
# map["a"] # -> [100, 300]
|
|
16
|
+
# map["b"] # -> [200]
|
|
17
|
+
# map.keys # -> #<Multiset: {a, a, b}>
|
|
18
|
+
class Multimap
|
|
19
|
+
extend Forwardable
|
|
20
|
+
|
|
21
|
+
include Enumerable
|
|
20
22
|
|
|
21
23
|
# call-seq:
|
|
22
24
|
# Multimap[ [key =>|, value]* ] => multimap
|
|
@@ -53,7 +55,8 @@ class Multimap < Hash
|
|
|
53
55
|
}
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
map =
|
|
58
|
+
map = new
|
|
59
|
+
map.instance_variable_set(:@hash, Hash[*args])
|
|
57
60
|
map.default = default
|
|
58
61
|
map
|
|
59
62
|
end
|
|
@@ -70,20 +73,24 @@ class Multimap < Hash
|
|
|
70
73
|
# h["a"] #=> [100].to_set
|
|
71
74
|
# h["c"] #=> [].to_set
|
|
72
75
|
def initialize(default = [])
|
|
73
|
-
|
|
76
|
+
@hash = Hash.new(default)
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
def initialize_copy(original) #:nodoc:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
@hash = Hash.new(original.default.dup)
|
|
81
|
+
original._internal_hash.each_pair do |key, container|
|
|
82
|
+
@hash[key] = container.dup
|
|
83
|
+
end
|
|
81
84
|
end
|
|
82
85
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
def_delegators :@hash, :clear, :default, :default=, :empty?,
|
|
87
|
+
:fetch, :has_key?, :key?
|
|
88
|
+
|
|
89
|
+
# Retrieves the <i>value</i> object corresponding to the
|
|
90
|
+
# <i>*keys</i> object.
|
|
91
|
+
def [](key)
|
|
92
|
+
@hash[key]
|
|
93
|
+
end
|
|
87
94
|
|
|
88
95
|
# call-seq:
|
|
89
96
|
# map[key] = value => value
|
|
@@ -118,9 +125,9 @@ class Multimap < Hash
|
|
|
118
125
|
# map.delete("a") #=> [100]
|
|
119
126
|
def delete(key, value = nil)
|
|
120
127
|
if value
|
|
121
|
-
|
|
128
|
+
@hash[key].delete(value)
|
|
122
129
|
else
|
|
123
|
-
|
|
130
|
+
@hash.delete(key)
|
|
124
131
|
end
|
|
125
132
|
end
|
|
126
133
|
|
|
@@ -157,14 +164,9 @@ class Multimap < Hash
|
|
|
157
164
|
#
|
|
158
165
|
# a is [100]
|
|
159
166
|
# b is [200, 300]
|
|
160
|
-
def each_association
|
|
161
|
-
|
|
167
|
+
def each_association(&block)
|
|
168
|
+
@hash.each_pair(&block)
|
|
162
169
|
end
|
|
163
|
-
#--
|
|
164
|
-
# Ignore alias_method since the definition above serves
|
|
165
|
-
# as its documentation.
|
|
166
|
-
#++
|
|
167
|
-
module_eval "alias_method :each_association, :each_pair"
|
|
168
170
|
|
|
169
171
|
# call-seq:
|
|
170
172
|
# map.each_container { |container| block } => map
|
|
@@ -247,6 +249,24 @@ class Multimap < Hash
|
|
|
247
249
|
end
|
|
248
250
|
end
|
|
249
251
|
|
|
252
|
+
def ==(other) #:nodoc:
|
|
253
|
+
case other
|
|
254
|
+
when Multimap
|
|
255
|
+
@hash == other._internal_hash
|
|
256
|
+
else
|
|
257
|
+
@hash == other
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def eql?(other) #:nodoc:
|
|
262
|
+
case other
|
|
263
|
+
when Multimap
|
|
264
|
+
@hash.eql?(other._internal_hash)
|
|
265
|
+
else
|
|
266
|
+
@hash.eql?(other)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
250
270
|
def freeze #:nodoc:
|
|
251
271
|
each_container { |container| container.freeze }
|
|
252
272
|
default.freeze
|
|
@@ -282,6 +302,48 @@ class Multimap < Hash
|
|
|
282
302
|
invert[value]
|
|
283
303
|
end
|
|
284
304
|
|
|
305
|
+
# call-seq:
|
|
306
|
+
# map.delete_if {| key, value | block } -> map
|
|
307
|
+
#
|
|
308
|
+
# Deletes every key-value pair from <i>map</i> for which <i>block</i>
|
|
309
|
+
# evaluates to <code>true</code>.
|
|
310
|
+
#
|
|
311
|
+
# map = Multimap["a" => 100, "b" => [200, 300]]
|
|
312
|
+
# map.delete_if {|key, value| value >= 300 }
|
|
313
|
+
# #=> Multimap["a" => 100, "b" => 200]
|
|
314
|
+
#
|
|
315
|
+
def delete_if
|
|
316
|
+
each_association do |key, container|
|
|
317
|
+
container.delete_if do |value|
|
|
318
|
+
yield [key, value]
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
self
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# call-seq:
|
|
325
|
+
# map.reject {| key, value | block } -> map
|
|
326
|
+
#
|
|
327
|
+
# Same as <code>Multimap#delete_if</code>, but works on (and returns) a
|
|
328
|
+
# copy of the <i>map</i>. Equivalent to
|
|
329
|
+
# <code><i>map</i>.dup.delete_if</code>.
|
|
330
|
+
#
|
|
331
|
+
def reject(&block)
|
|
332
|
+
dup.delete_if(&block)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# call-seq:
|
|
336
|
+
# map.reject! {| key, value | block } -> map or nil
|
|
337
|
+
#
|
|
338
|
+
# Equivalent to <code>Multimap#delete_if</code>, but returns
|
|
339
|
+
# <code>nil</code> if no changes were made.
|
|
340
|
+
#
|
|
341
|
+
def reject!(&block)
|
|
342
|
+
old_size = size
|
|
343
|
+
delete_if(&block)
|
|
344
|
+
old_size == size ? nil : self
|
|
345
|
+
end
|
|
346
|
+
|
|
285
347
|
# call-seq:
|
|
286
348
|
# map.replace(other_map) => map
|
|
287
349
|
#
|
|
@@ -294,11 +356,11 @@ class Multimap < Hash
|
|
|
294
356
|
def replace(other)
|
|
295
357
|
case other
|
|
296
358
|
when Array
|
|
297
|
-
|
|
359
|
+
@hash.replace(self.class[self.default, *other])
|
|
298
360
|
when Hash
|
|
299
|
-
|
|
361
|
+
@hash.replace(self.class[self.default, other])
|
|
300
362
|
when self.class
|
|
301
|
-
|
|
363
|
+
@hash.replace(other)
|
|
302
364
|
else
|
|
303
365
|
raise ArgumentError
|
|
304
366
|
end
|
|
@@ -332,6 +394,12 @@ class Multimap < Hash
|
|
|
332
394
|
keys
|
|
333
395
|
end
|
|
334
396
|
|
|
397
|
+
# Returns true if the given key is present in Multimap.
|
|
398
|
+
def include?(key)
|
|
399
|
+
keys.include?(key)
|
|
400
|
+
end
|
|
401
|
+
alias_method :member?, :include?
|
|
402
|
+
|
|
335
403
|
# call-seq:
|
|
336
404
|
# map.length => fixnum
|
|
337
405
|
# map.size => fixnum
|
|
@@ -425,7 +493,7 @@ class Multimap < Hash
|
|
|
425
493
|
# map = Multimap["a" => 100, "b" => [200, 300]]
|
|
426
494
|
# map.to_hash #=> { "a" => [100], "b" => [200, 300] }
|
|
427
495
|
def to_hash
|
|
428
|
-
dup
|
|
496
|
+
@hash.dup
|
|
429
497
|
end
|
|
430
498
|
|
|
431
499
|
# call-seq:
|
|
@@ -456,11 +524,46 @@ class Multimap < Hash
|
|
|
456
524
|
values
|
|
457
525
|
end
|
|
458
526
|
|
|
527
|
+
# Return an array containing the values associated with the given keys.
|
|
528
|
+
def values_at(*keys)
|
|
529
|
+
@hash.values_at(*keys)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def marshal_dump #:nodoc:
|
|
533
|
+
@hash
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def marshal_load(hash) #:nodoc:
|
|
537
|
+
@hash = hash
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def to_yaml(opts = {}) #:nodoc:
|
|
541
|
+
YAML::quick_emit(self, opts) do |out|
|
|
542
|
+
out.map(taguri, to_yaml_style) do |map|
|
|
543
|
+
@hash.each do |k, v|
|
|
544
|
+
map.add(k, v)
|
|
545
|
+
end
|
|
546
|
+
map.add('__default__', @hash.default)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def yaml_initialize(tag, val) #:nodoc:
|
|
552
|
+
default = val.delete('__default__')
|
|
553
|
+
@hash = val
|
|
554
|
+
@hash.default = default
|
|
555
|
+
self
|
|
556
|
+
end
|
|
557
|
+
|
|
459
558
|
protected
|
|
559
|
+
def _internal_hash #:nodoc:
|
|
560
|
+
@hash
|
|
561
|
+
end
|
|
562
|
+
|
|
460
563
|
def update_container(key) #:nodoc:
|
|
461
|
-
container =
|
|
564
|
+
container = @hash[key]
|
|
462
565
|
container = container.dup if container.equal?(default)
|
|
463
566
|
container = yield(container)
|
|
464
|
-
|
|
567
|
+
@hash[key] = container
|
|
465
568
|
end
|
|
466
569
|
end
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
require 'set'
|
|
2
2
|
|
|
3
3
|
# Multiset implements a collection of unordered values and
|
|
4
|
-
# allows
|
|
4
|
+
# allows duplicates.
|
|
5
|
+
#
|
|
6
|
+
# == Example
|
|
7
|
+
#
|
|
8
|
+
# require 'multiset'
|
|
9
|
+
# s1 = Multiset.new [1, 2] # -> #<Multiset: {1, 2}>
|
|
10
|
+
# s1.add(2) # -> #<Multiset: {1, 2, 2}>
|
|
11
|
+
# s1.merge([2, 6]) # -> #<Multiset: {1, 2, 2, 2, 3}>
|
|
12
|
+
# s1.multiplicity(2) # -> 3
|
|
13
|
+
# s1.multiplicity(3) # -> 1
|
|
5
14
|
class Multiset < Set
|
|
6
15
|
def initialize(*args, &block) #:nodoc:
|
|
7
16
|
@hash = Hash.new(0)
|
|
@@ -150,4 +159,27 @@ class Multiset < Set
|
|
|
150
159
|
superset?(set) && subset?(set)
|
|
151
160
|
end
|
|
152
161
|
alias_method :==, :eql?
|
|
162
|
+
|
|
163
|
+
def marshal_dump #:nodoc:
|
|
164
|
+
@hash
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def marshal_load(hash) #:nodoc:
|
|
168
|
+
@hash = hash
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def to_yaml(opts = {}) #:nodoc:
|
|
172
|
+
YAML::quick_emit(self, opts) do |out|
|
|
173
|
+
out.map(taguri, to_yaml_style) do |map|
|
|
174
|
+
@hash.each do |k, v|
|
|
175
|
+
map.add(k, v)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def yaml_initialize(tag, val) #:nodoc:
|
|
182
|
+
@hash = val
|
|
183
|
+
self
|
|
184
|
+
end
|
|
153
185
|
end
|
|
@@ -45,7 +45,7 @@ class NestedMultimap < Multimap
|
|
|
45
45
|
# map["a"] #=> [100, 300]
|
|
46
46
|
# map["c"] #=> [300]
|
|
47
47
|
def <<(value)
|
|
48
|
-
|
|
48
|
+
@hash.each_value { |container| container << value }
|
|
49
49
|
self.default << value
|
|
50
50
|
self
|
|
51
51
|
end
|
|
@@ -59,7 +59,7 @@ class NestedMultimap < Multimap
|
|
|
59
59
|
def [](*keys)
|
|
60
60
|
i, l, r, k = 0, keys.length, self, self.class
|
|
61
61
|
while r.is_a?(k)
|
|
62
|
-
r = i < l ? r.
|
|
62
|
+
r = i < l ? r._internal_hash[keys[i]] : r.default
|
|
63
63
|
i += 1
|
|
64
64
|
end
|
|
65
65
|
r
|
|
@@ -83,7 +83,7 @@ class NestedMultimap < Multimap
|
|
|
83
83
|
# ["a", "b"] is [100, 101, 102]
|
|
84
84
|
# "c" is [200]
|
|
85
85
|
def each_association
|
|
86
|
-
super do |key, container|
|
|
86
|
+
super() do |key, container|
|
|
87
87
|
if container.respond_to?(:each_association)
|
|
88
88
|
container.each_association do |nested_key, value|
|
|
89
89
|
yield [key, nested_key].flatten, value
|
|
@@ -111,20 +111,11 @@ class NestedMultimap < Multimap
|
|
|
111
111
|
# [100, 101, 102]
|
|
112
112
|
# [100, 102]
|
|
113
113
|
# []
|
|
114
|
-
def each_container_with_default
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
container.each_container_with_default do |value|
|
|
118
|
-
yield value
|
|
119
|
-
end
|
|
120
|
-
else
|
|
121
|
-
yield container
|
|
122
|
-
end
|
|
114
|
+
def each_container_with_default(&block)
|
|
115
|
+
@hash.each_value do |container|
|
|
116
|
+
iterate_over_container(container, &block)
|
|
123
117
|
end
|
|
124
|
-
|
|
125
|
-
hash_each_pair { |_, container| each_container.call(container) }
|
|
126
|
-
each_container.call(default)
|
|
127
|
-
|
|
118
|
+
iterate_over_container(default, &block)
|
|
128
119
|
self
|
|
129
120
|
end
|
|
130
121
|
|
|
@@ -148,6 +139,17 @@ class NestedMultimap < Multimap
|
|
|
148
139
|
def inspect #:nodoc:
|
|
149
140
|
super.gsub(/\}$/, ", default => #{default.inspect}}")
|
|
150
141
|
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
def iterate_over_container(container)
|
|
145
|
+
if container.respond_to?(:each_container_with_default)
|
|
146
|
+
container.each_container_with_default do |value|
|
|
147
|
+
yield value
|
|
148
|
+
end
|
|
149
|
+
else
|
|
150
|
+
yield container
|
|
151
|
+
end
|
|
152
|
+
end
|
|
151
153
|
end
|
|
152
154
|
|
|
153
155
|
begin
|