1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
|
# frozen_string_literal: true
# Runs a [lambda](https://puppet.com/docs/puppet/latest/lang_lambdas.html)
# recursively and repeatedly using values from a data structure, then returns the unchanged data structure, or if
# a lambda is not given, returns an `Iterator` for the tree.
#
# This function takes one mandatory argument, one optional, and an optional block in this order:
#
# 1. An `Array`, `Hash`, `Iterator`, or `Object` that the function will iterate over.
# 2. An optional hash with the options:
# * `include_containers` => `Optional[Boolean]` # default `true` - if containers should be given to the lambda
# * `include_values` => `Optional[Boolean]` # default `true` - if non containers should be given to the lambda
# * `include_root` => `Optional[Boolean]` # default `true` - if the root container should be given to the lambda
# * `container_type` => `Optional[Type[Variant[Array, Hash, Object]]]` # a type that determines what a container is - can only
# be set to a type that matches the default `Variant[Array, Hash, Object]`.
# * `order` => `Enum[depth_first, breadth_first]` # default ´depth_first`, the order in which elements are visited
# * `include_refs` => `Optional[Boolean]` # default `false`, if attributes in objects marked as bing of `reference` kind
# should be included.
# 3. An optional lambda, which the function calls for each element in the first argument. It must
# accept one or two arguments; either `$path`, and `$value`, or just `$value`.
#
# @example Using the `tree_each` function
#
# `$data.tree_each |$path, $value| { <PUPPET CODE BLOCK> }`
# `$data.tree_each |$value| { <PUPPET CODE BLOCK> }`
#
# or
#
# `tree_each($data) |$path, $value| { <PUPPET CODE BLOCK> }`
# `tree_each($data) |$value| { <PUPPET CODE BLOCK> }`
#
# The parameter `$path` is always given as an `Array` containing the path that when applied to
# the tree as `$data.dig(*$path) yields the `$value`.
# The `$value` is the value at that path.
#
# For `Array` values, the path will contain `Integer` entries with the array index,
# and for `Hash` values, the path will contain the hash key, which may be `Any` value.
# For `Object` containers, the entry is the name of the attribute (a `String`).
#
# The tree is walked in either depth-first order, or in breadth-first order under the control of the
# `order` option, yielding each `Array`, `Hash`, `Object`, and each entry/attribute.
# The default is `depth_first` which means that children are processed before siblings.
# An order of `breadth_first` means that siblings are processed before children.
#
# @example depth- or breadth-first order
#
# ```puppet
# [1, [2, 3], 4]
# ```
#
# If containers are skipped, results in:
#
# * `depth_first` order `1`, `2`, `3`, `4`
# * `breadth_first` order `1`, `4`,`2`, `3`
#
# If containers and root are included, results in:
#
# * `depth_first` order `[1, [2, 3], 4]`, `1`, `[2, 3]`, `2`, `3`, `4`
# * `breadth_first` order `[1, [2, 3], 4]`, `1`, `[2, 3]`, `4`, `2`, `3`
#
# Typical use of the `tree_each` function include:
# * a more efficient way to iterate over a tree than first using `flatten` on an array
# as that requires a new (potentially very large) array to be created
# * when a tree needs to be transformed and 'pretty printed' in a template
# * avoiding having to write a special recursive function when tree contains hashes (flatten does
# not work on hashes)
#
# @example A flattened iteration over a tree excluding Collections
#
# ```puppet
# $data = [1, 2, [3, [4, 5]]]
# $data.tree_each({include_containers => false}) |$v| { notice "$v" }
# ```
#
# This would call the lambda 5 times with with the following values in sequence: `1`, `2`, `3`, `4`, `5`
#
# @example A flattened iteration over a tree (including containers by default)
#
# ```puppet
# $data = [1, 2, [3, [4, 5]]]
# $data.tree_each |$v| { notice "$v" }
# ```
#
# This would call the lambda 7 times with the following values in sequence:
# `1`, `2`, `[3, [4, 5]]`, `3`, `[4, 5]`, `4`, `5`
#
# @example A flattened iteration over a tree (including only non root containers)
#
# ```puppet
# $data = [1, 2, [3, [4, 5]]]
# $data.tree_each({include_values => false, include_root => false}) |$v| { notice "$v" }
# ```
#
# This would call the lambda 2 times with the following values in sequence:
# `[3, [4, 5]]`, `[4, 5]`
#
# Any Puppet Type system data type can be used to filter what is
# considered to be a container, but it must be a narrower type than one of
# the default `Array`, `Hash`, `Object` types - for example it is not possible to make a
# `String` be a container type.
#
# @example Only `Array` as container type
#
# ```puppet
# $data = [1, {a => 'hello', b => [100, 200]}, [3, [4, 5]]]
# $data.tree_each({container_type => Array, include_containers => false} |$v| { notice "$v" }
# ```
#
# Would call the lambda 5 times with `1`, `{a => 'hello', b => [100, 200]}`, `3`, `4`, `5`
#
# **Chaining** When calling `tree_each` without a lambda the function produces an `Iterator`
# that can be chained into another iteration. Thus it is easy to use one of:
#
# * `reverse_each` - get "leaves before root"
# * `filter` - prune the tree
# * `map` - transform each element
#
# Note than when chaining, the value passed on is a `Tuple` with `[path, value]`.
#
# @example Pruning a tree
#
# ```puppet
# # A tree of some complexity (here very simple for readability)
# $tree = [
# { name => 'user1', status => 'inactive', id => '10'},
# { name => 'user2', status => 'active', id => '20'}
# ]
# notice $tree.tree_each.filter |$v| {
# $value = $v[1]
# $value =~ Hash and $value[status] == active
# }
# ```
#
# Would notice `[[[1], {name => user2, status => active, id => 20}]]`, which can then be processed
# further as each filtered result appears as a `Tuple` with `[path, value]`.
#
#
# For general examples that demonstrates iteration see the Puppet
# [iteration](https://puppet.com/docs/puppet/latest/lang_iteration.html)
# documentation.
#
# @since 5.0.0
#
Puppet::Functions.create_function(:tree_each) do
local_types do
type "OptionsType = Struct[{\
container_type => Optional[Type],\
include_root => Optional[Boolean],
include_containers => Optional[Boolean],\
include_values => Optional[Boolean],\
order => Optional[Enum[depth_first, breadth_first]],\
include_refs => Optional[Boolean]\
}]"
end
dispatch :tree_Enumerable2 do
param 'Variant[Iterator, Array, Hash, Object]', :tree
optional_param 'OptionsType', :options
block_param 'Callable[2,2]', :block
end
dispatch :tree_Enumerable1 do
param 'Variant[Iterator, Array, Hash, Object]', :tree
optional_param 'OptionsType', :options
block_param 'Callable[1,1]', :block
end
dispatch :tree_Iterable do
param 'Variant[Iterator, Array, Hash, Object]', :tree
optional_param 'OptionsType', :options
end
def tree_Enumerable1(enum, options = {}, &block)
iterator(enum, options).each { |_, v| yield(v) }
enum
end
def tree_Enumerable2(enum, options = {}, &block)
iterator(enum, options).each { |path, v| yield(path, v) }
enum
end
def tree_Iterable(enum, options = {}, &block)
Puppet::Pops::Types::Iterable.on(iterator(enum, options))
end
def iterator(enum, options)
if depth_first?(options)
Puppet::Pops::Types::Iterable::DepthFirstTreeIterator.new(enum, options)
else
Puppet::Pops::Types::Iterable::BreadthFirstTreeIterator.new(enum, options)
end
end
def depth_first?(options)
(order = options['order']).nil? ? true : order == 'depth_first'
end
end
|