# # Author:: Adam Jacob () # Author:: Steve Midgley (http://www.misuse.org/science) # Copyright:: Copyright (c) Chef Software Inc. # Copyright:: Copyright 2008-2016, Steve Midgley # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. class Chef module Mixin # Implements a deep merging algorithm for nested data structures. # # 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 # # Note that this is not considered a public interface. It is technically # public and has been used and we cannot break the API, but continued # external use is discouraged. We are unlikely to change the shape of # the API and break anyone, but this code does not serve the purposes of # cookbook authors and customers. It is intended only for the purposes # of the internal use in the chef-client codebase. We do not accept # pull requests to extend the functionality of this algorithm. Users # who find this does nearly what they want, should copy and paste the # algorithm and tune to their needs. We will not maintain any additional # use cases. # # "It is what it is, and if it isn't what you want, you need to build # that yourself" # # @api private # module DeepMerge extend self # @api private def merge(first, second) first = Mash.new(first) unless first.is_a?(Mash) second = Mash.new(second) unless second.is_a?(Mash) DeepMerge.deep_merge(second, first) end class InvalidParameter < StandardError; end # 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. # Where child elements are not of the same type, deep_merge will skip or optionally overwrite # the destination element with the contents of the source element at that level. # # So if you have two hashes like this: # # source = {:x => [1,2,3], :y => 2} # dest = {:x => [4,5,'6'], :y => [7,8,9]} # dest.deep_merge!(source) # Results: {:x => [1,2,3,4,5,'6'], :y => 2} # # By default, "deep_merge!" will overwrite any unmergeables and merge everything else. # To avoid this, use "deep_merge" (no bang/exclamation mark) # # @api private # def deep_merge!(source, dest) case source when Hash if dest.is_a?(Hash) source.each do |src_key, src_value| if dest.key?(src_key) dest[src_key] = deep_merge!(src_value, dest[src_key]) else # dest[src_key] doesn't exist so we take whatever source has dest[src_key] = src_value end end else # dest isn't a hash, so we overwrite it completely dest = source end when Array if dest.is_a?(Array) dest |= source else dest = source end when String dest = source else # src_hash is not an array or hash, so we'll have to overwrite dest dest = source end dest end # deep_merge! # @api private def hash_only_merge(merge_onto, merge_with) hash_only_merge!(safe_dup(merge_onto), safe_dup(merge_with)) end # @api private def safe_dup(thing) thing.dup rescue TypeError thing end # Deep merge without Array merge. # `merge_onto` is the object that will "lose" in case of conflict. # `merge_with` is the object whose values will replace `merge_onto`s # values when there is a conflict. # # @api private # def hash_only_merge!(merge_onto, merge_with) # If there are two Hashes, recursively merge. if merge_onto.is_a?(Hash) && merge_with.is_a?(Hash) merge_with.each do |key, merge_with_value| value = if merge_onto.key?(key) hash_only_merge(merge_onto[key], merge_with_value) else merge_with_value end if merge_onto.respond_to?(:public_method_that_only_deep_merge_should_use) # we can't call ImmutableMash#[]= because its immutable, but we need to mutate it to build it in-place merge_onto.public_method_that_only_deep_merge_should_use(key, value) else merge_onto[key] = value end end merge_onto # In all other cases, replace merge_onto with merge_with else merge_with end end # @api private # def deep_merge(source, dest) deep_merge!(safe_dup(source), safe_dup(dest)) end end end end