# 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| { }` # `$data.tree_each |$value| { }` # # or # # `tree_each($data) |$path, $value| { }` # `tree_each($data) |$value| { }` # # 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