Module: Chef::Mixin::DeepMerge

Extended by:
DeepMerge
Included in:
DeepMerge
Defined in:
lib/chef/mixin/deep_merge.rb

Overview

Chef::Mixin::DeepMerge

Implements a deep merging algorithm for nested data structures.

Notice:

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

Defined Under Namespace

Classes: InvalidParameter, InvalidSubtractiveMerge

Constant Summary collapse

OLD_KNOCKOUT_PREFIX =
"!merge:".freeze
OLD_KNOCKOUT_MATCH =

Regex to match the “knockout prefix” that was used to indicate subtractive merging in Chef 10.x and previous. Subtractive merging is removed as of Chef 11, but we detect attempted use of it and raise an error (see: raise_if_knockout_used!)

%r[!merge].freeze

Instance Method Summary collapse

Instance Method Details

#deep_merge(source, dest) ⇒ Object



160
161
162
# File 'lib/chef/mixin/deep_merge.rb', line 160

def deep_merge(source, dest)
  deep_merge!(source.dup, dest.dup)
end

#deep_merge!(source, dest) ⇒ Object

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. 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)



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/chef/mixin/deep_merge.rb', line 75

def deep_merge!(source, dest)
  # if dest doesn't exist, then simply copy source to it
  if dest.nil?
    dest = source; return dest
  end

  raise_if_knockout_used!(source)
  raise_if_knockout_used!(dest)
  case source
  when nil
    dest
  when Hash
    source.each do |src_key, src_value|
      if dest.kind_of?(Hash)
        if dest[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
          raise_if_knockout_used!(src_value)
          dest[src_key] = src_value
        end
      else # dest isn't a hash, so we overwrite it completely
        dest = source
      end
    end
  when Array
    if dest.kind_of?(Array)
      dest = 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

#hash_only_merge(merge_onto, merge_with) ⇒ Object

deep_merge!



113
114
115
# File 'lib/chef/mixin/deep_merge.rb', line 113

def hash_only_merge(merge_onto, merge_with)
  hash_only_merge!(merge_onto.dup, merge_with.dup)
end

#hash_only_merge!(merge_onto, merge_with) ⇒ Object

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.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/chef/mixin/deep_merge.rb', line 121

def hash_only_merge!(merge_onto, merge_with)
  # If there are two Hashes, recursively merge.
  if merge_onto.kind_of?(Hash) && merge_with.kind_of?(Hash)
    merge_with.each do |key, merge_with_value|
      merge_onto[key] = hash_only_merge!(merge_onto[key], merge_with_value)
    end
    merge_onto

  # If merge_with is nil, don't replace merge_onto
  elsif merge_with.nil?
    merge_onto

  # In all other cases, replace merge_onto with merge_with
  else
    merge_with
  end
end

#merge(first, second) ⇒ Object



43
44
45
46
47
48
# File 'lib/chef/mixin/deep_merge.rb', line 43

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)
end

#raise_if_knockout_used!(obj) ⇒ Object

Checks for attempted use of subtractive merge, which was removed for Chef 11.0. If subtractive merge use is detected, will raise an InvalidSubtractiveMerge exception.



142
143
144
145
146
# File 'lib/chef/mixin/deep_merge.rb', line 142

def raise_if_knockout_used!(obj)
  if uses_knockout?(obj)
    raise InvalidSubtractiveMerge, "subtractive merge with !merge is no longer supported"
  end
end

#role_merge(first, second) ⇒ Object

Inherited roles use the knockout_prefix array subtraction functionality This is likely to go away in Chef >= 0.11



52
53
54
55
56
57
# File 'lib/chef/mixin/deep_merge.rb', line 52

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)
end

#uses_knockout?(obj) ⇒ Boolean

Checks for attempted use of subtractive merge in obj.

Returns:

  • (Boolean)


149
150
151
152
153
154
155
156
157
158
# File 'lib/chef/mixin/deep_merge.rb', line 149

def uses_knockout?(obj)
  case obj
  when String
    obj =~ OLD_KNOCKOUT_MATCH
  when Array
    obj.any? {|element| element.respond_to?(:gsub) && element =~ OLD_KNOCKOUT_MATCH }
  else
    false
  end
end