Skip to content

Commit bbe5fd3

Browse files
author
Dmitry Ilyin
committed
Add a new function "getpath"
* Extracts a value from a deeply-nested data structure * Returns default if a value could not be extracted
1 parent f820bb1 commit bbe5fd3

File tree

4 files changed

+309
-0
lines changed

4 files changed

+309
-0
lines changed

README.markdown

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,40 @@ For example, the following returns 'param_value':
301301

302302
*Type*: rvalue.
303303

304+
#### `getpath`
305+
306+
*Type*: rvalue.
307+
308+
Looks up into a complex structure of arrays and hashes and returns a value
309+
or the default value if nothing was found.
310+
311+
Key can contain slashes to describe path components. The function will go down
312+
the structure and try to extract the required value.
313+
314+
$data = {
315+
'a' => {
316+
'b' => [
317+
'b1',
318+
'b2',
319+
'b3',
320+
]
321+
}
322+
}
323+
324+
$value = get_path($data, 'a/b/2', 'not_found', '/')
325+
=> $value = 'b3'
326+
327+
a -> first hash key
328+
b -> second hash key
329+
2 -> array index starting with 0
330+
331+
not_found -> (optional) will be returned if there is no value or the path did not match. Defaults to nil.
332+
/ -> (optional) path delimiter. Defaults to '/'.
333+
334+
In addition to the required "key" argument, "getpath" accepts default
335+
argument. It will be returned if no value was found or a path component is
336+
missing. And the fourth argument can set a variable path separator.
337+
304338
#### `getvar`
305339

306340
Looks up a variable in a remote namespace.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
module Puppet::Parser::Functions
2+
newfunction(
3+
:getpath,
4+
:type => :rvalue,
5+
:arity => -2,
6+
:doc => <<-eos
7+
Looks up into a complex structure of arrays and hashes and returns a value
8+
or the default value if nothing was found.
9+
10+
Key can contain slashes to describe path components. The function will go down
11+
the structure and try to extract the required value.
12+
13+
$data = {
14+
'a' => {
15+
'b' => [
16+
'b1',
17+
'b2',
18+
'b3',
19+
]
20+
}
21+
}
22+
23+
$value = getpath($data, 'a/b/2', 'not_found', '/')
24+
=> $value = 'b3'
25+
26+
a -> first hash key
27+
b -> second hash key
28+
2 -> array index starting with 0
29+
30+
not_found -> (optional) will be returned if there is no value or the path did not match. Defaults to nil.
31+
/ -> (optional) path delimiter. Defaults to '/'.
32+
33+
In addition to the required "key" argument, "getpath" accepts default
34+
argument. It will be returned if no value was found or a path component is
35+
missing. And the fourth argument can set a variable path separator.
36+
eos
37+
) do |args|
38+
path_lookup = lambda do |data, path, default|
39+
debug "Getpath: #{path.inspect} from: #{data.inspect}"
40+
if data.nil?
41+
debug "Getpath: no data, return default: #{default.inspect}"
42+
break default
43+
end
44+
unless path.is_a? Array
45+
debug "Getpath: wrong path, return default: #{default.inspect}"
46+
break default
47+
end
48+
unless path.any?
49+
debug "Getpath: value found, return data: #{data.inspect}"
50+
break data
51+
end
52+
unless data.is_a? Hash or data.is_a? Array
53+
debug "Getpath: incorrect data, return default: #{default.inspect}"
54+
break default
55+
end
56+
57+
key = path.shift
58+
if data.is_a? Array
59+
begin
60+
key = Integer key
61+
rescue ArgumentError
62+
debug "Getpath: non-numeric path for an array, return default: #{default.inspect}"
63+
break default
64+
end
65+
end
66+
path_lookup.call data[key], path, default
67+
end
68+
69+
data = args[0]
70+
path = args[1] || ''
71+
default = args[2]
72+
separator = args[3] || '/'
73+
74+
path = path.split separator
75+
path_lookup.call data, path, default
76+
end
77+
end

spec/acceptance/getpath_spec.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#! /usr/bin/env ruby -S rspec
2+
require 'spec_helper_acceptance'
3+
4+
describe 'getpath function', :unless => UNSUPPORTED_PLATFORMS.include?(fact('operatingsystem')) do
5+
describe 'success' do
6+
it 'getpathes a value' do
7+
pp = <<-EOS
8+
$data = {
9+
'a' => { 'b' => 'passing'}
10+
}
11+
12+
$tests = getpath($a, 'a/b')
13+
notice(inline_template('tests are <%= @tests.inspect %>'))
14+
EOS
15+
16+
apply_manifest(pp, :catch_failures => true) do |r|
17+
expect(r.stdout).to match(/tests are "passing"/)
18+
end
19+
end
20+
end
21+
describe 'failure' do
22+
it 'uses a default value' do
23+
pp = <<-EOS
24+
$data = {
25+
'a' => { 'b' => 'passing'}
26+
}
27+
28+
$tests = getpath($a, 'c/d', 'using the default value')
29+
notice(inline_template('tests are <%= @tests.inspect %>'))
30+
EOS
31+
32+
apply_manifest(pp, :expect_failures => true) do |r|
33+
expect(r.stdout).to match(/using the default value/)
34+
end
35+
end
36+
37+
it 'raises error on incorrect number of arguments' do
38+
pp = <<-EOS
39+
$o = getpath()
40+
EOS
41+
42+
apply_manifest(pp, :expect_failures => true) do |r|
43+
expect(r.stderr).to match(/wrong number of arguments/i)
44+
end
45+
end
46+
end
47+
end

