configliere 0.3.4 → 0.4.4

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