configliere 0.3.4 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.document +3 -0
  2. data/.watchr +20 -0
  3. data/CHANGELOG.textile +99 -3
  4. data/Gemfile +26 -0
  5. data/Gemfile.lock +54 -0
  6. data/README.textile +162 -138
  7. data/Rakefile +30 -21
  8. data/VERSION +1 -1
  9. data/bin/configliere +77 -77
  10. data/bin/configliere-decrypt +85 -0
  11. data/bin/configliere-delete +85 -0
  12. data/bin/configliere-dump +85 -0
  13. data/bin/configliere-encrypt +85 -0
  14. data/bin/configliere-list +85 -0
  15. data/bin/configliere-set +85 -0
  16. data/configliere.gemspec +53 -23
  17. data/examples/config_block_script.rb +9 -2
  18. data/examples/encrypted_script.rb +28 -16
  19. data/examples/env_var_script.rb +2 -2
  20. data/examples/help_message_demo.rb +16 -0
  21. data/examples/independent_config.rb +28 -0
  22. data/examples/prompt.rb +23 -0
  23. data/examples/simple_script.rb +28 -15
  24. data/examples/simple_script.yaml +1 -1
  25. data/lib/configliere.rb +22 -24
  26. data/lib/configliere/commandline.rb +135 -116
  27. data/lib/configliere/commands.rb +38 -54
  28. data/lib/configliere/config_block.rb +4 -2
  29. data/lib/configliere/config_file.rb +30 -52
  30. data/lib/configliere/crypter.rb +8 -5
  31. data/lib/configliere/deep_hash.rb +368 -0
  32. data/lib/configliere/define.rb +83 -89
  33. data/lib/configliere/encrypted.rb +17 -18
  34. data/lib/configliere/env_var.rb +5 -7
  35. data/lib/configliere/param.rb +37 -64
  36. data/lib/configliere/prompt.rb +23 -0
  37. data/spec/configliere/commandline_spec.rb +156 -57
  38. data/spec/configliere/commands_spec.rb +75 -30
  39. data/spec/configliere/config_block_spec.rb +10 -1
  40. data/spec/configliere/config_file_spec.rb +83 -55
  41. data/spec/configliere/crypter_spec.rb +3 -2
  42. data/spec/configliere/deep_hash_spec.rb +401 -0
  43. data/spec/configliere/define_spec.rb +121 -42
  44. data/spec/configliere/encrypted_spec.rb +53 -20
  45. data/spec/configliere/env_var_spec.rb +24 -4
  46. data/spec/configliere/param_spec.rb +25 -27
  47. data/spec/configliere/prompt_spec.rb +50 -0
  48. data/spec/configliere_spec.rb +3 -9
  49. data/spec/spec_helper.rb +17 -6
  50. metadata +110 -35
  51. data/lib/configliere/core_ext.rb +0 -2
  52. data/lib/configliere/core_ext/blank.rb +0 -93
  53. data/lib/configliere/core_ext/hash.rb +0 -108
  54. data/lib/configliere/core_ext/sash.rb +0 -170
  55. data/spec/configliere/core_ext/hash_spec.rb +0 -78
  56. data/spec/configliere/core_ext/sash_spec.rb +0 -312
@@ -15,11 +15,20 @@ describe "Configliere::ConfigBlock" do
15
15
  @config.resolve!
16
16
  end
17
17
  it 'resolves blocks last' do
18
- Configliere.use :config_block, :define, :encrypted
18
+ Configliere.use :config_block, :encrypted
19
19
  @config.should_receive(:resolve_types!).ordered
20
20
  @config.should_receive(:resolve_finally_blocks!).ordered
21
21
  @config.resolve!
22
22
  end
23
+
24
+ describe '#resolve!' do
25
+ it 'calls super and returns self' do
26
+ Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end
27
+ @config.should_receive(:dummy)
28
+ @config.resolve!.should equal(@config)
29
+ Configliere::ParamParent.class_eval do def resolve!() self ; end ; end
30
+ end
31
+ end
23
32
  end
24
33
 
25
34
  end
@@ -1,90 +1,118 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
- Configliere.use :config_file
3
2
 
4
- describe "Configliere::ConfigFile" do
3
+ describe Configliere::ConfigFile do
5
4
  before do
