diff options
Diffstat (limited to 'lib/chef/node/attribute_collections.rb')
-rw-r--r-- | lib/chef/node/attribute_collections.rb | 112 |
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 |