insensitive_hash 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.markdown CHANGED
@@ -1,3 +1,9 @@
1
+ ### 0.2.2 / 2012/02/13
2
+ * :underscore option added
3
+ * `Insensitive::KeyClashError` added for safer operations
4
+ * Bug fix: Preserve default/default_proc on merge/replace/insensitive
5
+ * Subtle behavioral change in `Insensitive#replace` when called with an ordinary Hash
6
+
1
7
  ### 0.2.1 / 2012/02/10
2
8
  * Bug fix: Insensitive `fetch`
3
9
 
data/README.markdown CHANGED
@@ -1,12 +1,15 @@
1
- # insensitive_hash
1
+ insensitive_hash
2
+ ================
2
3
  Hash with case-insensitive, Symbol/String-indifferent key access.
3
4
 
4
- ## Installation
5
+ Installation
6
+ ------------
5
7
  ```
6
8
  gem install insensitive_hash
7
9
  ```
8
10
 
9
- ## Examples
11
+ Examples
12
+ --------
10
13
 
11
14
  ### Instantiation
12
15
  ```ruby
@@ -69,6 +72,20 @@ db['Development']['ADAPTER']
69
72
  db[:production][:adapter]
70
73
  ```
71
74
 
75
+ ### Replacing spaces in String keys to underscores
76
+ ```ruby
77
+ h = { 'A key with spaces' => true }
78
+
79
+ ih = h.insensitive :underscore => true
80
+ ih[:a_key_with_spaces] # true
81
+
82
+ # Or,
83
+ ih = InsensitiveHash[ h ]
84
+ ih.underscore = true
85
+ ih.underscore? # true
86
+ ih[:a_key_with_spaces] # true
87
+ ```
88
+
72
89
  ## Contributing to insensitive_hash
73
90
 
74
91
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
@@ -2,8 +2,12 @@ require 'insensitive_hash/version'
2
2
  require 'insensitive_hash/insensitive_hash'
3
3
 
4
4
  class Hash
5
- def insensitive
6
- InsensitiveHash[ self ]
5
+ # @return [InsensitiveHash]
6
+ def insensitive options = {}
7
+ InsensitiveHash.new.tap do |ih|
8
+ ih.replace self
9
+ ih.underscore = options[:underscore] if options.has_key?(:underscore)
10
+ end
7
11
  end
8
12
  end
9
13
 
@@ -1,5 +1,6 @@
1
1
  class InsensitiveHash < Hash
2
- attr_reader :key_map
2
+ class KeyClashError < Exception
3
+ end
3
4
 
4
5
  def initialize default = nil, &block
5
6
  if block_given?
@@ -8,7 +9,31 @@ class InsensitiveHash < Hash
8
9
  super
9
10
  end
10
11
 
11
- @key_map = {}
12
+ @key_map = {}
13
+ @underscore = false
14
+ end
15
+
16
+ # Sets whether to replace spaces in String keys to underscores
17
+ # @param [Boolean] us
18
+ # @return [Boolean]
19
+ def underscore= us
20
+ raise ArgumentError.new("Not true or false") unless [true, false].include?(us)
21
+
22
+ # Check key clash
23
+ detect_clash self, us
24
+
25
+ @underscore = us
26
+ @key_map = {}
27
+ self.keys.each do |k|
28
+ self[k] = self.delete(k)
29
+ end
30
+
31
+ us
32
+ end
33
+
34
+ # @return [Boolean] Whether to replace spaces in String keys to underscores
35
+ def underscore?
36
+ @underscore
12
37
  end
13
38
 
14
39
  # Returns a normal, sensitive Hash
@@ -36,17 +61,15 @@ class InsensitiveHash < Hash
36
61
  end
37
62
 
38
63
  def []= key, value
39
- ekey = encode key
40
- if @key_map.has_key? ekey
41
- delete @key_map[ekey]
42
- end
43
-
44
- @key_map[encode key] = key
45
- super(lookup_key(key), InsensitiveHash.wrap(value))
64
+ delete key
65
+ ekey = encode key, @underscore
66
+ @key_map[ekey] = key
67
+ super key, wrap(value, underscore?)
46
68
  end
