Skip to content

Commit 52b84e9

Browse files
committed
stdlib::to_yaml add sort_keys option
This will help ensure generated yaml has a stable output. Signed-off-by: Patrick Riehecky <riehecky@fnal.gov>
1 parent 3f792ff commit 52b84e9

2 files changed

Lines changed: 132 additions & 10 deletions

File tree

lib/puppet/functions/stdlib/to_yaml.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
# ensure => file,
2020
# content => stdlib::to_yaml($myhash, {indentation => 4})
2121
# }
22+
# @example Sort keys for stable output
23+
# file { '/tmp/my.yaml':
24+
# ensure => file,
25+
# content => stdlib::to_yaml($myhash, {sort_keys => true})
2226
#
2327
# @return [String] The YAML document
2428
dispatch :to_yaml do
@@ -28,7 +32,33 @@
2832

2933
def to_yaml(data, options = {})
3034
call_function('stdlib::rewrap_sensitive_data', data) do |unwrapped_data|
31-
unwrapped_data.to_yaml(options.transform_keys(&:to_sym))
35+
# 1) build a fresh, mutable hash of options
36+
# transform_keys returns a new hash in Ruby 2.5+
37+
opts = options.transform_keys(&:to_sym)
38+
39+
# 2) pluck out our custom flag (default false)
40+
sort_keys = opts.delete(:sort_keys) || false
41+
42+
# 3) sort if requested
43+
unwrapped_data = deep_sort(unwrapped_data) if sort_keys
44+
45+
# 4) dump with the remaining opts
46+
unwrapped_data.to_yaml(opts)
47+
end
48+
end
49+
50+
private
51+
52+
def deep_sort(obj)
53+
case obj
54+
when Hash
55+
# sort returns an Array of [k,v], then to_h makes a new Hash
56+
obj.sort.to_h { |k, v| [k, deep_sort(v)] }
57+
when Array
58+
# preserve list order, but recurse into elements
59+
obj.map { |item| deep_sort(item) }
60+
else
61+
obj
3262
end
3363
end
3464
end

spec/functions/to_yaml_spec.rb

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,121 @@
11
# frozen_string_literal: true
2-
32
require 'spec_helper'
43

54
describe 'stdlib::to_yaml' do
65
it { is_expected.not_to be_nil }
6+
7+
# Basic scalars
78
it { is_expected.to run.with_params('').and_return("--- ''\n") }
8-
it { is_expected.to run.with_params(true).and_return(%r{--- true\n}) }
9-
it { is_expected.to run.with_params('one').and_return(%r{--- one\n}) }
9+
it { is_expected.to run.with_params(true).and_return("--- true\n") }
10+
it { is_expected.to run.with_params(false).and_return("--- false\n") }
11+
it { is_expected.to run.with_params(42).and_return("--- 42\n") }
12+
it { is_expected.to run.with_params('one').and_return("--- one\n") }
13+
14+
# Unicode
15+
it { is_expected.to run.with_params('‰').and_return("--- \"\"\n") }
16+
it { is_expected.to run.with_params('∇').and_return("--- \"\"\n") }
17+
18+
# Arrays
1019
it { is_expected.to run.with_params([]).and_return("--- []\n") }
1120
it { is_expected.to run.with_params(['one']).and_return("---\n- one\n") }
1221
it { is_expected.to run.with_params(['one', 'two']).and_return("---\n- one\n- two\n") }
22+
23+
# Hashes
1324
it { is_expected.to run.with_params({}).and_return("--- {}\n") }
1425
it { is_expected.to run.with_params('key' => 'value').and_return("---\nkey: value\n") }
1526

27+
# Nested structures
1628
it {
17-
expect(subject).to run.with_params('one' => { 'oneA' => 'A', 'oneB' => { 'oneB1' => '1', 'oneB2' => '2' } }, 'two' => ['twoA', 'twoB'])
18-
.and_return("---\none:\n oneA: A\n oneB:\n oneB1: '1'\n oneB2: '2'\ntwo:\n- twoA\n- twoB\n")
29+
is_expected.to run.with_params(
30+
'one' => { 'oneA' => 'A', 'oneB' => { 'oneB1' => '1', 'oneB2' => '2' } },
31+
'two' => ['twoA', 'twoB']
32+
).and_return("---\none:\n oneA: A\n oneB:\n oneB1: '1'\n oneB2: '2'\ntwo:\n- twoA\n- twoB\n")
1933
}
2034

