Skip to content

Commit 7246e5f

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 7246e5f

File tree

4 files changed

+315
-0
lines changed

4 files changed

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

0 commit comments

Comments
 (0)