47
69
  alias store []=
48
70
 
49
71
  def merge! other_hash
72
+ detect_clash other_hash, underscore?
50
73
  other_hash.each do |key, value|
51
74
  self[key] = value
52
75
  end
@@ -55,7 +78,8 @@ class InsensitiveHash < Hash
55
78
  alias update! merge!
56
79
 
57
80
  def merge other_hash
58
- InsensitiveHash[self].tap do |ih|
81
+ InsensitiveHash.new.tap do |ih|
82
+ ih.replace self
59
83
  ih.merge! other_hash
60
84
  end
61
85
  end
@@ -71,6 +95,16 @@ class InsensitiveHash < Hash
71
95
  end
72
96
 
73
97
  def replace other
98
+ # TODO
99
+ # What is the correct behavior of replace when the other hash is not an InsensitiveHash?
100
+ # underscore property precedence: other => self (FIXME)
101
+ us = other.respond_to?(:underscore?) ? other.underscore? : self.underscore?
102
+ detect_clash other, us
103
+
104
+ self.default = other.default
105
+ self.default_proc = other.default_proc if other.default_proc
106
+ self.underscore = us
107
+
74
108
  clear
75
109
  other.each do |k, v|
76
110
  self[k] = v
@@ -93,19 +127,21 @@ class InsensitiveHash < Hash
93
127
  end
94
128
 
95
129
  private
96
- def self.wrap value
130
+ def wrap value, us
97
131
  case value
132
+ when InsensitiveHash
133
+ value.tap { |ih| ih.underscore = us || ih.underscore? }
98
134
  when Hash
99
- value.class == InsensitiveHash ? value : InsensitiveHash[value]
135
+ InsensitiveHash[value].tap { |ih| ih.underscore = us }
100
136
  when Array
101
- value.map { |v| InsensitiveHash.wrap v }
137
+ value.map { |v| wrap v, us }
102
138
  else
103
139
  value
104
140
  end
105
141
  end
106
142
 
107
143
  def lookup_key key, delete = false
108
- ekey = encode key
144
+ ekey = encode key, @underscore
109
145
  if @key_map.has_key?(ekey)
110
146
  delete ? @key_map.delete(ekey) : @key_map[ekey]
111
147
  else
@@ -113,15 +149,24 @@ private
113
149
  end
114
150
  end
115
151
 
116
- def encode key
152
+ def encode key, us
117
153
  case key
118
- when String
119
- key.downcase
120
- when Symbol
121
- key.to_s.downcase
154
+ when String, Symbol
155
+ key = key.to_s.downcase
156
+ if us
157
+ key.gsub(' ', '_')
158
+ else
159
+ key
160
+ end
122
161
  else
123
162
  key
124
163
  end
125
164
  end
165
+
166
+ def detect_clash hash, us
167
+ hash.keys.map { |k| encode k, us }.tap { |ekeys|
168
+ raise KeyClashError.new("Key clash detected") if ekeys != ekeys.uniq
169
+ }
170
+ end
126
171
  end
127
172
 
@@ -1,3 +1,3 @@
1
1
  class InsensitiveHash < Hash
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
@@ -73,6 +73,60 @@ class TestInsensitiveHash < Test::Unit::TestCase
73
73
  end
74
74
  end
75
75
  end