21-
it { is_expected.to run.with_params('‰').and_return("--- \"\"\n") }
22-
it { is_expected.to run.with_params('∇').and_return("--- \"\"\n") }
35+
# Options: indentation
36+
it {
37+
is_expected.to run.with_params(
38+
{ 'foo' => { 'bar' => true, 'baz' => false } },
39+
'indentation' => 4
40+
).and_return("---\nfoo:\n bar: true\n baz: false\n")
41+
}
2342

24-
it { is_expected.to run.with_params({ 'foo' => { 'bar' => true, 'baz' => false } }, 'indentation' => 4).and_return("---\nfoo:\n bar: true\n baz: false\n") }
43+
# Frozen options hash must not raise
44+
it {
45+
is_expected.to run.with_params(
46+
{ 'foo' => { 'bar' => true } },
47+
{ 'indentation' => 4, 'sort_keys' => true }.freeze
48+
).and_return("---\nfoo:\n bar: true\n")
49+
}
2550

51+
# sort_keys: default behaviour preserves insertion order
52+
it {
53+
is_expected.to run.with_params(
54+
'z' => 1, 'a' => 2, 'm' => 3
55+
).and_return("---\nz: 1\na: 2\nm: 3\n")
56+
}
57+
58+
# sort_keys => true: top-level keys sorted
59+
it {
60+
is_expected.to run.with_params(
61+
{ 'z' => 1, 'a' => 2, 'm' => 3 },
62+
'sort_keys' => true
63+
).and_return("---\na: 2\nm: 3\nz: 1\n")
64+
}
65+
66+
# sort_keys => true: nested keys sorted
67+
it {
68+
is_expected.to run.with_params(
69+
{ 'parent' => { 'z' => 1, 'a' => 2 } },
70+
'sort_keys' => true
71+
).and_return("---\nparent:\n a: 2\n z: 1\n")
72+
}
73+
74+
# sort_keys => true: deeply nested keys sorted
75+
it {
76+
is_expected.to run.with_params(
77+
{ 'z_parent' => { 'z_child' => 1, 'a_child' => 2 }, 'a_parent' => { 'x' => 5 } },
78+
'sort_keys' => true
79+
).and_return("---\na_parent:\n x: 5\nz_parent:\n a_child: 2\n z_child: 1\n")
80+
}
81+
82+
# sort_keys => true: array order preserved, hashes inside arrays sorted
83+
it {
84+
is_expected.to run.with_params(
85+
{ 'items' => [{ 'z' => 1, 'a' => 2 }, { 'x' => 3, 'b' => 4 }] },
86+
'sort_keys' => true
87+
).and_return("---\nitems:\n- a: 2\n z: 1\n- b: 4\n x: 3\n")
88+
}
89+
90+
# sort_keys => true: plain array element order preserved
91+
it {
92+
is_expected.to run.with_params(
93+
{ 'items' => [3, 1, 2] },
94+
'sort_keys' => true
95+
).and_return("---\nitems:\n- 3\n- 1\n- 2\n")
96+
}
97+
98+
# sort_keys combined with indentation
99+
it {
100+
is_expected.to run.with_params(
101+
{ 'z' => { 'b' => 1, 'a' => 2 }, 'a' => 3 },
102+
'sort_keys' => true, 'indentation' => 4
103+
).and_return("---\na: 3\nz:\n a: 2\n b: 1\n")
104+
}
105+
106+
# Sensitive data
26107
context 'with data containing sensitive' do
27-
it { is_expected.to run.with_params('key' => sensitive('value')).and_return(sensitive("---\nkey: value\n")) }
108+
it {
109+
is_expected.to run.with_params(
110+
'key' => sensitive('value')
111+
).and_return(sensitive("---\nkey: value\n"))
112+
}
113+
114+
it {
115+
is_expected.to run.with_params(
116+
{ 'z' => sensitive('secret'), 'a' => 'public' },
117+
'sort_keys' => true
118+
).and_return(sensitive("---\na: public\nz: secret\n"))
119+
}
28120
end
29121
end

0 commit comments

Comments
 (0)