File: merge_strategy.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (441 lines) | stat: -rw-r--r-- 15,385 bytes parent folder | download | duplicates (2)
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
require 'deep_merge/core'

module Puppet::Pops
  # Merges to objects into one based on an implemented strategy.
  #
  class MergeStrategy
    NOT_FOUND = Object.new.freeze

    def self.strategies
      @@strategies ||= {}
    end
    private_class_method :strategies

    # Finds the merge strategy for the given _merge_, creates an instance of it and returns that instance.
    #
    # @param merge [MergeStrategy,String,Hash<String,Object>,nil] The merge strategy. Can be a string or symbol denoting the key
    #   identifier or a hash with options where the key 'strategy' denotes the key
    # @return [MergeStrategy] The matching merge strategy
    #
    def self.strategy(merge)
      return DefaultMergeStrategy::INSTANCE unless merge
      return merge if merge.is_a?(MergeStrategy)

      if merge.is_a?(Hash)
        merge_strategy = merge['strategy']
        if merge_strategy.nil?
          #TRANSLATORS 'merge' is a variable name and 'strategy' is a key and should not be translated
          raise ArgumentError, _("The hash given as 'merge' must contain the name of a strategy in string form for the key 'strategy'")
        end
        merge_options  = merge.size == 1 ? EMPTY_HASH : merge
      else
        merge_strategy = merge
        merge_options = EMPTY_HASH
      end
      merge_strategy = merge_strategy.to_sym if merge_strategy.is_a?(String)
      strategy_class = strategies[merge_strategy]
      raise ArgumentError, _("Unknown merge strategy: '%{strategy}'") % { strategy: merge_strategy } if strategy_class.nil?
      merge_options == EMPTY_HASH ? strategy_class::INSTANCE : strategy_class.new(merge_options)
    end

    # Returns the list of merge strategy keys known to this class
    #
    # @return [Array<Symbol>] List of strategy keys
    #
    def self.strategy_keys
      strategies.keys - [:default, :unconstrained_deep, :reverse_deep]
    end

    # Adds a new merge strategy to the map of strategies known to this class
    #
    # @param strategy_class [Class<MergeStrategy>] The class of the added strategy
    #
    def self.add_strategy(strategy_class)
      unless MergeStrategy > strategy_class
        #TRANSLATORS 'MergeStrategies.add_strategy' is a method, 'stratgey_class' is a variable and 'MergeStrategy' is a class name and should not be translated
        raise ArgumentError, _("MergeStrategies.add_strategy 'strategy_class' must be a 'MergeStrategy' class. Got %{strategy_class}") %
            { strategy_class: strategy_class }
      end
      strategies[strategy_class.key] = strategy_class
      nil
    end

    # Finds a merge strategy that corresponds to the given _merge_ argument and delegates the task of merging the elements of _e1_ and _e2_ to it.
    #
    # @param e1 [Object] The first element
    # @param e2 [Object] The second element
    # @return [Object] The result of the merge
    #
    def self.merge(e1, e2, merge)
      strategy(merge).merge(e1, e2)
    end

    def self.key
      raise NotImplementedError, "Subclass must implement 'key'"
    end

    # Create a new instance of this strategy configured with the given _options_
    # @param merge_options [Hash<String,Object>] Merge options
    def initialize(options)
      assert_type('The merge options', self.class.options_t, options) unless options.empty?
      @options = options
    end

    # Merges the elements of _e1_ and _e2_ according to the rules of this strategy and options given when this
    # instance was created
    #
    # @param e1 [Object] The first element
    # @param e2 [Object] The second element
    # @return [Object] The result of the merge
    #
    def merge(e1, e2)
      checked_merge(
        assert_type('The first element of the merge', value_t, e1),
        assert_type('The second element of the merge', value_t, e2))
    end

    # TODO: API 5.0 Remove this method
    # @deprecated
    def merge_lookup(lookup_variants)
      lookup(lookup_variants, Lookup::Invocation.current)
    end

    # Merges the result of yielding the given _lookup_variants_ to a given block.
    #
    # @param lookup_variants [Array] The variants to pass as second argument to the given block
    # @return [Object] the merged value.
    # @yield [} ]
    # @yieldparam variant [Object] each variant given in the _lookup_variants_ array.
    # @yieldreturn [Object] the value to merge with other values
    # @throws :no_such_key if the lookup was unsuccessful
    #
    # Merges the result of yielding the given _lookup_variants_ to a given block.
    #
    # @param lookup_variants [Array] The variants to pass as second argument to the given block
    # @return [Object] the merged value.
    # @yield [} ]
    # @yieldparam variant [Object] each variant given in the _lookup_variants_ array.
    # @yieldreturn [Object] the value to merge with other values
    # @throws :no_such_key if the lookup was unsuccessful
    #
    def lookup(lookup_variants, lookup_invocation)
      case lookup_variants.size
      when 0
        throw :no_such_key
      when 1
        merge_single(yield(lookup_variants[0]))
      else
        lookup_invocation.with(:merge, self) do
          result = lookup_variants.reduce(NOT_FOUND) do |memo, lookup_variant|
            not_found = true
            value = catch(:no_such_key) do
              v = yield(lookup_variant)
              not_found = false
              v
            end
            if not_found
              memo
            else
              memo.equal?(NOT_FOUND) ? convert_value(value) : merge(memo, value)
            end
          end
          throw :no_such_key if result == NOT_FOUND
          lookup_invocation.report_result(result)
        end
      end
    end

    # Converts a single value to the type expected when merging two elements
    # @param value [Object] the value to convert
    # @return [Object] the converted value
    def convert_value(value)
      value
    end

    # Applies the merge strategy on a single element. Only applicable for `unique`
    # @param value [Object] the value to merge with nothing
    # @return [Object] the merged value
    def merge_single(value)
      value
    end

    def options
      @options
    end

    def configuration
      if @options.nil? || @options.empty?
        self.class.key.to_s
      else
        @options.include?('strategy') ? @options : { 'strategy' => self.class.key.to_s }.merge(@options)
      end
    end

    protected

    class << self
      # Returns the type used to validate the options hash
      #
      # @return [Types::PStructType] the puppet type
      #
      def options_t
        @options_t ||=Types::TypeParser.singleton.parse("Struct[{strategy=>Optional[Pattern[/#{key}/]]}]")
      end
    end

    # Returns the type used to validate the options hash
    #
    # @return [Types::PAnyType] the puppet type
    #
    def value_t
      raise NotImplementedError, "Subclass must implement 'value_t'"
    end

    def checked_merge(e1, e2)
      raise NotImplementedError, "Subclass must implement 'checked_merge(e1,e2)'"
    end

    def assert_type(param, type, value)
      Types::TypeAsserter.assert_instance_of(param, type, value)
    end
  end

  # Simple strategy that returns the first value found. It never merges any values.
  #
  class FirstFoundStrategy < MergeStrategy
    INSTANCE = self.new(EMPTY_HASH)

    def self.key
      :first
    end

    # Returns the first value found
    #
    # @param lookup_variants [Array] The variants to pass as second argument to the given block
    # @return [Object] the merged value
    # @throws :no_such_key unless the lookup was successful
    #
    def lookup(lookup_variants, _)
      # First found does not continue when a root key was found and a subkey wasn't since that would
      # simulate a hash merge
      lookup_variants.each { |lookup_variant| catch(:no_such_key) { return yield(lookup_variant) } }
      throw :no_such_key
    end

    protected

    def value_t
      @value_t ||= Types::PAnyType::DEFAULT
    end

    MergeStrategy.add_strategy(self)
  end

  # Same as {FirstFoundStrategy} but used when no strategy has been explicitly given
  class DefaultMergeStrategy < FirstFoundStrategy
    INSTANCE = self.new(EMPTY_HASH)

    def self.key
      :default
    end

    MergeStrategy.add_strategy(self)
  end

  # Produces a new hash by merging hash e1 with hash e2 in such a way that the values of duplicate keys
  # will be those of e1
  #
  class HashMergeStrategy < MergeStrategy
    INSTANCE = self.new(EMPTY_HASH)

    def self.key
      :hash
    end

    # @param e1 [Hash<String,Object>] The hash that will act as the source of the merge
    # @param e2 [Hash<String,Object>] The hash that will act as the receiver for the merge
    # @return [Hash<String,Object]] The merged hash
    # @see Hash#merge
    def checked_merge(e1, e2)
      e2.merge(e1)
    end

    protected

    def value_t
      @value_t ||= Types::TypeParser.singleton.parse('Hash[String,Data]')
    end

    MergeStrategy.add_strategy(self)
  end

  # Merges two values that must be either scalar or arrays into a unique set of values.
  #
  # Scalar values will be converted into a one element arrays and array values will be flattened
  # prior to forming the unique set. The order of the elements is preserved with e1 being the
  # first contributor of elements and e2 the second.
  #
  class UniqueMergeStrategy < MergeStrategy
    INSTANCE = self.new(EMPTY_HASH)

    def self.key
      :unique
    end

    # @param e1 [Array<Object>] The first array
    # @param e2 [Array<Object>] The second array
    # @return [Array<Object>] The unique set of elements
    #
    def checked_merge(e1, e2)
      convert_value(e1) | convert_value(e2)
    end

    def convert_value(e)
      e.is_a?(Array) ? e.flatten : [e]
    end

    # If _value_ is an array, then return the result of calling `uniq` on that array. Otherwise,
    # the argument is returned.
    # @param value [Object] the value to merge with nothing
    # @return [Object] the merged value
    def merge_single(value)
      value.is_a?(Array) ? value.uniq : value
    end

    protected

    def value_t
      @value_t ||= Types::TypeParser.singleton.parse('Variant[Scalar,Array[Data]]')
    end

    MergeStrategy.add_strategy(self)
  end

  # Documentation copied from https://github.com/danielsdeleo/deep_merge/blob/master/lib/deep_merge/core.rb
  # altered with respect to _preserve_unmergeables_ since this implementation always disables that option.
  #
  # The destination is dup'ed before the deep_merge is called to allow frozen objects as values.
  #
  # 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}
  #
  # "deep_merge" will unconditionally overwrite any unmergeables and merge everything else.
  #
  # Options:
  #   Options are specified in the last parameter passed, which should be in hash format:
  #   hash.deep_merge!({:x => [1,2]}, {:knockout_prefix => '--'})
  #   - 'knockout_prefix' Set to string value to signify prefix which deletes elements from existing element. Defaults is _undef_
  #   - 'sort_merged_arrays' Set to _true_ to sort all arrays that are merged together. Default is _false_
  #   - 'merge_hash_arrays' Set to _true_ to merge hashes within arrays. Default is _false_
  #
  # 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']}
  #    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 => '--'}
  #    dest   = {:x => [1,2,3]}
  #    dest.ko_deep_merge!(source)
  #    Results: {:x => ""}
  #
  # :merge_hash_arrays => merge hashes within arrays
  #   source = {:x => [{:y => 1}]}
  #   dest   = {:x => [{:z => 2}]}
  #   dest.deep_merge!(source, {:merge_hash_arrays => true})
  #   Results: {:x => [{:y => 1, :z => 2}]}
  #
  class DeepMergeStrategy < MergeStrategy
    INSTANCE = self.new(EMPTY_HASH)

    def self.key
      :deep
    end

    def checked_merge(e1, e2)
      dm_options = { :preserve_unmergeables => false }
      options.each_pair { |k,v| dm_options[k.to_sym] = v unless k == 'strategy' }
      # e2 (the destination) is deep cloned to avoid that the passed in object mutates
      DeepMerge.deep_merge!(e1, deep_clone(e2), dm_options)
    end

    def deep_clone(value)
      if value.is_a?(Hash)
        result = value.clone
        value.each{ |k, v| result[k] = deep_clone(v) }
        result
      elsif value.is_a?(Array)
        value.map{ |v| deep_clone(v) }
      else
        value
      end
    end

    protected

    class << self
      # Returns a type that allows all deep_merge options except 'preserve_unmergeables' since we force
      # the setting of that option to false
      #
      # @return [Types::PAnyType] the puppet type used when validating the options hash
      def options_t
        @options_t ||= Types::TypeParser.singleton.parse('Struct[{'\
                                                         "strategy=>Optional[Pattern[#{key}]],"\
                                                         'knockout_prefix=>Optional[String],'\
                                                         'merge_debug=>Optional[Boolean],'\
                                                         'merge_hash_arrays=>Optional[Boolean],'\
                                                         'sort_merged_arrays=>Optional[Boolean],'\
                                                         '}]')
      end
    end

    def value_t
      @value_t ||= Types::PAnyType::DEFAULT
    end

    MergeStrategy.add_strategy(self)
  end

  # Same as {DeepMergeStrategy} but without constraint on valid merge options
  # (needed for backward compatibility with Hiera v3)
  class UnconstrainedDeepMergeStrategy < DeepMergeStrategy
    def self.key
      :unconstrained_deep
    end

    # @return [Types::PAnyType] the puppet type used when validating the options hash
    def self.options_t
      @options_t ||= Types::TypeParser.singleton.parse('Hash[String[1],Any]')
    end

    MergeStrategy.add_strategy(self)
  end

  # Same as {UnconstrainedDeepMergeStrategy} but with reverse priority of merged elements.
  # (needed for backward compatibility with Hiera v3)
  class ReverseDeepMergeStrategy < UnconstrainedDeepMergeStrategy
    INSTANCE = self.new(EMPTY_HASH)

    def self.key
      :reverse_deep
    end

    def checked_merge(e1, e2)
      super(e2, e1)
    end

    MergeStrategy.add_strategy(self)
  end
end