spec/functions/getpath_spec.rb

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
require 'spec_helper'
2+
3+
describe 'getpath' do
4+
5+
context 'single values' do
6+
it 'should exist' do
7+
is_expected.not_to eq(nil)
8+
end
9+
10+
it 'should be able to return a single value' do
11+
is_expected.to run.with_params('test').and_return('test')
12+
end
13+
14+
it 'should use the default value if data is a single value and path is present' do
15+
is_expected.to run.with_params('test', 'path', 'default').and_return('default')
16+
end
17+
18+
it 'should return default if there is no data' do
19+
is_expected.to run.with_params(nil, nil, 'default').and_return('default')
20+
end
21+
22+
it 'should be able to use data structures as default values' do
23+
is_expected.to run.with_params('test', 'path', {'a' => 'a'}).and_return({'a' => 'a'})
24+
end
25+
end
26+
27+
context 'structure values' do
28+
it 'should be able to extracts a single hash value' do
29+
data = {
30+
'a' => '1',
31+
}
32+
is_expected.to run.with_params(data, 'a', 'default').and_return('1')
33+
end
34+
35+
it 'should be able to extract a deeply nested hash value' do
36+
data = {
37+
'a' => {
38+
'b' => 'c'
39+
}
40+
}
41+
is_expected.to run.with_params(data, 'a/b', 'default').and_return('c')
42+
end
43+
44+
it 'should return the default value if the hash has no such key' do
45+
data = {
46+
'a' => '1',
47+
}
48+
is_expected.to run.with_params(data, 'b', 'default').and_return('default')
49+
end
50+
51+
it 'should return the default value if the path is not found' do
52+
data = {
53+
'a' => {
54+
'b' => 'c'
55+
}
56+
}
57+
is_expected.to run.with_params(data, 'missing', 'default').and_return('default')
58+
end
59+
60+
it 'should return the default value if the path is too long' do
61+
data = {
62+
'a' => {
63+
'b' => 'c'
64+
}
65+
}
66+
is_expected.to run.with_params(data, 'a/b/c/d', 'default').and_return('default')
67+
end
68+
69+
it 'should support an array index in the path' do
70+
data = {
71+
'a' => {
72+
'b' => ['b0', 'b1', 'b2', 'b3']
73+
}
74+
}
75+
is_expected.to run.with_params(data, 'a/b/2', 'default').and_return('b2')
76+
end
77+
78+
it 'should return the default value if an array index is not a number' do
79+
data = {
80+
'a' => {
81+
'b' => ['b0', 'b1', 'b2', 'b3']
82+
}
83+
}
84+
is_expected.to run.with_params(data, 'a/b/c', 'default').and_return('default')
85+
end
86+
87+
it 'should return the default value if and index is out of array length' do
88+
data = {
89+
'a' => {
90+
'b' => ['b0', 'b1', 'b2', 'b3']
91+
}
92+
}
93+
is_expected.to run.with_params(data, 'a/b/5', 'default').and_return('default')
94+
end
95+
96+
it 'should be able to path though both arrays and hashes' do
97+
data = {
98+
'a' => {
99+
'b' => [
100+
'b0',
101+
'b1',
102+
{
103+
'x' => {
104+
'y' => 'z'
105+
}
106+
},
107+
'b3'
108+
]
109+
}
110+
}
111+
is_expected.to run.with_params(data, 'a/b/2/x/y', 'default').and_return('z')
112+
end
113+
114+
it 'should be able to return "true" value' do
115+
data = {
116+
'a' => false,
117+
'b' => true,
118+
}
119+
is_expected.to run.with_params(data, 'b', 'default').and_return(true)
120+
is_expected.to run.with_params(data, 'c', true).and_return(true)
121+
end
122+
123+
it 'should be able to return "false" value' do
124+
data = {
125+
'a' => false,
126+
'b' => true,
127+
}
128+
is_expected.to run.with_params(data, 'a', 'default').and_return(false)
129+
is_expected.to run.with_params(data, 'c', false).and_return(false)
130+
end
131+
132+
it 'should return "nil" if value is not found and no default value is provided' do
133+
data = {
134+
'a' => {
135+
'b' => 'c'
136+
}
137+
}
138+
is_expected.to run.with_params(data, 'a/1').and_return(nil)
139+
end
140+
141+
it 'should be able to use a custom path separator' do
142+
data = {
143+
'a' => {
144+
'b' => 'c'
145+
}
146+
}
147+
is_expected.to run.with_params(data, 'a::b', 'default', '::').and_return('c')
148+
is_expected.to run.with_params(data, 'a::c', 'default', '::').and_return('default')
149+
end
150+
end
151+
end

0 commit comments

Comments
 (0)