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
This commit is contained in:
Dmitry Ilyin 2015-09-01 21:39:16 +03:00
parent f820bb1560
commit 823a352f0f
4 changed files with 258 additions and 0 deletions

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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