Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions lib/puppet/parser/functions/deep_merge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module Puppet::Parser::Functions
newfunction(:deep_merge, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args|
Recursively merges two or more hashes together and returns the resulting hash.

For example:

$hash1 = {'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } }
$hash2 = {'two' => 'dos', 'three' => { 'five' => 5 } }
$merged_hash = deep_merge($hash1, $hash2)
# The resulting hash is equivalent to:
# $merged_hash = { 'one' => 1, 'two' => 'dos', 'three' => { 'four' => 4, 'five' => 5 } }

When there is a duplicate key that is a hash, they are recursively merged.
When there is a duplicate key that is not a hash, the key in the rightmost hash will "win."

ENDHEREDOC

if args.length < 2
raise Puppet::ParseError, ("deep_merge(): wrong number of arguments (#{args.length}; must be at least 2)")
end

result = Hash.new
args.each do |arg|
next if arg.is_a? String and arg.empty? # empty string is synonym for puppet's undef
# If the argument was not a hash, skip it.
unless arg.is_a?(Hash)
raise Puppet::ParseError, "deep_merge: unexpected argument type #{arg.class}, only expects hash arguments"
end

# Now we have to traverse our hash assigning our non-hash values
# to the matching keys in our result while following our hash values
# and repeating the process.
overlay( result, arg )
end
return( result )
end
end

def overlay( hash1, hash2 )
hash2.each do |key, value|
if( value.is_a?(Hash) )
if( ! hash1.has_key?( key ) or ! hash1[key].is_a?(Hash))
hash1[key] = value
else
overlay( hash1[key], value )
end
else
hash1[key] = value
end
end
end

77 changes: 77 additions & 0 deletions spec/unit/puppet/parser/functions/deep_merge_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#! /usr/bin/env ruby -S rspec

require 'spec_helper'

describe Puppet::Parser::Functions.function(:deep_merge) do
let(:scope) { PuppetlabsSpec::PuppetInternals.scope }

describe 'when calling deep_merge from puppet' do
it "should not compile when no arguments are passed" do
pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./
Puppet[:code] = '$x = deep_merge()'
expect {
scope.compiler.compile
}.to raise_error(Puppet::ParseError, /wrong number of arguments/)
end

it "should not compile when 1 argument is passed" do
pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./
Puppet[:code] = "$my_hash={'one' => 1}\n$x = deep_merge($my_hash)"
expect {
scope.compiler.compile
}.to raise_error(Puppet::ParseError, /wrong number of arguments/)
end
end

describe 'when calling deep_merge on the scope instance' do
it 'should require all parameters are hashes' do
expect { new_hash = scope.function_deep_merge([{}, '2'])}.to raise_error(Puppet::ParseError, /unexpected argument type String/)
expect { new_hash = scope.function_deep_merge([{}, 2])}.to raise_error(Puppet::ParseError, /unexpected argument type Fixnum/)
end

it 'should accept empty strings as puppet undef' do
expect { new_hash = scope.function_deep_merge([{}, ''])}.not_to raise_error(Puppet::ParseError, /unexpected argument type String/)
end

it 'should be able to deep_merge two hashes' do
new_hash = scope.function_deep_merge([{'one' => '1', 'two' => '1'}, {'two' => '2', 'three' => '2'}])
new_hash['one'].should == '1'
new_hash['two'].should == '2'
new_hash['three'].should == '2'
end

it 'should deep_merge multiple hashes' do
hash = scope.function_deep_merge([{'one' => 1}, {'one' => '2'}, {'one' => '3'}])
hash['one'].should == '3'
end

it 'should accept empty hashes' do
scope.function_deep_merge([{},{},{}]).should == {}
end

it 'should deep_merge subhashes' do
hash = scope.function_deep_merge([{'one' => 1}, {'two' => 2, 'three' => { 'four' => 4 } }])
hash['one'].should == 1
hash['two'].should == 2
hash['three'].should == { 'four' => 4 }
end

it 'should append to subhashes' do
hash = scope.function_deep_merge([{'one' => { 'two' => 2 } }, { 'one' => { 'three' => 3 } }])
hash['one'].should == { 'two' => 2, 'three' => 3 }
end

it 'should append to subhashes 2' do
hash = scope.function_deep_merge([{'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } }, {'two' => 'dos', 'three' => { 'five' => 5 } }])
hash['one'].should == 1
hash['two'].should == 'dos'
hash['three'].should == { 'four' => 4, 'five' => 5 }
end

it 'should append to subhashes 3' do
hash = scope.function_deep_merge([{ 'key1' => { 'a' => 1, 'b' => 2 }, 'key2' => { 'c' => 3 } }, { 'key1' => { 'b' => 99 } }])
hash['key1'].should == { 'a' => 1, 'b' => 99 }
hash['key2'].should == { 'c' => 3 }
end
end
end