blueprints: Add !Enumerate, !Value and !Index tags (#4338)

* Added For and Item tags

* Removed Sequence node support from ForItem tag

* Added ForItemIndex tag

* Added support for iterating over mappings

* Added support for mapping output body

* Renamed tags: For to Enumerate, ForItem to Value, ForItemIndex to Index

* Refactored tests

* Formatting

* Improved exception info

* Improved error handing

* Added docs

* lint

* Small doc improvements

* Replaced deepcopy() call with call to copy()

* Fix mistake in docs example

* Fix missed "!" in example
This commit is contained in:
sdimovv
2023-01-05 20:36:19 +00:00
committed by GitHub
parent 2604dc14fe
commit 53cab07a48
4 changed files with 411 additions and 5 deletions

View File

@ -105,3 +105,130 @@ Requires at least one argument after the mode selection.
If only a single argument is provided, its boolean representation will be returned for all normal modes and its negated boolean representation will be returned for all negated modes.
Normally, it should be used to define complex conditions for use with an `!If` tag or for the `conditions` attribute of a blueprint entry (see [the blueprint file structure](./structure.md)). However, this is essentially just a boolean evaluator so it can be used everywhere a boolean representation is required.
#### `!Enumerate`, `!Index` and `!Value`
These tags collectively make it possible to iterate over objects which support iteration. Any iterable Python object is supported. Such objects are sequences (`[]`), mappings (`{}`) and even strings.
1. `!Enumerate` tag:
This tag takes 3 arguments:
```
!Enumerate [<iterable>, <output_object_type>, <single_item_yaml>]
```
- **iterable**: Any Python iterable or custom tag that resolves to such iterable
- **output_object_type**: `SEQ` or `MAP`. Controls whether the returned YAML will be a mapping or a sequence.
- **single_item_yaml**: The YAML to use to create a single entry in the output object
2. `!Index` tag:
:::info
This tag is only valid inside an `!Enumerate` tag
:::
This tag takes 1 argument:
```
!Index <depth>
```
- **depth**: Must be >= 0. A depth of 0 refers to the `!Enumerate` tag this tag is located in. A depth of 1 refers to one `!Enumerate` tag above that (to be used when multiple `!Enumerate` tags are nested inside each other).
Accesses the `!Enumerate` tag's iterable and resolves to the index of the item currently being iterated (in case `!Enumerate` is iterating over a sequence), or the mapping key (in case `!Enumerate` is iterating over a mapping).
For example, given a sequence like this - `["a", "b", "c"]`, this tag will resolve to `0` on the first `!Enumerate` tag iteration, `1` on the second and so on. However, if given a mapping like this - `{"key1": "value1", "key2": "value2", "key3": "value3"}`, it will first resolve to `key1`, then to `key2` and so on.
3. `!Value` tag:
:::info
This tag is only valid inside an `!Enumerate` tag
:::
This tag takes 1 argument:
```
!Value <depth>
```
- **depth**: Must be >= 0. A depth of 0 refers to the `!Enumerate` tag this tag is located in. A depth of 1 refers to one `!Enumerate` tag above that (to be used when multiple `!Enumerate` tags are nested inside each other).
Accesses the `!Enumerate` tag's iterable and resolves to the value of the item currently being iterated.
For example, given a sequence like this - `["a", "b", "c"]`, this tag will resolve to `a` on the first `!Enumerate` tag iteration, `b` on the second and so on. If given a mapping like this - `{"key1": "value1", "key2": "value2", "key3": "value3"}`, it will first resolve to `value1`, then to `value2` and so on.
Minimal examples:
```
configuration_stages: !Enumerate [
!Context map_of_totp_stage_names_and_types,
SEQ, # Output a sequence
!Find [!Format ["authentik_stages_authenticator_%s.authenticator%sstage", !Index 0, !Index 0], [name, !Value 0]] # The value of each item in the sequence
]
```
The above example will resolve to something like this:
```
configuration_stages:
- !Find [authentik_stages_authenticator_<stage_type_1>.authenticator<stage_type_1>stage, [name, <stage_name_1>]]
- !Find [authentik_stages_authenticator_<stage_type_2>.authenticator<stage_type_2>stage, [name, <stage_name_2>]]
```
Similarly, a mapping can be generated like so:
```
example: !Enumerate [
!Context list_of_totp_stage_names,
MAP, # Output a map
[
!Index 0, # The key to assign to each entry
!Value 0, # The value to assign to each entry
]
]
```
The above example will resolve to something like this:
```
example:
0: <stage_name_1>
1: <stage_name_2>
```
Full example:
:::warning
Note that an `!Enumeration` tag's iterable can never be an `!Item` or `!Value` tag with a depth of `0`. Minimum depth allowed is `1`. This is because a depth of `0` refers to the `!Enumeration` tag the `!Item` or `!Value` tag is in, and an `!Enumeration` tag cannot iterate over itself.
:::
```
example: !Enumerate [
!Context sequence, # ["foo", "bar"]
MAP, # Output a map
[
!Index 0, # Use the indexes of the items in the sequence as keys
!Enumerate [ # Nested enumeration
# Iterate over each item of the parent enumerate tag.
# Notice depth is 1, not 0, since we are inside the nested enumeration tag!
!Value 1,
SEQ, # Output a sequence
!Format ["%s: (index: %d, letter: %s)", !Value 1, !Index 0, !Value 0]
]
]
]
```
The above example will resolve to something like this:
```
'0':
- 'foo: (index: 0, letter: f)'
- 'foo: (index: 1, letter: o)'
- 'foo: (index: 2, letter: o)'
'1':
- 'bar: (index: 0, letter: b)'
- 'bar: (index: 1, letter: a)'
- 'bar: (index: 2, letter: r)'
```