Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
#
# Author:: Adam Jacob (<adam@chef.io>)
# 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