6
- @config = Configliere.new :my_param => 'val'
5
+ @config = Configliere::Param.new :my_param => 'default_val', :also_a_param => true
7
6
  FileUtils.stub! :mkdir_p
8
7
  end
9
8
 
10
- describe 'loads a config file' do
9
+ it 'is used by default' do
10
+ @config.should respond_to(:read)
11
+ end
12
+
13
+ describe 'loading a config file' do
11
14
  before do
12
- @config = Configliere::Param.new :my_param => 'val'
13
- @fake_file = '{ :my_param: val }'
15
+ @fake_file = '{ :my_param: val_from_file }'
14
16
  end
17
+
15
18
  describe 'successfully' do
16
- it 'loads a symbol name from the default config file' do
17
- File.should_receive(:open).with(Configliere::DEFAULT_CONFIG_FILE).and_return(':my_settings: ' + @fake_file)
18
- @config.read :my_settings
19
- end
20
- it 'loads an absolute pathname from the given file' do
19
+ it 'with an absolute pathname uses it directly' do
21
20
  File.should_receive(:open).with('/fake/path.yaml').and_return(@fake_file)
22
21
  @config.read '/fake/path.yaml'
23
22
  end
24
- it 'loads a simple filename from the default config dir' do
23
+ it 'with a simple filename, references it to the default config dir' do
25
24
  File.should_receive(:open).with(Configliere::DEFAULT_CONFIG_DIR + '/file.yaml').and_return(@fake_file)
26
25
  @config.read 'file.yaml'
27
26
  end
28
- it 'loads an absolute pathname from the given file' do
29
- File.should_receive(:open).with('/fake/path.yaml').and_return(@fake_file)
30
- @config.read '/fake/path.yaml'
27
+ it 'returns the config object for chaining' do
28
+ File.should_receive(:open).with(Configliere::DEFAULT_CONFIG_DIR + '/file.yaml').and_return(@fake_file)
29
+ @config.defaults :also_a_param => true
30
+ @config.read('file.yaml').should == { :my_param => 'val_from_file', :also_a_param => true }
31
31
  end
32
32
  after do
33
- @config[:my_param].should == 'val'
33
+ @config[:my_param].should == 'val_from_file'
34
34
  end
35
35
  end
36
- describe 'in edge cases' do
37
- it 'handles a file not found' do
38
- @config = Configliere::Param.new
39
- File.stub(:open).and_raise(Errno::ENOENT)
40
- @config.read('nonexistent_file.yaml').should == {}
41
- end
36
+
37
+ it 'no longer provides a default config file' do
38
+ lambda{ @config.read(:my_settings) }.should raise_error(Configliere::DeprecatedError)
39
+ defined?(Configliere::DEFAULT_CONFIG_FILE).should_not be_true
42
40
  end
43
- end
44
41
 
45
- describe 'saves to a config file' do
46
- describe 'successfully' do
47
- it 'saves a symbol name to the default config file' do
48
- Configliere::ConfigFile.should_receive(:merge_into_yaml_file).
49
- with(Configliere::DEFAULT_CONFIG_FILE, :my_settings, { :my_param => 'val'})
50
- @config.save! :my_settings
42
+ it 'warns but does not fail if the file is missing' do
43
+ @config = Configliere::Param.new
44
+ File.stub(:open).and_raise(Errno::ENOENT)
45
+ @config.should_receive(:warn).with("Loading empty configliere settings file #{Configliere::DEFAULT_CONFIG_DIR}/nonexistent_file.yaml")
46
+ @config.read('nonexistent_file.yaml').should == {}
47
+ end
48
+
49
+
50
+ describe 'with an environment scope' do
51
+ before do
52
+ @config.defaults :reload => :unset
53
+ @hash_with_subenvs = { :development => { :reload => true }, :production => { :reload => false }}
54
+ File.stub :open
55
+ YAML.should_receive(:load).and_return(@hash_with_subenvs)
51
56
  end
