Skip to content

Commit 12e7e99

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 12e7e99

3 files changed

Lines changed: 148 additions & 9 deletions

File tree

REFERENCE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4578,6 +4578,14 @@ file { '/tmp/my.yaml':
45784578
}
45794579
```
45804580

4581+
##### Sort keys for stable output
4582+
4583+
```puppet
4584+
file { '/tmp/my.yaml':
4585+
ensure => file,
4586+
content => stdlib::to_yaml($myhash, {sort_keys => true})
4587+
```
4588+
45814589
#### `stdlib::to_yaml(Any $data, Optional[Hash] $options)`
45824590

45834591
Convert a data structure and output it as YAML
@@ -4604,6 +4612,14 @@ file { '/tmp/my.yaml':
46044612
}
46054613
```
46064614

4615+
###### Sort keys for stable output
4616+
4617+
```puppet
4618+
file { '/tmp/my.yaml':
4619+
ensure => file,
4620+
content => stdlib::to_yaml($myhash, {sort_keys => true})
4621+
```
4622+
46074623
##### `data`
46084624

46094625
Data type: `Any`

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 & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,119 @@
44

55
describe 'stdlib::to_yaml' do
66
it { is_expected.not_to be_nil }
7+
8+
# Basic scalars
79
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}) }
10+
it { is_expected.to run.with_params(true).and_return("--- true\n") }
11+
it { is_expected.to run.with_params(false).and_return("--- false\n") }
12+
it { is_expected.to run.with_params(42).and_return("--- 42\n") }
13+
it { is_expected.to run.with_params('one').and_return("--- one\n") }
14+
15+
# Unicode
16+
it { is_expected.to run.with_params('‰').and_return("--- \"\"\n") }
17+
it { is_expected.to run.with_params('∇').and_return("--- \"\"\n") }
18+
19+
# Arrays
1020
it { is_expected.to run.with_params([]).and_return("--- []\n") }
1121
it { is_expected.to run.with_params(['one']).and_return("---\n- one\n") }
1222
it { is_expected.to run.with_params(['one', 'two']).and_return("---\n- one\n- two\n") }
23+
24+
# Hashes
1325
it { is_expected.to run.with_params({}).and_return("--- {}\n") }
1426
it { is_expected.to run.with_params('key' => 'value').and_return("---\nkey: value\n") }
1527

28+
# Nested structures
1629
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")
30+
is_expected.to run.with_params(
31+
'one' => { 'oneA' => 'A', 'oneB' => { 'oneB1' => '1', 'oneB2' => '2' } },
32+
'two' => ['twoA', 'twoB'],
33+
).and_return("---\none:\n oneA: A\n oneB:\n oneB1: '1'\n oneB2: '2'\ntwo:\n- twoA\n- twoB\n")
1934
}
2035

21-
it { is_expected.to run.with_params('‰').and_return("--- \"\"\n") }
22-
it { is_expected.to run.with_params('∇').and_return("--- \"\"\n") }
36+
# Options: indentation
37+
it {
38+
is_expected.to run.with_params(
39+
{ 'foo' => { 'bar' => true, 'baz' => false } },
40+
'indentation' => 4,
41+
).and_return("---\nfoo:\n bar: true\n baz: false\n")
42+
}
43+
44+
# Frozen options hash must not raise
45+
it {
46+
is_expected.to run.with_params(
47+
{ 'foo' => { 'bar' => true } },
48+
{ 'indentation' => 4, 'sort_keys' => true }.freeze,
49+
).and_return("---\nfoo:\n bar: true\n")
50+
}
51+
52+
# sort_keys: default behaviour preserves insertion order
53+
it {
54+
is_expected.to run.with_params(
55+
'z' => 1, 'a' => 2, 'm' => 3,
56+
).and_return("---\nz: 1\na: 2\nm: 3\n")
57+
}
58+
59+
# sort_keys => true: top-level keys sorted
60+
it {
61+
is_expected.to run.with_params(
62+
{ 'z' => 1, 'a' => 2, 'm' => 3 },
63+
'sort_keys' => true,
64+
).and_return("---\na: 2\nm: 3\nz: 1\n")
65+
}
2366

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") }
67+
# sort_keys => true: nested keys sorted
68+
it {
69+
is_expected.to run.with_params(
70+
{ 'parent' => { 'z' => 1, 'a' => 2 } },
71+
'sort_keys' => true,
72+
).and_return("---\nparent:\n a: 2\n z: 1\n")
73+
}
2574

75+
# sort_keys => true: deeply nested keys sorted
76+
it {
77+
is_expected.to run.with_params(
78+
{ 'z_parent' => { 'z_child' => 1, 'a_child' => 2 }, 'a_parent' => { 'x' => 5 } },
79+
'sort_keys' => true,
80+
).and_return("---\na_parent:\n x: 5\nz_parent:\n a_child: 2\n z_child: 1\n")
81+
}
82+
83+
# sort_keys => true: array order preserved, hashes inside arrays sorted
84+
it {
85+
is_expected.to run.with_params(
86+
{ 'items' => [{ 'z' => 1, 'a' => 2 }, { 'x' => 3, 'b' => 4 }] },
87+
'sort_keys' => true,
88+
).and_return("---\nitems:\n- a: 2\n z: 1\n- b: 4\n x: 3\n")
89+
}
90+
91+
# sort_keys => true: plain array element order preserved
92+
it {
93+
is_expected.to run.with_params(
94+
{ 'items' => [3, 1, 2] },
95+
'sort_keys' => true,
96+
).and_return("---\nitems:\n- 3\n- 1\n- 2\n")
97+
}
98+
99+
# sort_keys combined with indentation
100+
it {
101+
is_expected.to run.with_params(
102+
{ 'z' => { 'b' => 1, 'a' => 2 }, 'a' => 3 },
103+
'sort_keys' => true, 'indentation' => 4,
104+
).and_return("---\na: 3\nz:\n a: 2\n b: 1\n")
105+
}
106+
107+
# Sensitive data
26108
context 'with data containing sensitive' do
27-
it { is_expected.to run.with_params('key' => sensitive('value')).and_return(sensitive("---\nkey: value\n")) }
109+
it {
110+
is_expected.to run.with_params(
111+
'key' => sensitive('value'),
112+
).and_return(sensitive("---\nkey: value\n"))
113+
}
114+
115+
it {
116+
is_expected.to run.with_params(
117+
{ 'z' => sensitive('secret'), 'a' => 'public' },
118+
'sort_keys' => true,
119+
).and_return(sensitive("---\na: public\nz: secret\n"))
120+
}
28121
end
29122
end

0 commit comments

Comments
 (0)