lib/chef/mixin/deep_merge.rb in chef-0.10.8 vs lib/chef/mixin/deep_merge.rb in chef-0.10.10.beta.1

- old
+ new

@@ -20,25 +20,34 @@ class Chef module Mixin # == Chef::Mixin::DeepMerge # Implements a deep merging algorithm for nested data structures. # ==== Notice: - # This code is imported from deep_merge by Steve Midgley. deep_merge is - # available under the MIT license from + # This code was originally imported from deep_merge by Steve Midgley. + # deep_merge is available under the MIT license from # http://trac.misuse.org/science/wiki/DeepMerge module DeepMerge - def self.merge(first, second) + extend self + + def merge(first, second) first = Mash.new(first) unless first.kind_of?(Mash) second = Mash.new(second) unless second.kind_of?(Mash) - DeepMerge.deep_merge!(second, first, {:knockout_prefix => "!merge:", :preserve_unmergeables => false}) + DeepMerge.deep_merge(second, first, {:preserve_unmergeables => false}) end + + # Inherited roles use the knockout_prefix array subtraction functionality + # This is likely to go away in Chef >= 0.11 + def role_merge(first, second) + first = Mash.new(first) unless first.kind_of?(Mash) + second = Mash.new(second) unless second.kind_of?(Mash) + + DeepMerge.deep_merge(second, first, {:knockout_prefix => "!merge", :preserve_unmergeables => false}) + end class InvalidParameter < StandardError; end - DEFAULT_FIELD_KNOCKOUT_PREFIX = '--' unless defined?(DEFAULT_FIELD_KNOCKOUT_PREFIX) - # Deep Merge core documentation. # deep_merge! method permits merging of arbitrary child elements. The two top level # elements must be hashes. These hashes can contain unlimited (to stack limit) levels # of child elements. These child elements to not have to be of the same types. # Where child elements are of the same type, deep_merge will attempt to merge them together. @@ -52,32 +61,34 @@ # By default, "deep_merge!" will overwrite any unmergeables and merge everything else. # To avoid this, use "deep_merge" (no bang/exclamation mark) # # Options: # Options are specified in the last parameter passed, which should be in hash format: - # hash.deep_merge!({:x => [1,2]}, {:knockout_prefix => '--'}) + # hash.deep_merge!({:x => [1,2]}, {:knockout_prefix => '!merge'}) # :preserve_unmergeables DEFAULT: false # Set to true to skip any unmergeable elements from source # :knockout_prefix DEFAULT: nil # Set to string value to signify prefix which deletes elements from existing element + # A colon is appended when indicating a specific value, eg: + # :knockout_prefix => "dontmerge", is referenced as "dontmerge:foobar" in an array # :sort_merged_arrays DEFAULT: false # Set to true to sort all arrays that are merged together # :unpack_arrays DEFAULT: nil # Set to string value to run "Array::join" then "String::split" against all arrays # :merge_debug DEFAULT: false # Set to true to get console output of merge process for debugging # # Selected Options Details: # :knockout_prefix => The purpose of this is to provide a way to remove elements # from existing Hash by specifying them in a special way in incoming hash - # source = {:x => ['--1', '2']} + # source = {:x => ['!merge:1', '2']} # dest = {:x => ['1', '3']} # dest.ko_deep_merge!(source) # Results: {:x => ['2','3']} # Additionally, if the knockout_prefix is passed alone as a string, it will cause # the entire element to be removed: - # source = {:x => '--'} + # source = {:x => '!merge'} # dest = {:x => [1,2,3]} # dest.ko_deep_merge!(source) # Results: {:x => ""} # :unpack_arrays => The purpose of this is to permit compound elements to be passed # in as strings and to be converted into discrete array elements @@ -88,11 +99,11 @@ # Why: If receiving data from an HTML form, this makes it easy for a checkbox # to pass multiple values from within a single HTML element # # There are many tests for this library - and you can learn more about the features # and usages of deep_merge! by just browsing the test examples - def self.deep_merge!(source, dest, options = {}) + def deep_merge!(source, dest, options = {}) # turn on this line for stdout debugging text merge_debug = options[:merge_debug] || false overwrite_unmergeable = !options[:preserve_unmergeables] knockout_prefix = options[:knockout_prefix] || nil raise InvalidParameter, "knockout_prefix cannot be an empty string in deep_merge!" if knockout_prefix == "" @@ -106,11 +117,11 @@ return dest if source.nil? # if dest doesn't exist, then simply copy source to it if dest.nil? && overwrite_unmergeable dest = source; return dest end - + puts "#{di}Source class: #{source.class.inspect} :: Dest class: #{dest.class.inspect}" if merge_debug if source.kind_of?(Hash) puts "#{di}Hashes: #{source.inspect} :: #{dest.inspect}" if merge_debug source.each do |src_key, src_value| if dest.kind_of?(Hash) @@ -144,20 +155,23 @@ if dest.kind_of?(Array) dest = dest.join(array_split_char).split(array_split_char) end end # if there's a naked knockout_prefix in source, that means we are to truncate dest - if source.index(knockout_prefix) - dest = clear_or_nil(dest); source.delete(knockout_prefix) + ko_variants = [ knockout_prefix, "#{knockout_prefix}:" ] + ko_variants.each do |ko| + if source.index(ko) + dest = clear_or_nil(dest); source.delete(ko) + end end if dest.kind_of?(Array) if knockout_prefix print "#{di} knocking out: " if merge_debug # remove knockout prefix items from both source and dest source.delete_if do |ko_item| retval = false - item = ko_item.respond_to?(:gsub) ? ko_item.gsub(%r{^#{knockout_prefix}}, "") : ko_item + item = ko_item.respond_to?(:gsub) ? ko_item.gsub(%r{^#{knockout_prefix}:}, "") : ko_item if item != ko_item print "#{ko_item} - " if merge_debug dest.delete(item) dest.delete(ko_item) retval = true @@ -180,20 +194,24 @@ puts "#{di}Returning #{dest.inspect}" if merge_debug dest end # deep_merge! # allows deep_merge! to uniformly handle overwriting of unmergeable entities - def self.overwrite_unmergeables(source, dest, options) + def overwrite_unmergeables(source, dest, options) merge_debug = options[:merge_debug] || false overwrite_unmergeable = !options[:preserve_unmergeables] knockout_prefix = options[:knockout_prefix] || false di = options[:debug_indent] || '' if knockout_prefix && overwrite_unmergeable if source.kind_of?(String) # remove knockout string from source before overwriting dest - src_tmp = source.gsub(%r{^#{knockout_prefix}},"") + if source == knockout_prefix + src_tmp = "" + else + src_tmp = source.gsub(%r{^#{knockout_prefix}:},"") + end elsif source.kind_of?(Array) # remove all knockout elements before overwriting dest - src_tmp = source.delete_if {|ko_item| ko_item.kind_of?(String) && ko_item.match(%r{^#{knockout_prefix}}) } + src_tmp = source.delete_if {|ko_item| ko_item.kind_of?(String) && ko_item.match(%r{^#{knockout_prefix}:}) } else src_tmp = source end if src_tmp == source # if we didn't find a knockout_prefix then we just overwrite dest puts "#{di}#{src_tmp.inspect} -over-> #{dest.inspect}" if merge_debug @@ -205,11 +223,15 @@ elsif overwrite_unmergeable dest = source end dest end + + def deep_merge(source, dest, options = {}) + deep_merge!(source.dup, dest.dup, options) + end - def self.clear_or_nil(obj) + def clear_or_nil(obj) if obj.respond_to?(:clear) obj.clear else obj = nil end