From e7ba9adbcf47b87e7f443e509152e4cf63e077a7 Mon Sep 17 00:00:00 2001 From: Xavier Mol Date: Tue, 20 Jun 2023 14:19:34 +0200 Subject: [PATCH] Add Ruby function stdlib::cmp_hash --- lib/puppet/functions/stdlib/cmp_hash.rb | 70 +++++++++++++++++++++++++ spec/functions/cmp_hash_spec.rb | 63 ++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 lib/puppet/functions/stdlib/cmp_hash.rb create mode 100644 spec/functions/cmp_hash_spec.rb diff --git a/lib/puppet/functions/stdlib/cmp_hash.rb b/lib/puppet/functions/stdlib/cmp_hash.rb new file mode 100644 index 000000000..66f419491 --- /dev/null +++ b/lib/puppet/functions/stdlib/cmp_hash.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# @summary Compare two Hashes to each other, optionally after applying a block. +# +# Returns -1, 0 or 1, depending on whether the first Hash is smaller, equal or bigger +# than the second Hash. If no block is supplied, both Hashes will be transformed +# into sorted Arrays first. Otherwise, the block has to yield something that Ruby +# can compare with the `<=>` operator. +# +# @example Use stdlib::cmp_hash to sort a list of Hashes (here, the local disks by their volume) +# $::disks.values.sort |$a, $b| { stdlib::cmp_hash($a, $b) |$h| { $h['size_bytes'] } } +# +Puppet::Functions.create_function(:'stdlib::cmp_hash') do + # @param hash1 The first Hash. + # @param hash2 The second Hash. + # @example Compare by default tranformation to Arrays + # $hash1 = { + # 'primary_key' => 2, + # 'key1' => ['val1', 'val2'], + # 'key2' => { 'key3' => 'val3', }, + # 'key4' => true, + # 'key5' => 12345, + # } + # $hash2 = { + # 'primary_key' => 1, + # 'key6' => ['val1', 'val2'], + # 'key7' => { 'key8' => 'val9', }, + # 'key10' => true, + # 'key11' => 67890, + # } + # stdlib::cmp_hash($hash1, $hash2) # => -1; 'key1' is the smallest key, thus $hash1 is smaller + # @return [Integer] Returns an integer (-1, 0, or +1) if hash1 is less than, equal to, or greater than hash2. + dispatch :cmp_hashes_as_arrays do + param 'Hash[Any, Any]', :hash1 + param 'Hash[Any, Any]', :hash2 + end + + # @param hash1 The first Hash. + # @param hash2 The second Hash. + # @example Compare two Hashes by a specific key + # $hash1 = { + # 'primary_key' => 2, + # 'key1' => ['val1', 'val2'], + # 'key2' => { 'key3' => 'val3', }, + # 'key4' => true, + # 'key5' => 12345, + # } + # $hash2 = { + # 'primary_key' => 1, + # 'key6' => ['val1', 'val2'], + # 'key7' => { 'key8' => 'val9', }, + # 'key10' => true, + # 'key11' => 67890, + # } + # stdlib::cmp_hash($hash1, $hash2) |$h| { $h['primary_key'] } # => 1; $hash2 has the smaller value for 'primary_key', hence $hash1 is bigger + # @return [Integer] Returns an integer (-1, 0, or +1) if block(hash1) is less than, equal to, or greater than block(hash2). + dispatch :cmp_hashes_with_block do + param 'Hash[Any, Any]', :hash1 + param 'Hash[Any, Any]', :hash2 + block_param 'Callable[1,1]', :block + end + + def cmp_hashes_as_arrays(hash1, hash2) + cmp_hashes_with_block(hash1, hash2) { |h| h.to_a.sort } + end + + def cmp_hashes_with_block(hash1, hash2) + yield(hash1) <=> yield(hash2) + end +end diff --git a/spec/functions/cmp_hash_spec.rb b/spec/functions/cmp_hash_spec.rb new file mode 100644 index 000000000..a96784e50 --- /dev/null +++ b/spec/functions/cmp_hash_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'stdlib::cmp_hash' do + it { is_expected.not_to be_nil } + + describe 'raise exception unless two Hashes are provided' do + it { is_expected.to run.with_params.and_raise_error(ArgumentError, %r{'stdlib::cmp_hash' expects 2 arguments, got none}) } + it { is_expected.to run.with_params({}).and_raise_error(ArgumentError, %r{'stdlib::cmp_hash' expects 2 arguments, got 1}) } + it { is_expected.to run.with_params({}, {}, {}).and_raise_error(ArgumentError, %r{'stdlib::cmp_hash' expects 2 arguments, got 3}) } + it { is_expected.to run.with_params({}, 1).and_raise_error(ArgumentError, %r{'stdlib::cmp_hash' parameter 'hash2' expects a Hash value, got Integer}) } + it { is_expected.to run.with_params({}, 'a').and_raise_error(ArgumentError, %r{'stdlib::cmp_hash' parameter 'hash2' expects a Hash value, got String}) } + it { is_expected.to run.with_params(:undef, {}).and_raise_error(ArgumentError, %r{'stdlib::cmp_hash' parameter 'hash1' expects a Hash value, got Undef}) } + end + + describe 'raise exception in case lambda expects wrong number of arguments' do + it 'one too few' do + expect(subject).to run \ + .with_params({}, {}) \ + .with_lambda { 1 } \ + .and_raise_error(ArgumentError, Regexp.new(Regexp.escape('rejected: block expects 1 argument, got none'))) + end + + # TODO: Does not pass Rubocop tests, because running the function with this kind of lambda does not throw an error. + # it 'one too many' do + # expect(subject).to run \ + # .with_params({}, {}) \ + # .with_lambda { |_, _| 1 } \ + # .and_raise_error(ArgumentError, Regexp.new(Regexp.escape('rejected: block expects 1 argument, got 2'))) + # end + end + + hash1 = { + 'primary_key' => 2, + 'key1' => ['val1', 'val2'], + 'key2' => { 'key3' => 'val3' }, + 'key4' => true, + 'key5' => 123 + } + hash2 = { + 'primary_key' => 1, + 'key6' => ['val1', 'val2'], + 'key7' => { 'key8' => 'val9' }, + 'key10' => true, + 'key11' => 456 + } + describe 'compare without block' do + it { is_expected.to run.with_params(hash1, hash2).and_return(-1) } + it { is_expected.to run.with_params(hash2, hash1).and_return(1) } + it { is_expected.to run.with_params({}, hash1).and_return(-1) } + it { is_expected.to run.with_params(hash1, hash1).and_return(0) } + end + + describe 'compare with block' do + it { + expect(subject).to run \ + .with_params(hash1, hash2) \ + .with_lambda { |hsh| hsh['primary_key'] } \ + .and_return(1) + } + end +end