76
+
77
+ # InsensitiveHash#insensitive
78
+ ihash1 = hash.insensitive
79
+ ihash1.default = :default
80
+ ihash1.underscore = true
81
+ ihash2 = ihash1.insensitive.insensitive.insensitive
82
+
83
+ assert_equal InsensitiveHash, ihash2.class
84
+ assert_equal :default, ihash2.default
85
+ assert_equal true, ihash2.underscore?
86
+
87
+ # Preserving default
88
+ [:default, nil].each do |d|
89
+ hash.default = d
90
+ assert_equal d, hash.insensitive.insensitive.insensitive[:anything]
91
+ end
92
+
93
+ # Preserving default_proc
94
+ hash2 = {}
95
+ hash2.replace hash
96
+ hash2.default_proc = proc { |h, k| h[k] = :default }
97
+
98
+ ihash2 = hash2.insensitive.insensitive.insensitive
99
+ assert !ihash2.has_key?(:anything)
100
+ assert_equal :default, ihash2[:anything]
101
+ assert ihash2.has_key?(:anything)
102
+
103
+ # FIXME: test insensitive call with encoder
104
+ h = InsensitiveHash[{'Key with spaces' => 1}]
105
+ h2 = h.insensitive :underscore => true
106
+ h3 = h.insensitive :underscore => false
107
+ assert_raise(ArgumentError) { h.insensitive :underscore => :wat }
108
+
109
+ assert_equal 1, h['key with spaces']
110
+ assert_nil h[:key_with_spaces]
111
+ assert_equal 1, h3['key with spaces']
112
+ assert_nil h3[:key_with_spaces]
113
+ assert_equal 1, h2[:KEY_WITH_SPACES]
114
+ assert_equal 1, h2[:Key_with_spaces]
115
+
116
+ assert_equal ['Key with spaces'], h.keys
117
+ assert_equal ['Key with spaces'], h2.keys
118
+ assert_equal ['Key with spaces'], h3.keys
119
+
120
+ h = { 'A key with spaces' => true }
121
+ ih = h.insensitive :underscore => true
122
+ assert ih[:a_key_with_spaces]
123
+ assert ih.underscore?
124
+
125
+ # FIXME: from README
126
+ ih = InsensitiveHash[ h ]
127
+ ih.underscore = true
128
+ assert ih[:a_key_with_spaces]
129
+ assert ih.underscore?
76
130
  end
77
131
 
78
132
  def test_delete
@@ -88,13 +142,36 @@ class TestInsensitiveHash < Test::Unit::TestCase
88
142
 
89
143
  def test_merge
90
144
  [:merge, :update].each do |method|
91
- ih = InsensitiveHash[:a => 1]
145
+ ih = InsensitiveHash[:a => 1, 'hello world' => 2]
92
146
  ih2 = ih.send(method, :b => 2)
93
147
 
94
- assert_equal [:a], ih.keys
95
- assert_equal [:a, :b], ih2.keys
148
+ assert_equal [:a, 'hello world'], ih.keys
149
+ assert_equal [:a, 'hello world', :b], ih2.keys
96
150
  assert ih2.has_key?('B'), 'correctly merged another hash'
97
151
 
152
+ assert_nil ih[:hello_world]
153
+ assert_nil ih2[:hello_world]
154
+
155
+ ih.underscore = true
156
+ assert_equal 2, ih[:hello_world]
157
+ assert_equal 2, ih.send(method, :b => 2)[:hello_world]
158
+
159
+ ih.underscore = false
160
+ assert_nil ih[:hello_world]
161
+ assert_nil ih.send(method, :b => 2)[:hello_world]
162
+
163
+ ih.default = 10
164
+ assert_equal 10, ih.send(method, :b => 2)[:anything]
165
+
166
+ ih.default = nil
167
+ assert_nil ih.send(method, :b => 2)[:anything]
168
+
169
+ ih.default_proc = proc { |h, k| h[k] = :default }
170
+ mh = ih.send(method, :b => 2)
171
+ assert !mh.has_key?(:anything)
172
+ assert_equal :default, mh[:anything]
173
+ assert mh.has_key?(:anything)
174
+
98
175
  ih2.delete 'b'
99
176
  assert ih2.has_key?('B') == false
100
177
  end
@@ -114,6 +191,32 @@ class TestInsensitiveHash < Test::Unit::TestCase
114
191
  end
115
192
  end
116
193
 
