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.
Files changed (45) hide show
  1. data/README.rdoc +12 -4
  2. data/lib/rack/mount/analysis/frequency.rb +15 -6
  3. data/lib/rack/mount/analysis/histogram.rb +55 -6
  4. data/lib/rack/mount/analysis/splitting.rb +85 -71
  5. data/lib/rack/mount/code_generation.rb +117 -0
  6. data/lib/rack/mount/generatable_regexp.rb +95 -48
  7. data/lib/rack/mount/multimap.rb +2 -43
  8. data/lib/rack/mount/prefix.rb +12 -7
  9. data/lib/rack/mount/regexp_with_named_groups.rb +27 -7
  10. data/lib/rack/mount/route.rb +79 -18
  11. data/lib/rack/mount/route_set.rb +330 -22
  12. data/lib/rack/mount/strexp/parser.rb +0 -0
  13. data/lib/rack/mount/strexp/parser.y +34 -0
  14. data/lib/rack/mount/strexp/tokenizer.rb +83 -0
  15. data/lib/rack/mount/strexp/tokenizer.rex +12 -0
  16. data/lib/rack/mount/strexp.rb +54 -79
  17. data/lib/rack/mount/utils.rb +65 -174
  18. data/lib/rack/mount/vendor/multimap/multimap.rb +142 -39
  19. data/lib/rack/mount/vendor/multimap/multiset.rb +33 -1
  20. data/lib/rack/mount/vendor/multimap/nested_multimap.rb +18 -16
  21. data/lib/rack/mount/vendor/regin/regin/alternation.rb +40 -0
  22. data/lib/rack/mount/vendor/regin/regin/anchor.rb +4 -0
  23. data/lib/rack/mount/vendor/regin/regin/atom.rb +54 -0
  24. data/lib/rack/mount/vendor/regin/regin/character.rb +51 -0
  25. data/lib/rack/mount/vendor/regin/regin/character_class.rb +50 -0
  26. data/lib/rack/mount/vendor/regin/regin/collection.rb +77 -0
  27. data/lib/rack/mount/vendor/regin/regin/expression.rb +126 -0
  28. data/lib/rack/mount/vendor/regin/regin/group.rb +85 -0
  29. data/lib/rack/mount/vendor/regin/regin/options.rb +55 -0
  30. data/lib/rack/mount/vendor/regin/regin/parser.rb +521 -0
  31. data/lib/rack/mount/vendor/regin/regin/tokenizer.rb +0 -0
  32. data/lib/rack/mount/vendor/regin/regin/version.rb +3 -0
  33. data/lib/rack/mount/vendor/regin/regin.rb +75 -0
  34. data/lib/rack/mount/version.rb +3 -0
  35. data/lib/rack/mount.rb +13 -16
  36. metadata +73 -29
  37. data/lib/rack/mount/const.rb +0 -45
  38. data/lib/rack/mount/exceptions.rb +0 -3
  39. data/lib/rack/mount/generation/route.rb +0 -57
  40. data/lib/rack/mount/generation/route_set.rb +0 -163
  41. data/lib/rack/mount/meta_method.rb +0 -104
  42. data/lib/rack/mount/mixover.rb +0 -47
  43. data/lib/rack/mount/recognition/code_generation.rb +0 -99
  44. data/lib/rack/mount/recognition/route.rb +0 -59
  45. data/lib/rack/mount/recognition/route_set.rb +0 -88
