diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2016-10-07 15:04:47 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2016-10-25 09:29:29 -0700 |
commit | 6536f15dcb53938fe9c77e7438b20fe6ac3cdffb (patch) | |
tree | e6f6a3b0f28914582e4be97c5ecb9e3b20a0e9c1 | |
parent | 10904d6cf42a2a0ed69757aca3e80f512bb89379 (diff) | |
download | chef-6536f15dcb53938fe9c77e7438b20fe6ac3cdffb.tar.gz |
remove breadcrumb state
Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
-rw-r--r-- | lib/chef/node.rb | 4 | ||||
-rw-r--r-- | lib/chef/node/attribute_collections.rb | 22 | ||||
-rw-r--r-- | lib/chef/node/common_api.rb | 6 | ||||
-rw-r--r-- | lib/chef/node/immutable_collections.rb | 6 | ||||
-rw-r--r-- | lib/chef/node/mixin/deep_merge_cache.rb | 11 | ||||
-rw-r--r-- | lib/chef/node/mixin/state_tracking.rb | 18 | ||||
-rw-r--r-- | spec/unit/node/vivid_mash_spec.rb | 111 |
7 files changed, 52 insertions, 126 deletions
diff --git a/lib/chef/node.rb b/lib/chef/node.rb index 212b1ced14..34a92d325b 100644 --- a/lib/chef/node.rb +++ b/lib/chef/node.rb @@ -197,7 +197,6 @@ class Chef # Set a normal attribute of this node, but auto-vivify any Mashes that # might be missing def normal - attributes.top_level_breadcrumb = nil attributes.normal end @@ -209,14 +208,12 @@ class Chef # Set a default of this node, but auto-vivify any Mashes that might # be missing def default - attributes.top_level_breadcrumb = nil attributes.default end # Set an override attribute of this node, but auto-vivify any Mashes that # might be missing def override - attributes.top_level_breadcrumb = nil attributes.override end @@ -237,7 +234,6 @@ class Chef end def automatic_attrs - attributes.top_level_breadcrumb = nil attributes.automatic end diff --git a/lib/chef/node/attribute_collections.rb b/lib/chef/node/attribute_collections.rb index be87c028cb..412fb52a48 100644 --- a/lib/chef/node/attribute_collections.rb +++ b/lib/chef/node/attribute_collections.rb @@ -64,13 +64,12 @@ class Chef MUTATOR_METHODS.each do |mutator| define_method(mutator) do |*args, &block| ret = super(*args, &block) - __root.reset_cache(__root.top_level_breadcrumb) + send_reset_cache ret end end - def initialize(data = [], root = self) - @__root ||= root + def initialize(data = []) super(data) map! { |e| convert_value(e) } end @@ -129,7 +128,6 @@ class Chef # object. MUTATOR_METHODS = [ :clear, - :delete, :delete_if, :keep_if, :merge!, @@ -143,21 +141,24 @@ class Chef # For all of the mutating methods on Mash, override them so that they # also invalidate the cached `merged_attributes` on the root Attribute # object. + + def delete(key, &block) + send_reset_cache(__path + [ key ]) + super + end + MUTATOR_METHODS.each do |mutator| define_method(mutator) do |*args, &block| - __root.reset_cache(__root.top_level_breadcrumb) + send_reset_cache super(*args, &block) end end - def initialize(data = {}, root = self) - puts caller unless root.class == Chef::Node::Attribute - @__root ||= root + def initialize(data = {}) super(data) end def [](key) - __root.top_level_breadcrumb ||= key value = super if !key?(key) value = self.class.new({}, __root) @@ -168,9 +169,8 @@ class Chef end def []=(key, value) - __root.top_level_breadcrumb ||= key ret = super - __root.reset_cache(__root.top_level_breadcrumb) + send_reset_cache(__path + [ key ]) ret end diff --git a/lib/chef/node/common_api.rb b/lib/chef/node/common_api.rb index 6aae880667..9bb83a5178 100644 --- a/lib/chef/node/common_api.rb +++ b/lib/chef/node/common_api.rb @@ -32,7 +32,6 @@ class Chef # - autovivifying / autoreplacing writer # - non-container-ey intermediate objects are replaced with hashes def write(*args, &block) - __root.top_level_breadcrumb = nil if respond_to?(:__root) value = block_given? ? yield : args.pop last = args.pop prev_memo = prev_key = nil @@ -56,7 +55,6 @@ class Chef # something that is not a container ("schema violation" issues). # def write!(*args, &block) - __root.top_level_breadcrumb = nil if respond_to?(:__root) value = block_given? ? yield : args.pop last = args.pop obj = args.inject(self) do |memo, key| @@ -71,7 +69,6 @@ class Chef # return true or false based on if the attribute exists def exist?(*path) - __root.top_level_breadcrumb = nil if respond_to?(:__root) path.inject(self) do |memo, key| return false unless valid_container?(memo, key) if memo.is_a?(Hash) @@ -103,7 +100,6 @@ class Chef # non-autovivifying reader that throws an exception if the attribute does not exist def read!(*path) raise Chef::Exceptions::NoSuchAttribute unless exist?(*path) - __root.top_level_breadcrumb = nil if respond_to?(:__root) path.inject(self) do |memo, key| memo[key] end @@ -112,10 +108,8 @@ class Chef # FIXME:(?) does anyone really like the autovivifying reader that we have and wants the same behavior? readers that write? ugh... def unlink(*path, last) - __root.top_level_breadcrumb = nil if respond_to?(:__root) hash = path.empty? ? self : read(*path) return nil unless hash.is_a?(Hash) || hash.is_a?(Array) - __root.top_level_breadcrumb ||= last hash.delete(last) end diff --git a/lib/chef/node/immutable_collections.rb b/lib/chef/node/immutable_collections.rb index 50ac8daf93..623290f287 100644 --- a/lib/chef/node/immutable_collections.rb +++ b/lib/chef/node/immutable_collections.rb @@ -52,8 +52,7 @@ class Chef alias :internal_push :<< private :internal_push - def initialize(array_data = [], root = self) - @__root = root + def initialize(array_data = []) array_data.each do |value| internal_push(immutablize(value)) end @@ -114,8 +113,7 @@ class Chef alias :internal_set :[]= private :internal_set - def initialize(mash_data = {}, root = self) - @__root = root + def initialize(mash_data = {}) mash_data.each do |key, value| internal_set(key, immutablize(value)) end diff --git a/lib/chef/node/mixin/deep_merge_cache.rb b/lib/chef/node/mixin/deep_merge_cache.rb index d6a2149c52..b18d6b10cb 100644 --- a/lib/chef/node/mixin/deep_merge_cache.rb +++ b/lib/chef/node/mixin/deep_merge_cache.rb @@ -19,14 +19,6 @@ class Chef class Node module Mixin module DeepMergeCache - # This is used to track the top level key as we descend through method chaining into - # a precedence level (e.g. node.default['foo']['bar']['baz']= results in 'foo' here). We - # need this so that when we hit the end of a method chain which results in a mutator method - # that we can invalidate the whole top-level deep merge cache for the top-level key. It is - # the responsibility of the accessor on the Chef::Node object to reset this to nil, and then - # the first VividMash#[] call can ||= and set this to the first key we encounter. - attr_accessor :top_level_breadcrumb - # Cache of deep merged values by top-level key. This is a simple hash which has keys that are the # top-level keys of the node object, and we save the computed deep-merge for that key here. There is # no cache of subtrees. @@ -36,7 +28,6 @@ class Chef @merged_attributes = nil @combined_override = nil @combined_default = nil - @top_level_breadcrumb = nil @deep_merge_cache = {} end @@ -46,7 +37,7 @@ class Chef # must invalidate the entire cache and re-deep-merge the entire node object. def reset_cache(path = nil) if path.nil? - @deep_merge_cache = {} + deep_merge_cache.clear else deep_merge_cache.delete(path.to_s) end diff --git a/lib/chef/node/mixin/state_tracking.rb b/lib/chef/node/mixin/state_tracking.rb index 7472d2a67d..2447de1c16 100644 --- a/lib/chef/node/mixin/state_tracking.rb +++ b/lib/chef/node/mixin/state_tracking.rb @@ -22,10 +22,14 @@ class Chef attr_reader :__path attr_reader :__root - def initialize(*args) - super(*args) - @__path ||= [] - @__root ||= self + NULL = Object.new + + def initialize(data = NULL, root = self) + # __path and __root must be nil when we call super so it knows + # to avoid resetting the cache on construction + data == NULL ? super() : super(data) + @__path = [] + @__root = root end def [](key) @@ -55,6 +59,12 @@ class Chef def __root=(root) @__root = root end + + private + + def send_reset_cache(path = __path) + __root.reset_cache(path.first) if !__root.nil? && __root.respond_to?(:reset_cache) && !path.nil? + end end end end diff --git a/spec/unit/node/vivid_mash_spec.rb b/spec/unit/node/vivid_mash_spec.rb index eb22929685..0955b6b1c3 100644 --- a/spec/unit/node/vivid_mash_spec.rb +++ b/spec/unit/node/vivid_mash_spec.rb @@ -19,37 +19,46 @@ require "spec_helper" require "chef/node/attribute_collections" describe Chef::Node::VividMash do - class Root - attr_accessor :top_level_breadcrumb - end - - let(:root) { Root.new } + let(:root) { instance_double(Chef::Node::Attribute) } let(:vivid) do - expect(root).to receive(:reset_cache).at_least(:once).with(nil) Chef::Node::VividMash.new( { "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }, root ) end - def with_breadcrumb(key) - expect(root).to receive(:top_level_breadcrumb=).with(nil).at_least(:once).and_call_original - expect(root).to receive(:top_level_breadcrumb=).with(key).at_least(:once).and_call_original + context "without a root node" do + let(:vivid) do + Chef::Node::VividMash.new( + { "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil } + ) + end + + it "sets the root to the root object" do + expect(vivid["one"]["two"].__root).to eql(vivid) + end + + it "does not send reset cache" do + # if we setup the expectation here then the object winds up responding to :reset_cache and then it fails... + # expect(vivid).not_to receive(:reset_cache) + # but even so we expect to blow up here with NoMethodError if we screw up and send :reset_cache to a root VividMash + vivid["one"]["foo"] = "bar" + end end context "#[]=" do it "deep converts values through arrays" do - allow(root).to receive(:reset_cache) - vivid[:foo] = [ { :bar => true } ] + expect(root).to receive(:reset_cache).with("foo") + vivid["foo"] = [ { :bar => true } ] expect(vivid["foo"].class).to eql(Chef::Node::AttrArray) expect(vivid["foo"][0].class).to eql(Chef::Node::VividMash) expect(vivid["foo"][0]["bar"]).to be true end it "deep converts values through nested arrays" do - allow(root).to receive(:reset_cache) - vivid[:foo] = [ [ { :bar => true } ] ] + expect(root).to receive(:reset_cache).with("foo") + vivid["foo"] = [ [ { :bar => true } ] ] expect(vivid["foo"].class).to eql(Chef::Node::AttrArray) expect(vivid["foo"][0].class).to eql(Chef::Node::AttrArray) expect(vivid["foo"][0][0].class).to eql(Chef::Node::VividMash) @@ -57,8 +66,8 @@ describe Chef::Node::VividMash do end it "deep converts values through hashes" do - allow(root).to receive(:reset_cache) - vivid[:foo] = { baz: { :bar => true } } + expect(root).to receive(:reset_cache).with("foo") + vivid["foo"] = { baz: { :bar => true } } expect(vivid["foo"]).to be_an_instance_of(Chef::Node::VividMash) expect(vivid["foo"]["baz"]).to be_an_instance_of(Chef::Node::VividMash) expect(vivid["foo"]["baz"]["bar"]).to be true @@ -67,182 +76,144 @@ describe Chef::Node::VividMash do context "#read" do before do - # vivify the vividmash, then we're read-only so the cache should never be cleared afterwards - vivid expect(root).not_to receive(:reset_cache) end it "reads hashes deeply" do - with_breadcrumb("one") expect(vivid.read("one", "two", "three")).to eql("four") end it "does not trainwreck when hitting hash keys that do not exist" do - with_breadcrumb("one") expect(vivid.read("one", "five", "six")).to eql(nil) end it "does not trainwreck when hitting an array with an out of bounds index" do - with_breadcrumb("array") expect(vivid.read("array", 5, "one")).to eql(nil) end it "does not trainwreck when hitting an array with a string key" do - with_breadcrumb("array") expect(vivid.read("array", "one", "two")).to eql(nil) end it "does not trainwreck when traversing a nil" do - with_breadcrumb("nil") expect(vivid.read("nil", "one", "two")).to eql(nil) end end context "#exist?" do before do - # vivify the vividmash, then we're read-only so the cache should never be cleared afterwards - vivid expect(root).not_to receive(:reset_cache) end it "true if there's a hash key there" do - with_breadcrumb("one") expect(vivid.exist?("one", "two", "three")).to be true end it "true for intermediate hashes" do - with_breadcrumb("one") expect(vivid.exist?("one")).to be true end it "true for arrays that exist" do - with_breadcrumb("array") expect(vivid.exist?("array", 1)).to be true end it "true when the value of the key is nil" do - with_breadcrumb("nil") expect(vivid.exist?("nil")).to be true end it "false when attributes don't exist" do - with_breadcrumb("one") expect(vivid.exist?("one", "five", "six")).to be false end it "false when traversing a non-container" do - with_breadcrumb("one") expect(vivid.exist?("one", "two", "three", "four")).to be false end it "false when an array index does not exist" do - with_breadcrumb("array") expect(vivid.exist?("array", 3)).to be false end it "false when traversing a nil" do - with_breadcrumb("nil") expect(vivid.exist?("nil", "foo", "bar")).to be false end end context "#read!" do before do - # vivify the vividmash, then we're read-only so the cache should never be cleared afterwards - vivid expect(root).not_to receive(:reset_cache) end it "reads hashes deeply" do - with_breadcrumb("one") expect(vivid.read!("one", "two", "three")).to eql("four") end it "reads arrays deeply" do - with_breadcrumb("array") expect(vivid.read!("array", 1)).to eql(1) end it "throws an exception when attributes do not exist" do - with_breadcrumb("one") expect { vivid.read!("one", "five", "six") }.to raise_error(Chef::Exceptions::NoSuchAttribute) end it "throws an exception when traversing a non-container" do - with_breadcrumb("one") expect { vivid.read!("one", "two", "three", "four") }.to raise_error(Chef::Exceptions::NoSuchAttribute) end it "throws an exception when an array element does not exist" do - with_breadcrumb("array") expect { vivid.read!("array", 3) }.to raise_error(Chef::Exceptions::NoSuchAttribute) end end context "#write" do - before do - vivid - expect(root).not_to receive(:reset_cache).with(nil) - end - it "should write into hashes" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") vivid.write("one", "five", "six") expect(vivid["one"]["five"]).to eql("six") end it "should deeply autovivify" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") vivid.write("one", "five", "six", "seven", "eight", "nine", "ten") expect(vivid["one"]["five"]["six"]["seven"]["eight"]["nine"]).to eql("ten") end it "should raise an exception if you overwrite an array with a hash" do - with_breadcrumb("array") expect(root).to receive(:reset_cache).at_least(:once).with("array") vivid.write("array", "five", "six") expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => { "five" => "six" }, "nil" => nil }) end it "should raise an exception if you traverse through an array with a hash" do - with_breadcrumb("array") expect(root).to receive(:reset_cache).at_least(:once).with("array") vivid.write("array", "five", "six", "seven") expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => { "five" => { "six" => "seven" } }, "nil" => nil }) end it "should raise an exception if you overwrite a string with a hash" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") vivid.write("one", "two", "three", "four", "five") expect(vivid).to eql({ "one" => { "two" => { "three" => { "four" => "five" } } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should raise an exception if you traverse through a string with a hash" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") vivid.write("one", "two", "three", "four", "five", "six") expect(vivid).to eql({ "one" => { "two" => { "three" => { "four" => { "five" => "six" } } } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should raise an exception if you overwrite a nil with a hash" do - with_breadcrumb("nil") expect(root).to receive(:reset_cache).at_least(:once).with("nil") vivid.write("nil", "one", "two") expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => { "one" => "two" } }) end it "should raise an exception if you traverse through a nil with a hash" do - with_breadcrumb("nil") expect(root).to receive(:reset_cache).at_least(:once).with("nil") vivid.write("nil", "one", "two", "three") expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => { "one" => { "two" => "three" } } }) end it "writes with a block" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") vivid.write("one", "five") { "six" } expect(vivid["one"]["five"]).to eql("six") @@ -250,69 +221,55 @@ describe Chef::Node::VividMash do end context "#write!" do - before do - vivid - expect(root).not_to receive(:reset_cache).with(nil) - end - it "should write into hashes" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") vivid.write!("one", "five", "six") expect(vivid["one"]["five"]).to eql("six") end it "should deeply autovivify" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") vivid.write!("one", "five", "six", "seven", "eight", "nine", "ten") expect(vivid["one"]["five"]["six"]["seven"]["eight"]["nine"]).to eql("ten") end it "should raise an exception if you overwrite an array with a hash" do - with_breadcrumb("array") expect(root).not_to receive(:reset_cache) expect { vivid.write!("array", "five", "six") }.to raise_error(Chef::Exceptions::AttributeTypeMismatch) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should raise an exception if you traverse through an array with a hash" do - with_breadcrumb("array") expect(root).not_to receive(:reset_cache) expect { vivid.write!("array", "five", "six", "seven") }.to raise_error(Chef::Exceptions::AttributeTypeMismatch) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should raise an exception if you overwrite a string with a hash" do - with_breadcrumb("one") expect(root).not_to receive(:reset_cache) expect { vivid.write!("one", "two", "three", "four", "five") }.to raise_error(Chef::Exceptions::AttributeTypeMismatch) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should raise an exception if you traverse through a string with a hash" do - with_breadcrumb("one") expect(root).not_to receive(:reset_cache) expect { vivid.write!("one", "two", "three", "four", "five", "six") }.to raise_error(Chef::Exceptions::AttributeTypeMismatch) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should raise an exception if you overwrite a nil with a hash" do - with_breadcrumb("nil") expect(root).not_to receive(:reset_cache) expect { vivid.write!("nil", "one", "two") }.to raise_error(Chef::Exceptions::AttributeTypeMismatch) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should raise an exception if you traverse through a nil with a hash" do - with_breadcrumb("nil") expect(root).not_to receive(:reset_cache) expect { vivid.write!("nil", "one", "two", "three") }.to raise_error(Chef::Exceptions::AttributeTypeMismatch) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "writes with a block" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") vivid.write!("one", "five") { "six" } expect(vivid["one"]["five"]).to eql("six") @@ -320,41 +277,31 @@ describe Chef::Node::VividMash do end context "#unlink" do - before do - vivid - expect(root).not_to receive(:reset_cache).with(nil) - end - it "should return nil if the keys don't already exist" do - expect(root).to receive(:top_level_breadcrumb=).with(nil).at_least(:once).and_call_original expect(root).not_to receive(:reset_cache) expect(vivid.unlink("five", "six", "seven", "eight")).to eql(nil) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should unlink hashes" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") expect( vivid.unlink("one") ).to eql({ "two" => { "three" => "four" } }) expect(vivid).to eql({ "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should unlink array elements" do - with_breadcrumb("array") expect(root).to receive(:reset_cache).at_least(:once).with("array") expect(vivid.unlink("array", 2)).to eql(2) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1 ], "nil" => nil }) end it "should unlink nil" do - with_breadcrumb("nil") expect(root).to receive(:reset_cache).at_least(:once).with("nil") expect(vivid.unlink("nil")).to eql(nil) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ] }) end it "should traverse a nil and safely do nothing" do - with_breadcrumb("nil") expect(root).not_to receive(:reset_cache) expect(vivid.unlink("nil", "foo")).to eql(nil) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) @@ -362,41 +309,31 @@ describe Chef::Node::VividMash do end context "#unlink!" do - before do - vivid - expect(root).not_to receive(:reset_cache).with(nil) - end - it "should raise an exception if the keys don't already exist" do - expect(root).to receive(:top_level_breadcrumb=).with(nil).at_least(:once).and_call_original expect(root).not_to receive(:reset_cache) expect { vivid.unlink!("five", "six", "seven", "eight") }.to raise_error(Chef::Exceptions::NoSuchAttribute) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should unlink! hashes" do - with_breadcrumb("one") expect(root).to receive(:reset_cache).at_least(:once).with("one") expect( vivid.unlink!("one") ).to eql({ "two" => { "three" => "four" } }) expect(vivid).to eql({ "array" => [ 0, 1, 2 ], "nil" => nil }) end it "should unlink! array elements" do - with_breadcrumb("array") expect(root).to receive(:reset_cache).at_least(:once).with("array") expect(vivid.unlink!("array", 2)).to eql(2) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1 ], "nil" => nil }) end it "should unlink! nil" do - with_breadcrumb("nil") expect(root).to receive(:reset_cache).at_least(:once).with("nil") expect(vivid.unlink!("nil")).to eql(nil) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ] }) end it "should raise an exception if it traverses a nil" do - with_breadcrumb("nil") expect(root).not_to receive(:reset_cache) expect { vivid.unlink!("nil", "foo") }.to raise_error(Chef::Exceptions::NoSuchAttribute) expect(vivid).to eql({ "one" => { "two" => { "three" => "four" } }, "array" => [ 0, 1, 2 ], "nil" => nil }) |