194
+ def test_merge_clash
195
+ ih = InsensitiveHash.new
196
+ ih2 = InsensitiveHash.new
197
+ ih2.underscore = true
198
+
199
+ [:merge, :merge!, :update, :update!].each do |method|
200
+ [ih, ih2].each do |h|
201
+ assert_raise(InsensitiveHash::KeyClashError) {
202
+ h.send(method, { :a => 1, :A => 1, 'A' => 1})
203
+ }
204
+ assert_raise(InsensitiveHash::KeyClashError) {
205
+ h.send(method, { 'a' => 1, 'A' => 1 })
206
+ }
207
+ end
208
+
209
+ ih.send(method, { :hello_world => 1, 'hello world' => 2})
210
+ assert_raise(InsensitiveHash::KeyClashError) {
211
+ ih2.send(method, { :hello_world => 1, 'hello world' => 2})
212
+ }
213
+ end
214
+
215
+ assert_raise(InsensitiveHash::KeyClashError) {
216
+ ih.merge({ :a => 1, :A => 1, 'A' => 1})
217
+ }
218
+ end
219
+
117
220
  def test_assoc
118
221
  h = InsensitiveHash[{
119
222
  "colors" => ["red", "blue", "green"],
@@ -248,6 +351,41 @@ class TestInsensitiveHash < Test::Unit::TestCase
248
351
 
249
352
  assert !h.has_key?('A')
250
353
  assert h.has_key?('B')
354
+
355
+ # Default value
356
+ h.replace(Hash.new(5))
357
+ assert_equal 5, h[:anything]
358
+ assert_equal [], h.keys
359
+
360
+ # Default proc
361
+ h.replace(Hash.new { |h, k| h[k] = :default })
362
+ assert_equal :default, h[:anything]
363
+ assert_equal [:anything], h.keys
364
+
365
+ # underscore property of self
366
+ assert !h.underscore?
367
+ h.replace(InsensitiveHash['hello world' => 1].tap { |ih| ih.underscore = true })
368
+ assert h.underscore?
369
+ assert_equal 1, h[:hello_world]
370
+
371
+ h = InsensitiveHash.new
372
+ assert_raise(InsensitiveHash::KeyClashError) {
373
+ h.replace({ 'a' => 1, :A => 2 })
374
+ }
375
+
376
+ # TODO: FIXME
377
+ # underscore property of other
378
+ h = InsensitiveHash.new
379
+ oh = InsensitiveHash[ 'hello world' => 1 ]
380
+ oh.underscore = true
381
+ assert !h.underscore?
382
+ h.replace(oh)
383
+ assert h.underscore?
384
+
385
+ oh.underscore = false
386
+ oh[:hello_world => 2]
387
+ h.replace(oh)
388
+ assert !h.underscore?
251
389
  end
252
390
 
253
391
  def test_rassoc
@@ -278,5 +416,120 @@ class TestInsensitiveHash < Test::Unit::TestCase
278
416
  assert_equal 3, h.fetch(:c) { 3 }
279
417
  assert_raise(KeyError) { h.fetch('D') }
280
418
  end
419
+
420
+ def test_underscore
421
+ h = InsensitiveHash[{'Key with spaces' => 1}]
422
+
423
+ assert_equal 1, h['key with spaces']
424
+ assert_nil h[:key_with_spaces]
425
+
426
+ test_keys = [
427
+ :KEY_WITH_SPACES,
428
+ :Key_with_spaces,
429
+ :key_with_spaces,
430
+ 'key with_spaces',
431
+ 'KEY_WITH spaces'
432
+ ]
433
+
434
+ 10.times do
435
+ assert_raise(ArgumentError) { h.underscore = 'wat' }
436
+ assert !h.underscore?
437
+ h.underscore = true
438
+ assert h.underscore?
439
+
440
+
441
+ test_keys.each do |k|
442
+ assert_equal 1, h[k]
443
+ end
444
+ assert_equal ['Key with spaces'], h.keys
445
+
446
+ h.underscore = false
447
+ assert !h.underscore?
448
+ assert_equal 1, h['KEY WITH SPACES']
449
+ test_keys.each do |k|
450
+ assert_nil h[k]
451
+ end
452
+ assert_equal ['Key with spaces'], h.keys
453
+ end
454
+
455
+ h.underscore = true
456
+ test_keys.each do |tk|
457
+ h[tk] = 1
458
+ end
459
+
460
+ assert_equal [test_keys.last], h.keys
461
+ end
462
+
463
+ def test_underscore_inheritance
464
+ h = InsensitiveHash[
465
+ {
466
+ 'Key with spaces' => {
467
+ 'Another key with spaces' => 1
468
+ },
469
+ 'Key 2 with spaces' =>
470
+ InsensitiveHash['Yet another key with spaces' => 2].tap { |ih| ih.underscore = true },
471
+ 'Key 3 with spaces' =>
472
+ InsensitiveHash['Yet another key with spaces' => 3]
473
+ }
474
+ ]
475
+
476
+ assert_equal 1, h['key with spaces']['another key with spaces']
477
+ assert_equal false, h['key with spaces'].underscore?
478
+ assert_nil h['key with spaces'][:another_key_with_spaces]
479
+ assert_nil h[:key_with_spaces]
480
+
481
+ assert_equal 2, h['key 2 with spaces']['yet another key with spaces']
482
+ assert_equal 2, h['key 2 with spaces'][:yet_another_key_with_spaces]
483
+ assert_nil h[:key_2_with_spaces]
484
+
485
+ assert_equal 3, h['key 3 with spaces']['yet another key with spaces']
486
+ assert_nil h['key 3 with spaces'][:yet_another_key_with_spaces]
487
+ assert_nil h[:key_3_with_spaces]
488
+
489
+ h.underscore = true
490
+ assert_equal true, h[:key_with_spaces].underscore?
491
+ assert_equal 1, h[:key_with_spaces][:another_key_with_spaces]
492
+ assert_equal true, h[:key_2_with_spaces].underscore?
493
+ assert_equal 2, h[:key_2_with_spaces][:yet_another_key_with_spaces]
494
+ assert_equal true, h[:key_3_with_spaces].underscore?
495
+ assert_equal 3, h[:key_3_with_spaces][:yet_another_key_with_spaces]
496
+
497
+ h.underscore = false
498
+ assert_nil h[:key_with_spaces]
499
+ assert_nil h[:key_2_with_spaces]
500
+ assert_nil h[:key_3_with_spaces]
501
+
502
+ assert_equal false, h.underscore?
503
+ assert_equal true, h['key 2 with spaces'].underscore?
504
+ assert_equal true, h['key 3 with spaces'].underscore?
505
+ end
506
+
507
+ def test_underscore_clash
508
+ h = InsensitiveHash.new
509
+ h['hello world'] = 1
510
+ h['HELLO_world'] = 2
511
+ assert_equal 1, h['HELLO WORLD']
512
+ assert_equal 2, h['HELLO_WORLD']
513
+ assert_equal ['hello world', 'HELLO_world'], h.keys
514
+
515
+ assert_raise(InsensitiveHash::KeyClashError) { h.underscore = true }
516
+ h.delete('hello world')
517
+ h.underscore = true
518
+
519
+ assert_equal ['HELLO_world'], h.keys
520
+ assert_equal 2, h['HELLO WORLD']
521
+ assert_equal 2, h[:HELLO_WORLD]
522
+ end
523
+
524
+ def test_unset_underscore
525
+ h = InsensitiveHash.new
526
+ h.underscore = true
527
+ h[:hello_world] = 1
528
+ h.underscore = false
529
+ h['hello world'] = 2
530
+
531
+ assert_equal [:hello_world, 'hello world'], h.keys
532
+ assert_equal 2, h['Hello World']
533
+ end
281
534
  end
282
535
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: insensitive_hash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-10 00:00:00.000000000 Z
12
+ date: 2012-02-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: test-unit
16
- requirement: &2152981220 !ruby/object:Gem::Requirement
16
+ requirement: &2152875840 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: 2.3.0
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *2152981220
24
+ version_requirements: *2152875840
25
25
  description: Hash with case-insensitive, Symbol/String-indifferent key access
26
26
  email:
27
27
  - junegunn.c@gmail.com