Skip to content

Commit 4d2e388

Browse files
author
Dmitry Ilyin
committed
Add a new function "fetch"
* Extracts a value from a deeply-nested data structure
1 parent f820bb1 commit 4d2e388

File tree

4 files changed

+287
-0
lines changed

4 files changed

+287
-0
lines changed

README.markdown

+34
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,40 @@ Versions | Puppet 2.6 | Puppet 2.7 | Puppet 3.x | Puppet 4.x |
10231023

10241024
**stdlib 5.x**: When released, stdlib 5.x will drop support for Puppet 2.7.x. Please see [this discussion](https://github.com/puppetlabs/puppetlabs-stdlib/pull/176#issuecomment-30251414).
10251025

1026+
#### `fetch`
1027+
1028+
*Type*: rvalue.
1029+
1030+
Looks up into a complex structure of arrays and hashes and returns a value
1031+
or the default value if nothing was found.
1032+
1033+
Key can contain slashes to describe path components. The function will go down
1034+
the structure and try to extract the required value.
1035+
1036+
$data = {
1037+
'a' => {
1038+
'b' => [
1039+
'b1',
1040+
'b2',
1041+
'b3',
1042+
]
1043+
}
1044+
}
1045+
1046+
$value = fetch($data, 'a/b/2', 'not_found', '/')
1047+
=> $value = 'b3'
1048+
1049+
a -> first hash key
1050+
b -> second hash key
1051+
2 -> array index starting with 0
1052+
1053+
not_found -> (optional) will be returned if there is no value or the path did not match. Defaults to nil.
1054+
/ -> (optional) path delimiter. Defaults to '/'.
1055+
1056+
In addition to the required "key" argument, "structure" accepts default
1057+
argument. It will be returned if no value was found or a path component is
1058+
missing. And the fourth argument can set a variable path separator.
1059+
10261060
##Development
10271061

10281062
Puppet Labs modules on the Puppet Forge are open projects, and community contributions are essential for keeping them great. We can’t access the huge number of platforms and myriad hardware, software, and deployment configurations that Puppet is intended to serve. We want to keep it as easy as possible to contribute changes so that our modules work in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. For more information, see our [module contribution guide.](https://docs.puppetlabs.com/forge/contributing.html)

lib/puppet/parser/functions/fetch.rb

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module Puppet::Parser::Functions
2+
newfunction(
3+
:fetch,
4+
:type => :rvalue,
5+
:arity => -2,
6+
:doc => <<-eos
7+
Looks up into a compex 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 fuction 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 = fetch($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, "structure" 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+
# no data -> return default
41+
break default if data.nil?
42+
# wrong path -> return default
43+
break default unless path.is_a? Array
44+
# empty path -> value found
45+
break data unless path.any?
46+
# non-empty path and non-structure data -> return default
47+
break default unless data.is_a? Hash or data.is_a? Array
48+
49+
key = path.shift
50+
if data.is_a? Array
51+
begin
52+
key = Integer key
53+
rescue ArgumentError
54+
break default
55+
end
56+
end
57+
path_lookup.call data[key], path, default
58+
end
59+
60+
data = args[0]
61+
path = args[1]
62+
default = args[2]
63+
separator = args[3]
64+
separator = '/' unless separator
65+
66+
path = '' unless path
67+
path = path.split separator
68+
path_lookup.call data, path, default
69+
end
70+
end

spec/acceptance/fetch_spec.rb

+47
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 'fetch function', :unless => UNSUPPORTED_PLATFORMS.include?(fact('operatingsystem')) do
5+
describe 'success' do
6+
it 'fetches a value' do
7+
pp = <<-EOS
8+
$data = {
9+
'a' => { 'b' => 'passing'}
10+
}
11+
12+
$tests = fetch($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 = fetch($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 = fetch()
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/fetch_spec.rb

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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('fetch')).to eq 'function_fetch'
9+
end
10+
11+
context 'single values' do
12+
13+
it 'should be able to return a single value' do
14+
expect(scope.function_fetch(['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_fetch(['test', 'path', 'default'])).to eq 'default'
19+
end
20+
21+
it 'should return default if there is no data' do
22+
expect(scope.function_fetch([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_fetch(['test', 'path', {'a' => 'a'}])).to eq({'a' => 'a'})
27+
end
28+
end
29+
30+
context 'structure values' do
31+
it 'extracts a hash value' do
32+
data = {
33+
'a' => "1",
34+
}
35+
expect(scope.function_fetch([data, 'a', '2'])).to eq '1'
36+
end
37+
38+
it 'returns the default value if there is no hash value' do
39+
data = {
40+
'a' => "1",
41+
}
42+
expect(scope.function_fetch([data, 'b', '2'])).to eq '2'
43+
end
44+
45+
it 'should extract a deep hash value' do
46+
data = {
47+
'a' => {
48+
'b' => 'c'
49+
}
50+
}
51+
expect(scope.function_fetch([data, 'a/b', 'default'])).to eq 'c'
52+
end
53+
54+
it 'should return default value if path is not found' do
55+
data = {
56+
'a' => {
57+
'b' => 'c'
58+
}
59+
}
60+
expect(scope.function_fetch([data, 'missing', 'default'])).to eq 'default'
61+
end
62+
63+
it 'should return default if path is too long' do
64+
data = {
65+
'a' => {
66+
'b' => 'c'
67+
}
68+
}
69+
expect(scope.function_fetch([data, 'a/b/c/d', 'default'])).to eq 'default'
70+
end
71+
72+
it 'should support array index in the path' do
73+
data = {
74+
'a' => {
75+
'b' => ['b0', 'b1', 'b2', 'b3']
76+
}
77+
}
78+
expect(scope.function_fetch([data, 'a/b/2', 'default'])).to eq 'b2'
79+
end
80+
81+
it 'should return default if index is out of array length' do
82+
data = {
83+
'a' => {
84+
'b' => ['b0', 'b1', 'b2', 'b3']
85+
}
86+
}
87+
expect(scope.function_fetch([data, 'a/b/5', 'default'])).to eq 'default'
88+
end
89+
90+
it 'should be able to path though both array and hashes' do
91+
data = {
92+
'a' => {
93+
'b' => [
94+
'b0',
95+
'b1',
96+
{
97+
'x' => {
98+
'y' => 'z'
99+
}
100+
},
101+
'b3'
102+
]
103+
}
104+
}
105+
expect(scope.function_fetch([data, 'a/b/2/x/y', 'default'])).to eq 'z'
106+
end
107+
108+
it 'should be able to return "true" value' do
109+
data = {
110+
'a' => false,
111+
'b' => true,
112+
}
113+
expect(scope.function_fetch([data, 'b', 1])).to eq true
114+
expect(scope.function_fetch([data, 'c', true])).to eq true
115+
end
116+
117+
it 'should be able to return "false" value' do
118+
data = {
119+
'a' => false,
120+
'b' => true,
121+
}
122+
expect(scope.function_fetch([data, 'a', 1])).to eq false
123+
expect(scope.function_fetch([data, 'c', false])).to eq false
124+
end
125+
126+
it 'should be able to use a custom path separator' do
127+
data = {
128+
'a' => {
129+
'b' => 'c'
130+
}
131+
}
132+
expect(scope.function_fetch([data, 'a::b', nil, '::'])).to eq 'c'
133+
expect(scope.function_fetch([data, 'a::c', nil, '::'])).to eq nil
134+
end
135+
end
136+
end

0 commit comments

Comments
 (0)