summaryrefslogtreecommitdiff
path: root/lib/chef/node/attribute_collections.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/node/attribute_collections.rb')
-rw-r--r--lib/chef/node/attribute_collections.rb112
1 files changed, 112 insertions, 0 deletions
diff --git a/lib/chef/node/attribute_collections.rb b/lib/chef/node/attribute_collections.rb
index f09b02b106..c8bc618762 100644
--- a/lib/chef/node/attribute_collections.rb
+++ b/lib/chef/node/attribute_collections.rb
@@ -209,5 +209,117 @@ class Chef
end
+ # == MultiMash
+ # This is a Hash-like object that contains multiple VividMashes in it. Its
+ # purpose is so that the user can descend into the mash and delete a subtree
+ # from all of the Mash objects (used to delete all values in a subtree from
+ # default, force_default, role_default and env_default at the same time). The
+ # assignment operator strictly does assignment (does no merging) and works
+ # by deleting the subtree and then assigning to the last mash which passed in
+ # the initializer.
+ #
+ # A lot of the complexity of this class comes from the fact that at any key
+ # value some or all of the mashes may walk off their ends and become nil or
+ # true or something. The schema may change so that one precidence leve may
+ # be 'true' object and another may be a VividMash. It is also possible that
+ # one or many of them may transition from VividMashes to Hashes or Arrays.
+ #
+ # It also supports the case where you may be deleting a key using node.rm
+ # in which case if intermediate keys all walk off into nil then you don't want
+ # to be autovivifying keys as you go. On the other hand you may be using
+ # node.force_default! in which case you'll wind up with a []= operator at the
+ # end and you want autovivification, so we conditionally have to support either
+ # operation.
+ #
+ # @todo: can we have an autovivify class that decorates a class that doesn't
+ # autovivify or something so that the code is less awful?
+ #
+ class MultiMash
+ attr_reader :root
+ attr_reader :mashes
+ attr_reader :opts
+ attr_reader :primary_mash
+
+ # Initialize with an array of mashes. For the delete return value to work
+ # properly the mashes must come from the same attribute level (i.e. all
+ # override or all default, but not a mix of both).
+ def initialize(root, primary_mash, mashes, opts={})
+ @root = root
+ @primary_mash = primary_mash
+ @mashes = mashes
+ @opts = opts
+ @opts[:autovivify] = true if @opts[:autovivify].nil?
+ end
+
+ def [](key)
+ # handle the secondary mashes
+ new_mashes = []
+ mashes.each do |mash|
+ new_mash = safe_evalute_key(mash, key)
+ # secondary mashes never autovivify so once they fall into nil, we just stop tracking them
+ new_mashes.push(new_mash) unless new_mash.nil?
+ end
+
+ new_primary_mash = safe_evalute_key(primary_mash, key)
+
+ if new_primary_mash.nil? && @opts[:autovivify]
+ primary_mash[key] = VividMash.new(root)
+ new_primary_mash = primary_mash[key]
+ end
+
+ MultiMash.new(root, new_primary_mash, new_mashes, opts)
+ end
+
+ def []=(key, value)
+ if primary_mash.nil?
+ # This theoretically should never happen since node#force_default! setter methods will autovivify and
+ # node#rm methods do not end in #[]= operators.
+ raise TypeError, "No autovivification was specified initially on a method chain ending in assignment"
+ end
+ ret = delete(key)
+ primary_mash[key] = value
+ ret
+ end
+
+ # mash.element('foo', 'bar') is the same as mash['foo']['bar']
+ def element(key = nil, *subkeys)
+ return self if key.nil?
+ submash = self[key]
+ subkeys.empty? ? submash : submash.element(*subkeys)
+ end
+
+ def delete(key)
+ # the return value is a deep merge which is correct semantics when
+ # merging between attributes on the same level (this would be incorrect
+ # if passed both override and default attributes which would need hash_only
+ # merging).
+ ret = mashes.inject(Mash.new) do |merged, mash|
+ Chef::Mixin::DeepMerge.merge(merged, mash)
+ end
+ ret = Chef::Mixin::DeepMerge.merge(ret, primary_mash)
+ mashes.each do |mash|
+ mash.delete(key) if mash.respond_to?(:delete)
+ end
+ primary_mash.delete(key) if primary_mash.respond_to?(:delete)
+ ret[key]
+ end
+
+ private
+
+ def safe_evalute_key(mash, key)
+ if mash.respond_to?(:[])
+ if mash.respond_to?(:has_key?)
+ if mash.has_key?(key)
+ return mash[key] if mash[key].respond_to?(:[])
+ end
+ elsif !mash[key].nil?
+ return mash[key] if mash[key].respond_to?(:[])
+ end
+ end
+ return nil
+ end
+
+ end
+
end
end