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
|