Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4578,6 +4578,14 @@ file { '/tmp/my.yaml':
}
```

##### Sort keys for stable output

```puppet
file { '/tmp/my.yaml':
ensure => file,
content => stdlib::to_yaml($myhash, {sort_keys => true})
```

#### `stdlib::to_yaml(Any $data, Optional[Hash] $options)`

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

###### Sort keys for stable output

```puppet
file { '/tmp/my.yaml':
ensure => file,
content => stdlib::to_yaml($myhash, {sort_keys => true})
```

##### `data`

Data type: `Any`
Expand Down
32 changes: 31 additions & 1 deletion lib/puppet/functions/stdlib/to_yaml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
# ensure => file,
# content => stdlib::to_yaml($myhash, {indentation => 4})
# }
# @example Sort keys for stable output
# file { '/tmp/my.yaml':
# ensure => file,
# content => stdlib::to_yaml($myhash, {sort_keys => true})
#
# @return [String] The YAML document
dispatch :to_yaml do
Expand All @@ -28,7 +32,33 @@

def to_yaml(data, options = {})
call_function('stdlib::rewrap_sensitive_data', data) do |unwrapped_data|
unwrapped_data.to_yaml(options.transform_keys(&:to_sym))
# 1) build a fresh, mutable hash of options
# transform_keys returns a new hash in Ruby 2.5+
opts = options.transform_keys(&:to_sym)

# 2) pluck out our custom flag (default false)
sort_keys = opts.delete(:sort_keys) || false

# 3) sort if requested
unwrapped_data = deep_sort(unwrapped_data) if sort_keys

# 4) dump with the remaining opts
unwrapped_data.to_yaml(opts)
end
end

private

def deep_sort(obj)
case obj
when Hash
# sort returns an Array of [k,v], then to_h makes a new Hash
obj.sort.to_h { |k, v| [k, deep_sort(v)] }
when Array
# preserve list order, but recurse into elements
obj.map { |item| deep_sort(item) }
else
obj
end
end
end
109 changes: 101 additions & 8 deletions spec/functions/to_yaml_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,119 @@

describe 'stdlib::to_yaml' do
it { is_expected.not_to be_nil }

# Basic scalars
it { is_expected.to run.with_params('').and_return("--- ''\n") }
it { is_expected.to run.with_params(true).and_return(%r{--- true\n}) }
it { is_expected.to run.with_params('one').and_return(%r{--- one\n}) }
it { is_expected.to run.with_params(true).and_return("--- true\n") }
it { is_expected.to run.with_params(false).and_return("--- false\n") }
it { is_expected.to run.with_params(42).and_return("--- 42\n") }
it { is_expected.to run.with_params('one').and_return("--- one\n") }

# Unicode
it { is_expected.to run.with_params('‰').and_return("--- \"‰\"\n") }
it { is_expected.to run.with_params('∇').and_return("--- \"∇\"\n") }

# Arrays
it { is_expected.to run.with_params([]).and_return("--- []\n") }
it { is_expected.to run.with_params(['one']).and_return("---\n- one\n") }
it { is_expected.to run.with_params(['one', 'two']).and_return("---\n- one\n- two\n") }

# Hashes
it { is_expected.to run.with_params({}).and_return("--- {}\n") }
it { is_expected.to run.with_params('key' => 'value').and_return("---\nkey: value\n") }

# Nested structures
it {
expect(subject).to run.with_params('one' => { 'oneA' => 'A', 'oneB' => { 'oneB1' => '1', 'oneB2' => '2' } }, 'two' => ['twoA', 'twoB'])
.and_return("---\none:\n oneA: A\n oneB:\n oneB1: '1'\n oneB2: '2'\ntwo:\n- twoA\n- twoB\n")
is_expected.to run.with_params(
'one' => { 'oneA' => 'A', 'oneB' => { 'oneB1' => '1', 'oneB2' => '2' } },
'two' => ['twoA', 'twoB'],
).and_return("---\none:\n oneA: A\n oneB:\n oneB1: '1'\n oneB2: '2'\ntwo:\n- twoA\n- twoB\n")
}

it { is_expected.to run.with_params('‰').and_return("--- \"‰\"\n") }
it { is_expected.to run.with_params('∇').and_return("--- \"∇\"\n") }
# Options: indentation
it {
is_expected.to run.with_params(
{ 'foo' => { 'bar' => true, 'baz' => false } },
'indentation' => 4,
).and_return("---\nfoo:\n bar: true\n baz: false\n")
}

# Frozen options hash must not raise
it {
is_expected.to run.with_params(
{ 'foo' => { 'bar' => true } },
{ 'indentation' => 4, 'sort_keys' => true }.freeze,
).and_return("---\nfoo:\n bar: true\n")
}

# sort_keys: default behaviour preserves insertion order
it {
is_expected.to run.with_params(
'z' => 1, 'a' => 2, 'm' => 3,
).and_return("---\nz: 1\na: 2\nm: 3\n")
}

# sort_keys => true: top-level keys sorted
it {
is_expected.to run.with_params(
{ 'z' => 1, 'a' => 2, 'm' => 3 },
'sort_keys' => true,
).and_return("---\na: 2\nm: 3\nz: 1\n")
}

it { is_expected.to run.with_params({ 'foo' => { 'bar' => true, 'baz' => false } }, 'indentation' => 4).and_return("---\nfoo:\n bar: true\n baz: false\n") }
# sort_keys => true: nested keys sorted
it {
is_expected.to run.with_params(
{ 'parent' => { 'z' => 1, 'a' => 2 } },
'sort_keys' => true,
).and_return("---\nparent:\n a: 2\n z: 1\n")
}

# sort_keys => true: deeply nested keys sorted
it {
is_expected.to run.with_params(
{ 'z_parent' => { 'z_child' => 1, 'a_child' => 2 }, 'a_parent' => { 'x' => 5 } },
'sort_keys' => true,
).and_return("---\na_parent:\n x: 5\nz_parent:\n a_child: 2\n z_child: 1\n")
}

# sort_keys => true: array order preserved, hashes inside arrays sorted
it {
is_expected.to run.with_params(
{ 'items' => [{ 'z' => 1, 'a' => 2 }, { 'x' => 3, 'b' => 4 }] },
'sort_keys' => true,
).and_return("---\nitems:\n- a: 2\n z: 1\n- b: 4\n x: 3\n")
}

# sort_keys => true: plain array element order preserved
it {
is_expected.to run.with_params(
{ 'items' => [3, 1, 2] },
'sort_keys' => true,
).and_return("---\nitems:\n- 3\n- 1\n- 2\n")
}

# sort_keys combined with indentation
it {
is_expected.to run.with_params(
{ 'z' => { 'b' => 1, 'a' => 2 }, 'a' => 3 },
'sort_keys' => true, 'indentation' => 4,
).and_return("---\na: 3\nz:\n a: 2\n b: 1\n")
}

# Sensitive data
context 'with data containing sensitive' do
it { is_expected.to run.with_params('key' => sensitive('value')).and_return(sensitive("---\nkey: value\n")) }
it {
is_expected.to run.with_params(
'key' => sensitive('value'),
).and_return(sensitive("---\nkey: value\n"))
}

it {
is_expected.to run.with_params(
{ 'z' => sensitive('secret'), 'a' => 'public' },
'sort_keys' => true,
).and_return(sensitive("---\na: public\nz: secret\n"))
}
end
end
Loading