52
- it 'saves a pathname to the given file' do
53
- fake_file = StringIO.new('', 'w')
54
- File.should_receive(:open).with('/fake/path.yaml', 'w').and_yield(fake_file)
55
- fake_file.should_receive(:<<).with("--- \n:my_param: val\n")
56
- @config.save! '/fake/path.yaml'
57
+
58
+ it 'slices out a subhash given by :env' do
59
+ @config.read('f', :env => :development)
60
+ @config.should == { :reload => true, :my_param => 'default_val', :also_a_param => true }
61
+ end
62
+
63
+ it 'slices out a different subhash with a different :env' do
64
+ @config.read('f', :env => :production)
65
+ @config.should == { :reload => false, :my_param => 'default_val', :also_a_param => true }
66
+ end
67
+
68
+ it 'does no slicing without the :env option' do
69
+ @config.read('f')
70
+ @config.should == { :development => { :reload => true }, :production => { :reload => false }, :reload => :unset, :my_param => 'default_val', :also_a_param => true }
71
+ end
72
+
73
+ it 'has no effect if the key given by :env option is absent' do
74
+ @config.read('f', :env => :foobar)
75
+ @config.should == { :reload => :unset, :my_param => 'default_val', :also_a_param => true }
57
76
  end
58
- it 'ensures the directory exists' do
59
- fake_file = StringIO.new('', 'w')
60
- File.stub!(:open).with('/fake/path.yaml', 'w').and_yield(fake_file)
61
- FileUtils.should_receive(:mkdir_p).with('/fake')
62
- @config.save! '/fake/path.yaml'
77
+
78
+ it 'does not type convert the :env option' do
79
+ @hash_with_subenvs['john_woo'] = { :reload => :sideways }
80
+ @config.read('f', :env => 'john_woo')
81
+ @config.should == { :reload => :sideways, :my_param => 'default_val', :also_a_param => true }
63
82
  end
64
83
  end
65
84
  end
66
85
 
67
- describe 'merge_into_yaml_file' do
68
- it 'merges, leaving existing values put' do
69
- fake_file = StringIO.new(":my_settings: { :my_param: orig_val }\n:other_settings: { :that_param: other_val }", 'r+')
70
- File.should_receive(:open).with('/fake/path.yaml').and_return(fake_file)
86
+
87
+ describe 'saves to a config file' do
88
+ it 'with an absolute pathname, as given' do
89
+ fake_file = StringIO.new('', 'w')
71
90
  File.should_receive(:open).with('/fake/path.yaml', 'w').and_yield(fake_file)
72
- mock_dump = 'mock_dump'
73
- YAML.should_receive(:dump).with({ :my_settings => { :my_param => 'new_val'}, :other_settings => { :that_param => 'other_val'}}).and_return(mock_dump)
74
- fake_file.should_receive(:<<).with(mock_dump)
75
- Configliere::ConfigFile.merge_into_yaml_file '/fake/path.yaml', :my_settings, :my_param => 'new_val'
91
+ fake_file.should_receive(:<<).with("--- \n:my_param: default_val\n:also_a_param: true\n")
92
+ @config.save! '/fake/path.yaml'
76
93
  end
77
- end
78
94
 
79
- describe 'maps handles to filenames' do
80
- it 'loads a symbol name from the default config file' do
81
- @config.send(:filename_for_handle, :my_settings).should == Configliere::DEFAULT_CONFIG_FILE
95
+ it 'with a simple pathname, in the default config dir' do
96
+ fake_file = StringIO.new('', 'w')
97
+ File.should_receive(:open).with(Configliere::DEFAULT_CONFIG_DIR + '/file.yaml', 'w').and_yield(fake_file)
98
+ fake_file.should_receive(:<<).with("--- \n:my_param: default_val\n:also_a_param: true\n")
99
+ @config.save! 'file.yaml'
82
100
  end
83
- it 'loads an absolute pathname from the given file' do
84
- @config.send(:filename_for_handle, '/fake/path.yaml').should == '/fake/path.yaml'
101
+
102
+ it 'and ensures the directory exists' do
103
+ fake_file = StringIO.new('', 'w')
104
+ File.stub!(:open).with('/fake/path.yaml', 'w').and_yield(fake_file)
105
+ FileUtils.should_receive(:mkdir_p).with('/fake')
106
+ @config.save! '/fake/path.yaml'
85
107
  end
86
- it 'loads a simple filename from the default config dir' do
87
- @config.send(:filename_for_handle, 'file.yaml').should == Configliere::DEFAULT_CONFIG_DIR + '/file.yaml'
108
+ end
109
+
110
+ describe '#resolve!' do
111
+ it 'calls super and returns self' do
112
+ Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end
113
+ @config.should_receive(:dummy)
114
+ @config.resolve!.should equal(@config)
115
+ Configliere::ParamParent.class_eval do def resolve!() self ; end ; end
88
116
  end
