From 823a352f0f47d4481844bb6b6a6c00224ed556b8 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin Date: Tue, 1 Sep 2015 21:39:16 +0300 Subject: Add a new function "try_get_value" * Extracts a value from a deeply-nested data structure * Returns default if a value could not be extracted --- README.markdown | 34 +++++++++ lib/puppet/parser/functions/try_get_value.rb | 77 +++++++++++++++++++++ spec/acceptance/try_get_value_spec.rb | 47 +++++++++++++ spec/functions/try_get_value_spec.rb | 100 +++++++++++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 lib/puppet/parser/functions/try_get_value.rb create mode 100755 spec/acceptance/try_get_value_spec.rb create mode 100644 spec/functions/try_get_value_spec.rb diff --git a/README.markdown b/README.markdown index 8ed3d9b..22bece8 100644 --- a/README.markdown +++ b/README.markdown @@ -669,6 +669,40 @@ Returns the current Unix epoch time as an integer. For example, `time()` returns Converts the argument into bytes, for example "4 kB" becomes "4096". Takes a single string value as an argument. *Type*: rvalue. +#### `try_get_value` + +*Type*: rvalue. + +Looks up into a complex structure of arrays and hashes and returns a value +or the default value if nothing was found. + +Key can contain slashes to describe path components. The function will go down +the structure and try to extract the required value. + +$data = { + 'a' => { + 'b' => [ + 'b1', + 'b2', + 'b3', + ] + } +} + +$value = try_get_value($data, 'a/b/2', 'not_found', '/') +=> $value = 'b3' + +a -> first hash key +b -> second hash key +2 -> array index starting with 0 + +not_found -> (optional) will be returned if there is no value or the path did not match. Defaults to nil. +/ -> (optional) path delimiter. Defaults to '/'. + +In addition to the required "key" argument, "try_get_value" accepts default +argument. It will be returned if no value was found or a path component is +missing. And the fourth argument can set a variable path separator. + #### `type3x` Returns a string description of the type when passed a value. Type can be a string, array, hash, float, integer, or boolean. This function will be removed when Puppet 3 support is dropped and the new type system can be used. *Type*: rvalue. diff --git a/lib/puppet/parser/functions/try_get_value.rb b/lib/puppet/parser/functions/try_get_value.rb new file mode 100644 index 0000000..0c19fd9 --- /dev/null +++ b/lib/puppet/parser/functions/try_get_value.rb @@ -0,0 +1,77 @@ +module Puppet::Parser::Functions + newfunction( + :try_get_value, + :type => :rvalue, + :arity => -2, + :doc => <<-eos +Looks up into a complex structure of arrays and hashes and returns a value +or the default value if nothing was found. + +Key can contain slashes to describe path components. The function will go down +the structure and try to extract the required value. + +$data = { + 'a' => { + 'b' => [ + 'b1', + 'b2', + 'b3', + ] + } +} + +$value = try_get_value($data, 'a/b/2', 'not_found', '/') +=> $value = 'b3' + +a -> first hash key +b -> second hash key +2 -> array index starting with 0 + +not_found -> (optional) will be returned if there is no value or the path did not match. Defaults to nil. +/ -> (optional) path delimiter. Defaults to '/'. + +In addition to the required "key" argument, "try_get_value" accepts default +argument. It will be returned if no value was found or a path component is +missing. And the fourth argument can set a variable path separator. + eos + ) do |args| + path_lookup = lambda do |data, path, default| + debug "Try_get_value: #{path.inspect} from: #{data.inspect}" + if data.nil? + debug "Try_get_value: no data, return default: #{default.inspect}" + break default + end + unless path.is_a? Array + debug "Try_get_value: wrong path, return default: #{default.inspect}" + break default + end + unless path.any? + debug "Try_get_value: value found, return data: #{data.inspect}" + break data + end + unless data.is_a? Hash or data.is_a? Array + debug "Try_get_value: incorrect data, return default: #{default.inspect}" + break default + end + + key = path.shift + if data.is_a? Array + begin + key = Integer key + rescue ArgumentError + debug "Try_get_value: non-numeric path for an array, return default: #{default.inspect}" + break default + end + end + path_lookup.call data[key], path, default + end + + data = args[0] + path = args[1] || '' + default = args[2] + separator = args[3] || '/' + + path = path.split separator + path_lookup.call data, path, default + end +end diff --git a/spec/acceptance/try_get_value_spec.rb b/spec/acceptance/try_get_value_spec.rb new file mode 100755 index 0000000..46b1c4d --- /dev/null +++ b/spec/acceptance/try_get_value_spec.rb @@ -0,0 +1,47 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper_acceptance' + +describe 'try_get_value function', :unless => UNSUPPORTED_PLATFORMS.include?(fact('operatingsystem')) do + describe 'success' do + it 'try_get_valuees a value' do + pp = <<-EOS + $data = { + 'a' => { 'b' => 'passing'} + } + + $tests = try_get_value($a, 'a/b') + notice(inline_template('tests are <%= @tests.inspect %>')) + EOS + + apply_manifest(pp, :catch_failures => true) do |r| + expect(r.stdout).to match(/tests are "passing"/) + end + end + end + describe 'failure' do + it 'uses a default value' do + pp = <<-EOS + $data = { + 'a' => { 'b' => 'passing'} + } + + $tests = try_get_value($a, 'c/d', 'using the default value') + notice(inline_template('tests are <%= @tests.inspect %>')) + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stdout).to match(/using the default value/) + end + end + + it 'raises error on incorrect number of arguments' do + pp = <<-EOS + $o = try_get_value() + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/wrong number of arguments/i) + end + end + end +end diff --git a/spec/functions/try_get_value_spec.rb b/spec/functions/try_get_value_spec.rb new file mode 100644 index 0000000..38c0efd --- /dev/null +++ b/spec/functions/try_get_value_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' + +describe 'try_get_value' do + + let(:data) do + { + 'a' => { + 'g' => '2', + 'e' => [ + 'f0', + 'f1', + { + 'x' => { + 'y' => 'z' + } + }, + 'f3', + ] + }, + 'b' => true, + 'c' => false, + 'd' => '1', + } + end + + context 'single values' do + it 'should exist' do + is_expected.not_to eq(nil) + end + + it 'should be able to return a single value' do + is_expected.to run.with_params('test').and_return('test') + end + + it 'should use the default value if data is a single value and path is present' do + is_expected.to run.with_params('test', 'path', 'default').and_return('default') + end + + it 'should return default if there is no data' do + is_expected.to run.with_params(nil, nil, 'default').and_return('default') + end + + it 'should be able to use data structures as default values' do + is_expected.to run.with_params('test', 'path', data).and_return(data) + end + end + + context 'structure values' do + it 'should be able to extracts a single hash value' do + is_expected.to run.with_params(data, 'd', 'default').and_return('1') + end + + it 'should be able to extract a deeply nested hash value' do + is_expected.to run.with_params(data, 'a/g', 'default').and_return('2') + end + + it 'should return the default value if the path is not found' do + is_expected.to run.with_params(data, 'missing', 'default').and_return('default') + end + + it 'should return the default value if the path is too long' do + is_expected.to run.with_params(data, 'a/g/c/d', 'default').and_return('default') + end + + it 'should support an array index in the path' do + is_expected.to run.with_params(data, 'a/e/1', 'default').and_return('f1') + end + + it 'should return the default value if an array index is not a number' do + is_expected.to run.with_params(data, 'a/b/c', 'default').and_return('default') + end + + it 'should return the default value if and index is out of array length' do + is_expected.to run.with_params(data, 'a/e/5', 'default').and_return('default') + end + + it 'should be able to path though both arrays and hashes' do + is_expected.to run.with_params(data, 'a/e/2/x/y', 'default').and_return('z') + end + + it 'should be able to return "true" value' do + is_expected.to run.with_params(data, 'b', 'default').and_return(true) + is_expected.to run.with_params(data, 'm', true).and_return(true) + end + + it 'should be able to return "false" value' do + is_expected.to run.with_params(data, 'c', 'default').and_return(false) + is_expected.to run.with_params(data, 'm', false).and_return(false) + end + + it 'should return "nil" if value is not found and no default value is provided' do + is_expected.to run.with_params(data, 'a/1').and_return(nil) + end + + it 'should be able to use a custom path separator' do + is_expected.to run.with_params(data, 'a::g', 'default', '::').and_return('2') + is_expected.to run.with_params(data, 'a::c', 'default', '::').and_return('default') + end + end +end -- cgit v1.2.3