@@ -1,5 +1,10 @@
1
- require 'rack/mount/regexp_with_named_groups'
2
- require 'strscan'
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!(Const::SLASH)
23
- path.sub!(%r{/+\Z}, Const::EMPTY_STRING)
24
- path = Const::SLASH if path == Const::EMPTY_STRING
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
- # pop_trailing_nils!([1, 2, 3]) # => [1, 2, 3]
32
- # pop_trailing_nils!([1, 2, 3, nil, nil]) # => [1, 2, 3]
33
- # pop_trailing_nils!([nil]) # => []
34
- def pop_trailing_nils!(ary)
35
- while ary.length > 0 && ary.last.nil?
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 :pop_trailing_nils!
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
- URI.escape(uri.to_s, UNSAFE_PCHAR)
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
- URI.unescape(uri).force_encoding('utf-8')
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
- "#{Rack::Utils.escape(prefix)}=#{Rack::Utils.escape(value)}"
80
- when NilClass
81
- Rack::Utils.escape(prefix)
100
+ "#{prefix}=#{Rack::Utils.escape(value)}"
82
101
  else
83
- if value.respond_to?(:to_param)
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
- regexp.source =~ /\A(\\A|\^).*(\\Z|\$)\Z/ ? true : false
114
+ regexp.source =~ /\A(\\A|\^).*(\\Z|\$)\Z/m ? true : false
110
115
  end
111
116
  module_function :regexp_anchored?
112
117
 
113
- # Returns static string source of Regexp if it only includes static
114
- # characters and no metacharacters. Otherwise the original Regexp is
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
- if regexp_anchored?(regexp)
130
- source.sub!(/^(\\A|\^)(.*)(\\Z|\$)$/, '\2')
131
- unescaped_source = source.gsub(/\\/, Const::EMPTY_STRING)
132
- if source == Regexp.escape(unescaped_source) &&
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 :extract_named_captures
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
- def first_part
208
- first.is_a?(Capture) ? first.first_part : first
209
- end
128
+ def parse_regexp(regexp)
129
+ cache = @@_parse_regexp_cache ||= {}
210
130
 
211
- def last_part
212
- last.is_a?(Capture) ? last.last_part : last
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
- if regexp.source =~ /\?<([^>]+)>/
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
- if char == '\\' && scanner.peek(1) == 'Z'
242
- scanner.pos += 1
243
- cur.push(Const::NULL)
244
- elsif escaped
245
- cur.push('') unless cur.last.is_a?(String)
246
- cur.last << char
247
- elsif char == '('
248
- name = names[capture_index]
249
- capture = Capture.new(:name => name)
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
- stack.pop
155
+ cache[regexp] = expression.freeze
156
+ expression
157
+ rescue Racc::ParseError, Regin::Parser::ScanError
158
+ []
268
159
  end
269
- module_function :extract_regexp_parts
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
- class Multimap < Hash
7
- #--
8
- # Ignore protected aliases back to the original Hash methods
9
- #++
10
- module_eval %{
11
- alias_method :hash_aref, :[]
12
- protected :hash_aref
13
-
14
- alias_method :hash_aset, :[]=
15
- protected :hash_aset
16
-
17
- alias_method :hash_each_pair, :each_pair
18
- protected :hash_each_pair
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 = super
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
- super
76
+ @hash = Hash.new(default)
74
77
  end
75
78
 
76
79
  def initialize_copy(original) #:nodoc:
77
- super
78
- clear
79
- original.each_pair { |key, container| self[key] = container }
80
- self.default = original.default.dup
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
- # Setting a default proc is not supported
85
- #++
86
- undef :default_proc
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
- hash_aref(key).delete(value)
128
+ @hash[key].delete(value)
122
129
  else
123
- super(key)
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
- # each_pair
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
- super(self.class[self.default, *other])
359
+ @hash.replace(self.class[self.default, *other])
298
360
  when Hash
299
- super(self.class[self.default, other])
361
+ @hash.replace(self.class[self.default, other])
300
362
  when self.class
301
- super
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 = hash_aref(key)
564
+ container = @hash[key]
462
565
  container = container.dup if container.equal?(default)
463
566
  container = yield(container)
464
- hash_aset(key, container)
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 duplicate values.
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
- hash_each_pair { |_, container| container << value }
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.hash_aref(keys[i]) : r.default
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
- each_container = Proc.new do |container|
116
- if container.respond_to?(:each_container_with_default)
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