89
117
  end
90
118
 
@@ -3,12 +3,13 @@ require 'configliere/crypter'
3
3
  include Configliere
4
4
 
5
5
  describe "Crypter" do
6
- ENCRYPTED_FOO_VAL = "q\317\201\247\230\314P\021\305\n\363\315d\207\345y\346\255\a\202\006i\245\343T\203e\327a\316\245\313"
6
+ ENCRYPTED_FOO_VAL = "cc+Bp5jMUBHFCvPNZIfleeatB4IGaaXjVINl12HOpcs=\n"
7
+ FOO_VAL_IV = Base64.decode64(ENCRYPTED_FOO_VAL)[0..15]
7
8
  it "encrypts" do
8
9
  # Force the same initialization vector as used to prepare the test value
9
10
  @cipher = Crypter.send(:new_cipher, :encrypt, 'sekrit')
10
11
  Crypter.should_receive(:new_cipher).and_return(@cipher)
11
- @cipher.should_receive(:random_iv).and_return ENCRYPTED_FOO_VAL[0..15]
12
+ @cipher.should_receive(:random_iv).and_return FOO_VAL_IV
12
13
  # OK so do the test now.
13
14
  Crypter.encrypt('foo_val', 'sekrit').should == ENCRYPTED_FOO_VAL
14
15
  end
