insensitive_hash 0.2.1 → 0.2.2

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/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