Skip to content

Commit 2676d1b

Browse files
authored
[yamlcomposer] Add !for loops, scoped local variables (!var), and enhance conditionals (!elseif, !else) (#21408)
* [yamlcomposer] Add control flow directives (`!if`, `!else`, `!for`), local variables (`!var`), `range()` function, and `enumerate` support Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
1 parent ffe3815 commit 2676d1b

56 files changed

Lines changed: 8201 additions & 3383 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bundles/org.openhab.io.yamlcomposer/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ children:
33
- ["doc/basics", "YAML Basics"]
44
- ["doc/variables", "Variables"]
55
- ["doc/conditionals", "Conditionals"]
6+
- ["doc/loops", "Loops"]
67
- ["doc/include", "Include"]
78
- ["doc/templates", "Templates"]
89
- ["doc/packages", "Packages"]
@@ -27,6 +28,7 @@ Each feature addresses a different kind of reuse, composition, or abstraction to
2728
|--------------------------------------------|-----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
2829
| **Variables and Substitution (`${..}`)** | Insert dynamic values or evaluate expressions | Build labels, topics, IDs, or computed values |
2930
| **Conditionals (`!if`)** | Conditionally include or exclude YAML blocks | Enable or disable features when using packages or template flags |
31+
| **Loops (`!for`)** | Generate repeated YAML blocks from a list or map | Create multiple items, channels, or thing definitions from structured data |
3032
| **Include (`!include`)** | Insert the contents of another file | Reuse YAML across files; parameterize reusable blocks |
3133
| **Templates (`!insert`)** | Reuse YAML defined within the same file | Local parameterized blocks; reusable channel or item fragments |
3234
| **Packages** | Bundle multiple top-level sections into one reusable unit | Define reusable device structures containing things, items, metadata; sourced from external files or templates |
@@ -37,6 +39,7 @@ Each feature has a dedicated documentation page:
3739

3840
- [Variables and Substitution](doc/variables.md)
3941
- [Conditionals](doc/conditionals.md)
42+
- [Loops](doc/loops.md)
4043
- [Include](doc/include.md)
4144
- [Templates](doc/templates.md)
4245
- [Packages](doc/packages.md)

bundles/org.openhab.io.yamlcomposer/doc/conditionals.md

Lines changed: 197 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,193 @@
11
# Conditionals (!if)
22

3-
The `!if` tag performs logical branching during the **preprocessing phase**.
3+
Conditional tags perform logical branching during the **preprocessing phase**.
44

55
> **Note:** Conditions are evaluated **once** when the YAML file is loaded.
66
> These are not runtime rules.
77
> They do not react to live state changes in openHAB.
88
99
[[toc]]
1010

11-
## When to Use `!if`
11+
## When to Use Conditional Tags
1212

13-
Use the `!if` tag to adapt your configuration based on the **Resolution Context** (variables defined in the file, injected via `!include` or `!insert`, or [environment globals](variables.md#env-to-access-environment-variables)).
13+
Use conditional tags to adapt your configuration based on the **Resolution Context**.
14+
The Resolution Context includes variables defined in the file, variables injected via `!include` or `!insert`, and [environment globals](variables.md#env-to-access-environment-variables).
15+
16+
Conditional tags are useful for selecting configuration blocks, enabling optional features, or merging additional properties.
1417

1518
- **Conditional Snippets**: choose between alternative configuration blocks or values.
1619
- **Optional Properties**: conditionally merge in additional settings using [merge keys (<<)](merge-keys.md).
20+
- **Multi-Branch Logic**: select one of several possible configuration branches.
21+
22+
## Conditional Forms
23+
24+
The conditional system supports three forms:
25+
26+
- **Key‑Level Form**: uses `!if`, `!elseif` (and aliases `!elsif`, `!elif`), and `!else ~:` as map keys.
27+
- **Mapping Form**: uses `if:`, `then:`, and `else:` keys inside a single mapping.
28+
- **Sequence Form**: uses `if:`, `elseif:`, and `else:` entries inside a list.
29+
30+
All forms evaluate expressions once during preprocessing.
31+
32+
### Differences Between the Three Forms
33+
34+
Each form serves a different purpose and behaves differently when used inside maps or lists.
35+
The table below summarizes the key differences so you can choose the right form before diving into the detailed syntax.
36+
37+
| Feature / Aspect | **Key‑Level Form** | **Mapping Form** | **Sequence Form** |
38+
|:-----------------------|:-----------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
39+
| **Syntax** | `!if <expr>:`<br>`!elseif <expr>:`<br>`!else ~:` | `!if`<br>&nbsp;&nbsp;`if: <expr>`<br>&nbsp;&nbsp;`then: <val>`<br>&nbsp;&nbsp;`else: <val>` | `!if`<br>&nbsp;&nbsp;`- if: <expr>`<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`then: <val>`<br>&nbsp;&nbsp;`- elseif: <expr>`<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`then: <val>`<br>&nbsp;&nbsp;`- else: <val>` |
40+
| **Behavior in Maps** | Merges nested key‑value pairs directly into the parent map | Resolves to a single value | Resolves to a single value |
41+
| **Behavior in Lists** | Splices items directly into the parent list | Returns a single list element (branch may itself be a list) | Returns a single list element (branch may itself be a list) |
42+
| **Multi‑Branching** | Supported via sibling keys (`!elseif`, `!else ~`) | Single condition (`if` / `then` / `else`) | Multi‑branch entries (`if`, `elseif`, `else`) |
43+
| **Unmatched Fallback** | Inactive branches are omitted entirely | Resolves to `null` | Resolves to `null` |
44+
| **Primary Use Case** | Conditionally merging groups of properties or inserting multiple inline list items | Simple ternary scalar/container assignment | Multi‑branch ternary scalar/container assignment |
45+
46+
## Key‑Level Form
47+
48+
The key‑level form applies conditional tags directly as map keys.
49+
The tags `!if`, `!elseif` (and its aliases `!elsif`, `!elif`), and `!else ~` allow multi‑branch logic using separate map entries.
50+
Each tag evaluates its expression (except `!else ~`, which has no expression).
51+
The nested content (map or list) underneath the selected tag is merged into the parent structure.
52+
53+
Use this form when you want to conditionally merge additional map entries or list items into the surrounding structure.
54+
55+
::: tip Hints
56+
57+
- Expressions may be quoted or unquoted.
58+
Quote expressions when they contain characters YAML might misinterpret, such as `:` or `#`.
59+
- If the expression is truthy, the nested content (map or list) is merged into the parent structure.
60+
- If the expression is falsy, the nested content is ignored.
61+
- Branches are evaluated in order from top to bottom.
62+
Only the first truthy branch is selected.
63+
Inactive branches are ignored.
64+
65+
:::
1766

18-
## Basic Syntax
67+
### Simple Example
1968

20-
The `!if` tag supports two forms: a **Mapping Form** for simple logic and a **Sequence Form** for multiple branches.
69+
```yaml
70+
variables:
71+
items_count: 20
72+
things_count: 5
73+
74+
test:
75+
!if '"bar" == "bar"':
76+
foo: bar
77+
!if items_count > 10:
78+
items: a lot of items
79+
!if things_count > 10:
80+
things: a lot of things
81+
other: baz
82+
```
2183
22-
The `if:` and `elseif:` keys are treated as **implicit expressions**.
23-
You do not need to wrap the expression in `${...}`.
84+
Result:
2485
25-
### Mapping Form (Simple)
86+
```yaml
87+
test:
88+
foo: bar
89+
items: a lot of items
90+
other: baz
91+
```
2692
27-
Use this for simple if/else decisions.
93+
### List Example
94+
95+
```yaml
96+
variables:
97+
add_extra: true
98+
99+
items:
100+
MyItem:
101+
tags:
102+
- alpha
103+
- beta
104+
- !if add_extra:
105+
- gamma
106+
- delta
107+
- epsilon
108+
```
109+
110+
Result:
111+
112+
```yaml
113+
items:
114+
MyItem:
115+
tags:
116+
- alpha
117+
- beta
118+
- gamma
119+
- delta
120+
- epsilon
121+
```
122+
123+
### Multi‑Branch Example
124+
125+
```yaml
126+
mode:
127+
!if "env == 'prod'":
128+
value: "production"
129+
!elseif "env == 'staging'":
130+
value: "staging"
131+
!elif "env == 'dev'": # !elseif, !elif, and !elsif can be used interchangeably
132+
value: "development"
133+
!else ~:
134+
value: "unknown"
135+
```
136+
137+
Result:
138+
139+
```yaml
140+
mode:
141+
value: "production"
142+
```
143+
144+
::: tip Important — `!else` Requires `~`
145+
146+
A bare `!else:` is **invalid YAML** because it produces an empty mapping.
147+
148+
You **must** write:
149+
150+
```yaml
151+
!else ~:
152+
```
153+
154+
The `~` is YAML's canonical `null` literal and exists only to satisfy YAML's requirement that every key has a value.
155+
156+
:::
157+
158+
### Key Uniqueness
159+
160+
Each conditional tag is a map key.
161+
YAML requires each key to be unique.
162+
A duplicate‑key error occurs only when two conditional tags have identical key scalars.
163+
Inline comments may be used to make identical expressions unique.
164+
165+
```yaml
166+
test:
167+
!if "true # unique 1":
168+
foo: bar
169+
!if "true # unique 2":
170+
qux: quux
171+
!if "true # unique 3":
172+
corge: grault
173+
```
174+
175+
### Returning Simple Values
176+
177+
The _key‑level form_ is intended for returning maps and lists.
178+
Use inline expressions when you want to return a simple scalar value.
179+
Inline expressions do not require conditional tags.
180+
Inline expressions evaluate directly to a single value.
181+
182+
```yaml
183+
foo: ${"High" if 5 > 2 else "Low"}
184+
```
185+
186+
Alternatively the other two forms below also return a single value.
187+
188+
## Mapping Form
189+
190+
Use the mapping form for simple if/else decisions inside a single block.
28191

29192
```yaml
30193
example: !if
@@ -36,14 +199,15 @@ example: !if
36199
| Key | Description | Required |
37200
|:-------|:------------------------------------------------------------------------------------------------------------------------|:---------|
38201
| `if` | The expression to evaluate. | Yes |
39-
| `then` | The value to return if **truthy** ([see rules below](#truthiness-rules)). Can be a scalar, map, list, or any valid tag. | Yes |
40-
| `else` | The value to return if **falsy**. | No |
202+
| `then` | The value to return if truthy. Can be a scalar, map, list, or any valid tag. | Yes |
203+
| `else` | The value to return if falsy. | No |
41204

42-
### Sequence Form (Multiple Branches)
205+
## Sequence Form
43206

44-
Use this for multiple ordered conditions.
207+
Use the sequence form when you prefer to express multi‑branch logic inside a single list.
208+
This form mirrors the behavior of the key‑level tags but keeps all branches grouped together.
45209

46-
Evaluates conditions in order and stops at the first **truthy** match.
210+
Conditions are evaluated in order and stop at the first truthy match.
47211
If no condition matches and no `else` is provided, the tag resolves to `null`.
48212

49213
```yaml
@@ -57,9 +221,9 @@ environment_type: !if
57221

58222
## Expression Evaluation
59223

60-
The `if:` key follows a specific order of operations.
224+
Expressions used in key‑level `!if` and `!elseif` tags follow the same evaluation rules as expressions in `if:` keys.
61225

62-
### 1. Bare Expressions (Recommended)
226+
### Bare Expressions (Recommended)
63227

64228
The string is evaluated directly as an expression against the available variables.
65229

@@ -68,32 +232,26 @@ if: count > 10 and status == 'ALARM'
68232
```
69233

70234
::: tip
71-
The expression can be quoted when it contains characters that YAML would otherwise misinterpret, such as `:` or `#`.
235+
Quote expressions when they contain characters YAML would otherwise misinterpret, such as `:` or `#`.
72236
:::
73237

74-
### 2. Using substitution pattern (Advanced — Double Evaluation)
238+
### Substitution Pattern (Advanced — Double Evaluation)
75239

76-
If you use a substitution pattern inside an `if:` key, the substitution engine runs **first** to resolve `${...}` patterns.
240+
If you use a substitution pattern inside an `if:` key, the substitution engine runs first.
77241
The resulting string is then evaluated as an expression.
78-
This is useful for building logic strings from variables.
79242

80243
```yaml
81244
variables:
82245
operator: ">"
83246
84247
test: !if
85248
if: 75 ${operator} 50
86-
# Step 1: ${operator} resolves to ">"
87-
# Step 2: The if expression resolves to "75 > 50"
88-
# Step 3: expression evaluates to true
89249
then: "High"
90250
```
91251

92252
## Truthiness Rules
93253

94-
When a value is used in a conditional, it is first evaluated and then interpreted as either **truthy** or **falsy**.
95-
96-
The following values are considered **falsy**:
254+
Falsy values:
97255

98256
- `false`
99257
- `null`
@@ -102,21 +260,20 @@ The following values are considered **falsy**:
102260
- empty lists (`[]`)
103261
- empty maps (`{}`)
104262

105-
All other values are **truthy**.
106-
This includes any non‑empty string, any non‑zero number, and any non‑empty collection.
263+
All other values are truthy.
107264

108-
### Short-Circuiting (Lazy Evaluation)
265+
### ShortCircuiting (Lazy Evaluation)
109266

110267
Only the active branch is processed.
111268
Tags such as `!include` inside inactive branches are ignored.
112-
An `!include` in an inactive branch is never loaded and will not cause errors if the file does not exist.
269+
Inactive branches do not load files and do not cause errors.
113270

114271
## Advanced Integration
115272

116273
### Nesting and Composition
117274

118-
The `!if` tag is fully recursive.
119-
You can nest `!if` tags within the `then` or `else` blocks to create complex decision trees.
275+
Conditional tags are fully recursive.
276+
You can nest conditional tags inside `then` or `else` blocks.
120277

121278
```yaml
122279
status: !if
@@ -130,7 +287,7 @@ status: !if
130287

131288
### Conditional Merging (Mixins)
132289

133-
Use `!if` with the YAML merge key (`<<`) to conditionally mix in sets of properties.
290+
Use conditional tags with the YAML merge key (`<<`) to conditionally mix in sets of properties.
134291

135292
```yaml
136293
server_config:
@@ -157,8 +314,14 @@ network_settings: !if
157314
## Common Pitfalls
158315

159316
1. **Expression vs String Literal**: `if: production` checks for a variable named `production`.
160-
To check for the literal string, quote it: `if: env == 'production'`.
317+
Quote string literals: `if: env == 'production'`.
318+
1. **Incorrect `!else` syntax**:
319+
A bare `!else:` is invalid.
320+
Always write `!else ~:` when using the key‑level form.
161321
1. **Omitting `else`**: If no condition matches and there is no `else`, the result is `null`.
162322
1. **Invalid YAML**: Even inactive branches must be syntactically valid YAML.
323+
1. **Branch Ordering**: In the key‑level form, branches are evaluated in map order.
324+
The first truthy `!if` or `!elseif` wins.
325+
The `!else ~` branch applies only when no earlier branch matches.
163326

164327
See [Expression Syntax](variables.md#expression-syntax) for more details.

0 commit comments

Comments
 (0)