@@ -0,0 +1,401 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ class AwesomeHash < DeepHash ; end
4
+
5
+ describe DeepHash do
6
+ before(:each) do
7
+ @deep_hash = DeepHash.new.merge!({ :nested_1 => { :nested_2 => { :leaf_3 => "val3" }, :leaf_2 => "val2" }, :leaf_at_top => 'val1b' })
8
+ @hash = { "str_key" => "strk_val", :sym_key => "symk_val"}
9
+ @sub = AwesomeHash.new("str_key" => "strk_val", :sym_key => "symk_val")
10
+ end
11
+
12
+ describe "#initialize" do
13
+ it 'adopts a Hash when given' do
14
+ deep_hash = DeepHash.new(@hash)
15
+ @hash.each{|k,v| deep_hash[k].should == v }
16
+ deep_hash.keys.any?{|key| key.is_a?(String) }.should be_false
17
+ end
18
+
19
+ it 'converts all pure Hash values into DeepHashes if param is a Hash' do
20
+ deep_hash = DeepHash.new :sym_key => @hash
21
+ deep_hash[:sym_key].should be_an_instance_of(DeepHash)
22
+ # sanity check
23
+ deep_hash[:sym_key][:sym_key].should == "symk_val"
24
+ end
25
+
26
+ it 'does not convert Hash subclass values into DeepHashes' do
27
+ deep_hash = DeepHash.new :sub => @sub
28
+ deep_hash[:sub].should be_an_instance_of(AwesomeHash)
29
+ end
30
+
31
+ it 'converts all value items if value is an Array' do
32
+ deep_hash = DeepHash.new :arry => { :sym_key => [@hash] }
33
+ deep_hash[:arry].should be_an_instance_of(DeepHash)
34
+ # sanity check
35
+ deep_hash[:arry][:sym_key].first[:sym_key].should == "symk_val"
36
+ end
37
+
38
+ it 'delegates to superclass constructor if param is not a Hash' do
39
+ deep_hash = DeepHash.new("dash berlin")
40
+
41
+ deep_hash["unexisting key"].should == "dash berlin"
42
+ end
43
+ end # describe "#initialize"
44
+
45
+ describe "#update" do
46
+ it 'converts all keys into symbols when param is a Hash' do
47
+ deep_hash = DeepHash.new(@hash)
48
+ deep_hash.update("starry" => "night")
49
+
50
+ deep_hash.keys.any?{|key| key.is_a?(String) }.should be_false
51
+ end
52
+
53
+ it 'converts all Hash values into DeepHashes if param is a Hash' do
54
+ deep_hash = DeepHash.new :hash => @hash
55
+ deep_hash.update(:hash => { :sym_key => "is buggy in Ruby 1.8.6" })
56
+
57
+ deep_hash[:hash].should be_an_instance_of(DeepHash)
58
+ end
59
+ end # describe "#update"
60
+
61
+ describe '#[]=' do
62
+ it 'symbolizes keys' do
63
+ @deep_hash['leaf_at_top'] = :fedora
64
+ @deep_hash['new'] = :unseen
65
+ @deep_hash.should == {:nested_1 => {:nested_2 => {:leaf_3 => "val3"}, :leaf_2 => "val2"}, :leaf_at_top => :fedora, :new => :unseen}
66
+ end
67
+ it 'deep-sets dotted vals, replacing values' do
68
+ @deep_hash['moon.man'] = :cheesy
69
+ @deep_hash[:moon][:man].should == :cheesy
70
+ end
71
+ it 'deep-sets dotted vals, creating new values' do
72
+ @deep_hash['moon.cheese.type'] = :tilsit
73
+ @deep_hash[:moon][:cheese][:type].should == :tilsit
74
+ end
75
+ it 'deep-sets dotted vals, auto-vivifying intermediate hashes' do
76
+ @deep_hash['this.that.the_other'] = :fuhgeddaboudit
77
+ @deep_hash[:this][:that][:the_other].should == :fuhgeddaboudit
78
+ end
79
+ it 'converts all Hash value into DeepHash' do
80
+ deep_hash = DeepHash.new :hash => @hash
81
+ deep_hash[:hash] = { :sym_key => "is buggy in Ruby 1.8.6" }
82
+ deep_hash[:hash].should be_an_instance_of(DeepHash)
83
+ end
84
+ end
85
+
86
+ describe '#[]' do
87
+ it 'deep-gets dotted vals' do
88
+ hsh = { :hat => :cat, :basket => :lotion, :moon => { :man => :smiling, :cheese => {:type => :tilsit} } }
89
+ @deep_hash = Configliere::Param.new hsh.dup
90
+ @deep_hash['moon.man'].should == :smiling
91
+ @deep_hash['moon.cheese.type'].should == :tilsit
92
+ @deep_hash['moon.cheese.smell'].should be_nil
93
+ @deep_hash['moon.non.existent.interim.values'].should be_nil
94
+ @deep_hash['moon.non'].should be_nil
95
+ if (RUBY_VERSION >= '1.9') then lambda{ @deep_hash['hat.cat'] }.should raise_error(TypeError)
96
+ else lambda{ @deep_hash['hat.cat'] }.should raise_error(NoMethodError, 'undefined method `[]\' for :cat:Symbol') end
97
+ @deep_hash.should == hsh # shouldn't change from reading (specifically, shouldn't autovivify)
98
+ end
99
+ end
100
+
101
+ def arrays_should_be_equal arr1, arr2
102
+ arr1.sort_by{|s| s.to_s }.should == arr2.sort_by{|s| s.to_s }
103
+ end
104
+
105
+ describe "#to_hash" do
106
+ it 'returns instance of Hash' do
107
+ DeepHash.new(@hash).to_hash.should be_an_instance_of(Hash)
108
+ end
109
+
110
+ it 'preserves keys' do
111
+ deep_hash = DeepHash.new(@hash)
112
+ converted = deep_hash.to_hash
113
+ arrays_should_be_equal deep_hash.keys, converted.keys
114
+ end
115
+
116
+ it 'preserves value' do
117
+ deep_hash = DeepHash.new(@hash)
118
+ converted = deep_hash.to_hash
119
+ arrays_should_be_equal deep_hash.values, converted.values
120
+ end
121
+ end
122
+
123
+ describe '#compact' do
124
+ it 'removes nils but not empties or falsehoods' do
125
+ DeepHash.new({ 1 => nil }).compact.should == {}
126
+ DeepHash.new({ 1 => nil, 2 => false, 3 => {}, 4 => "", :remains => true }).compact!.should == { 2 => false, 3 => {}, 4 => "", :remains => true }
127
+ end
128
+
129
+ it 'leaves original alone' do
130
+ deep_hash = DeepHash.new({ 1 => nil, :remains => true })
131
+ deep_hash.compact.should == { :remains => true }
132
+ deep_hash.should == { 1 => nil, :remains => true }
133
+ end
134
+ end
135
+
136
+ describe '#compact!' do
137
+ it 'removes nils but not empties or falsehoods' do
138
+ DeepHash.new({ 1 => nil}).compact!.should == {}
139
+ DeepHash.new({ 1 => nil, 2 => false, 3 => {}, 4 => "", :remains => true }).compact!.should == { 2 => false, 3 => {}, 4 => "", :remains => true }
140
+ end
141
+
142
+ it 'modifies in-place' do
143
+ deep_hash = DeepHash.new({ 1 => nil, :remains => true })
144
+ deep_hash.compact!.should == { :remains => true }
145
+ deep_hash.should == { :remains => true }
146
+ end
147
+ end
148
+
149
+ describe '#slice' do
150
+ before do
151
+ @deep_hash = DeepHash.new({ :a => 'x', :b => 'y', :c => 10 })
152
+ end
153
+
154
+ it 'returns a new hash with only the given keys' do
155
+ @deep_hash.slice(:a, :b).should == { :a => 'x', :b => 'y' }
156
+ @deep_hash.should == { :a => 'x', :b => 'y', :c => 10 }
157
+ end
158
+
159
+ it 'with bang replaces the hash with only the given keys' do
160
+ @deep_hash.slice!(:a, :b).should == { :c => 10 }
161
+ @deep_hash.should == { :a => 'x', :b => 'y' }
162
+ end
163
+
164
+ it 'ignores an array key' do
165
+ @deep_hash.slice([:a, :b], :c).should == { :c => 10 }
166
+ @deep_hash.should == { :a => 'x', :b => 'y', :c => 10 }
167
+ end
168
+
169
+ it 'with bang ignores an array key' do
170
+ @deep_hash.slice!([:a, :b], :c).should == { :a => 'x', :b => 'y' }
171
+ @deep_hash.should == { :c => 10 }
172
+ end
173
+
174
+ it 'uses splatted keys individually' do
175
+ @deep_hash.slice(*[:a, :b]).should == { :a => 'x', :b => 'y' }
176
+ @deep_hash.should == { :a => 'x', :b => 'y', :c => 10 }
177
+ end
178
+
179
+ it 'with bank uses splatted keys individually' do
180
+ @deep_hash.slice!(*[:a, :b]).should == { :c => 10 }
181
+ @deep_hash.should == { :a => 'x', :b => 'y' }
182
+ end
183
+ end
184
+
185
+ describe '#extract' do
186
+ before do
187
+ @deep_hash = DeepHash.new({ :a => 'x', :b => 'y', :c => 10 })
188
+ end
189
+
190
+ it 'replaces the hash with only the given keys' do
191
+ @deep_hash.extract!(:a, :b).should == { :a => 'x', :b => 'y' }
192
+ @deep_hash.should == { :c => 10 }
193
+ end
194
+
195
+ it 'leaves the hash empty if all keys are gone' do
196
+ @deep_hash.extract!(:a, :b, :c).should == { :a => 'x', :b => 'y', :c => 10 }
197
+ @deep_hash.should == {}
198
+ end
199
+
200
+ it 'gets values for all given keys even if missing' do
201
+ @deep_hash.extract!(:bob, :c).should == { :bob => nil, :c => 10 }
202
+ @deep_hash.should == { :a => 'x', :b => 'y' }
203
+ end
204
+
205
+ it 'is OK when empty' do
206
+ DeepHash.new.slice!(:a, :b, :c).should == {}
207
+ end
208
+
209
+ it 'returns an instance of the same class' do
210
+ @deep_hash.slice(:a).should be_a(DeepHash)
211
+ end
212
+ end
213
+
214
+ describe 'assert_valid_keys' do
215
+ before do
216
+ @deep_hash = DeepHash.new({ :failure => "stuff", :funny => "business" })
217
+ end
218
+
219
+ it 'is true and does not raise when valid' do
220
+ @deep_hash.assert_valid_keys([ :failure, :funny ]).should be_nil
221
+ @deep_hash.assert_valid_keys(:failure, :funny).should be_nil
222
+ end
223
+
224
+ it 'fails when invalid' do
225
+ @deep_hash[:failore] = @deep_hash.delete(:failure)
226
+ lambda{ @deep_hash.assert_valid_keys([ :failure, :funny ]) }.should raise_error(ArgumentError, "Unknown key(s): failore")
227
+ lambda{ @deep_hash.assert_valid_keys(:failure, :funny) }.should raise_error(ArgumentError, "Unknown key(s): failore")
228
+ end
229
+ end
230
+
231
+ describe "#delete" do
232
+ it 'converts Symbol key into String before deleting' do
233
+ deep_hash = DeepHash.new(@hash)
234
+
235
+ deep_hash.delete(:sym_key)
236
+ deep_hash.key?("hash").should be_false
237
+ end
238
+
239
+ it 'works with String keys as well' do
240
+ deep_hash = DeepHash.new(@hash)
241
+
242
+ deep_hash.delete("str_key")
243
+ deep_hash.key?("str_key").should be_false
244
+ end
245
+ end
246
+
247
+ describe "#merge" do
248
+ before(:each) do
249
+ @deep_hash = DeepHash.new(@hash).merge(:no => "in between")
250
+ end
251
+
252
+ it 'returns instance of DeepHash' do
253
+ @deep_hash.should be_an_instance_of(DeepHash)
254
+ end
255
+
256
+ it 'merges in give Hash' do
257
+ @deep_hash["no"].should == "in between"
258
+ end
259
+ end
260
+
261
+ describe "#fetch" do
262
+ before(:each) do
263
+ @deep_hash = DeepHash.new(@hash).merge(:no => "in between")
264
+ end
265
+
266
+ it 'converts key before fetching' do
267
+ @deep_hash.fetch("no").should == "in between"
268
+ end
269
+
270
+ it 'returns alternative value if key lookup fails' do
271
+ @deep_hash.fetch("flying", "screwdriver").should == "screwdriver"
272
+ end
273
+ end
274
+
275
+
276
+ describe "#values_at" do
277
+ before(:each) do
278
+ @deep_hash = DeepHash.new(@hash).merge(:no => "in between")
279
+ end
280
+
281
+ it 'is indifferent to whether keys are strings or symbols' do
282
+ @deep_hash.values_at("sym_key", :str_key, :no).should == ["symk_val", "strk_val", "in between"]
283
+ end
284
+ end
285
+
286
+ describe "#symbolize_keys" do
287
+ it 'with bang returns the deep_hash itself' do
288
+ deep_hash = DeepHash.new(@hash)
289
+ deep_hash.symbolize_keys!.object_id.should == deep_hash.object_id
290
+ end
291
+
292
+ it 'returns a dup of itself' do
293
+ deep_hash = DeepHash.new(@hash)
294
+ deep_hash.symbolize_keys.should == deep_hash
295
+ end
296
+ end
297
+
298
+ describe "#reverse_merge" do
299
+ before do
300
+ @defaults = { :a => "x", :b => "y", :c => 10 }.freeze
301
+ @deep_hash = DeepHash.new({ :a => 1, :b => 2 })
302
+ end
303
+
304
+ it 'merges defaults into options, creating a new hash' do
305
+ @deep_hash.reverse_merge(@defaults).should == { :a => 1, :b => 2, :c => 10 }
306
+ @deep_hash.should == { :a => 1, :b => 2 }
307
+ end
308
+
309
+ it 'with bang merges! defaults into options, replacing options' do
310
+ @deep_hash.reverse_merge!(@defaults).should == { :a => 1, :b => 2, :c => 10 }
311
+ @deep_hash.should == { :a => 1, :b => 2, :c => 10 }
312
+ end
313
+ end
314
+
315
+ describe "#deep_merge!" do
316
+ it "merges two subhashes when they share a key" do
317
+ @deep_hash.deep_merge!(:nested_1 => { :nested_2 => { :leaf_3_also => "val3a" } })
318
+ @deep_hash.should == { :nested_1 => { :nested_2 => { :leaf_3_also => "val3a", :leaf_3 => "val3" }, :leaf_2 => "val2" }, :leaf_at_top => 'val1b' }
319
+ end
320
+ it "merges two subhashes when they share a symbolized key" do
321
+ @deep_hash.deep_merge!(:nested_1 => { "nested_2" => { "leaf_3_also" => "val3a" } })
322
+ @deep_hash.should == { :nested_1 => { :nested_2 => { :leaf_3_also => "val3a", :leaf_3 => "val3" }, :leaf_2 => "val2" }, :leaf_at_top => "val1b" }
323
+ end
324
+ it "preserves values in the original" do
325
+ @deep_hash.deep_merge! :other_key => "other_val"
326
+ @deep_hash[:nested_1][:leaf_2].should == "val2"
327
+ @deep_hash[:other_key].should == "other_val"
328
+ end
329
+ # it "converts all Hash values into DeepHashes if param is a Hash" do
330
+ # @deep_hash.deep_merge!({:nested_1 => { :nested_2 => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }})
331
+ # @deep_hash[:nested_1].should be_an_instance_of(DeepHash)
332
+ # @deep_hash[:nested_1][:nested_2].should be_an_instance_of(DeepHash)
333
+ # @deep_hash[:other1].should be_an_instance_of(DeepHash)
334
+ # end
335
+ it "replaces values from the given DeepHash" do
336
+ @deep_hash.deep_merge!(:nested_1 => { :nested_2 => { :leaf_3 => "new_val3" }, :leaf_2 => { "other2" => "other_val2" }})
337
+ @deep_hash[:nested_1][:nested_2][:leaf_3].should == 'new_val3'
338
+ @deep_hash[:nested_1][:leaf_2].should == { :other2 => "other_val2" }
339
+ end
340
+
341
+ it "replaces values from the given DeepHash" do
342
+ @deep_hash.deep_merge!(:nested_1 => { :nested_2 => { :leaf_3 => [] }, :leaf_2 => nil }, :leaf_at_top => '')
343
+ @deep_hash[:nested_1][:nested_2][:leaf_3].should == []
344
+ @deep_hash[:nested_1][:leaf_2].should == "val2"
345
+ @deep_hash[:leaf_at_top].should == ""
346
+ end
347
+ end
348
+
349
+ describe "#deep_set" do
350
+ it 'should set a new value (single arg)' do
351
+ @deep_hash.deep_set :new_key, 'new_val'
352
+ @deep_hash[:new_key].should == 'new_val'
353
+ end
354
+ it 'should set a new value (multiple args)' do
355
+ @deep_hash.deep_set :nested_1, :nested_2, :new_key, 'new_val'
356
+ @deep_hash[:nested_1][:nested_2][:new_key].should == 'new_val'
357
+ end
358
+ it 'should replace an existing value (single arg)' do
359
+ @deep_hash.deep_set :leaf_at_top, 'new_val'
360
+ @deep_hash[:leaf_at_top].should == 'new_val'
361
+ end
362
+ it 'should replace an existing value (multiple args)' do
363
+ @deep_hash.deep_set :nested_1, :nested_2, 'new_val'
364
+ @deep_hash[:nested_1][:nested_2].should == 'new_val'
365
+ end
366
+ it 'should auto-vivify intermediate hashes' do
367
+ @deep_hash.deep_set :one, :two, :three, :four, 'new_val'
368
+ @deep_hash[:one][:two][:three][:four].should == 'new_val'
369
+ end
370
+ end
371
+
372
+ describe "#deep_delete" do
373
+ it 'should remove the key from the array (multiple args)' do
374
+ @deep_hash.deep_delete(:nested_1)
375
+ @deep_hash[:nested_1].should be_nil
376
+ @deep_hash.should == { :leaf_at_top => 'val1b'}
377
+ end
378
+ it 'should remove the key from the array (multiple args)' do
379
+ @deep_hash.deep_delete(:nested_1, :nested_2, :leaf_3)
380
+ @deep_hash[:nested_1][:nested_2][:leaf_3].should be_nil
381
+ @deep_hash.should == {:leaf_at_top => "val1b", :nested_1 => {:leaf_2 => "val2", :nested_2 => {}}}
382
+ end
383
+ it 'should return the value if present (single args)' do
384
+ returned_val = @deep_hash.deep_delete(:leaf_at_top)
385
+ returned_val.should == 'val1b'
386
+ end
387
+ it 'should return the value if present (multiple args)' do
388
+ returned_val = @deep_hash.deep_delete(:nested_1, :nested_2, :leaf_3)
389
+ returned_val.should == 'val3'
390
+ end
391
+ it 'should return nil if the key is absent (single arg)' do
392
+ returned_val = @deep_hash.deep_delete(:nested_1, :nested_2, :missing_key)
393
+ returned_val.should be_nil
394
+ end
395
+ it 'should return nil if the key is absent (multiple args)' do
396
+ returned_val = @deep_hash.deep_delete(:missing_key)
397
+ returned_val.should be_nil
398
+ end
399
+ end
400
+
401
+ end