summaryrefslogtreecommitdiff
path: root/lib/chef/mixin/deep_merge.rb
blob: 300ae1a31fe8d774911a60fef7b8dd7b08c37f92 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#
# 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)
        # if dest doesn't exist, then simply copy source to it
        if dest.nil?
          dest = source; return dest
        end

        case source
        when nil
          dest
        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

        # 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

      # @api private
      #
      def deep_merge(source, dest)
        deep_merge!(safe_dup(source), safe_dup(dest))
      end

    end
  end
end