Skip to content

Commit 2b49b05

Browse files
committed
[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 933db81 commit 2b49b05

55 files changed

Lines changed: 7779 additions & 3380 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
---
22
children:
3+
- ["doc/CHANGELOG", "Changelog"]
34
- ["doc/basics", "YAML Basics"]
45
- ["doc/variables", "Variables"]
56
- ["doc/conditionals", "Conditionals"]
7+
- ["doc/loops", "Loops"]
68
- ["doc/include", "Include"]
79
- ["doc/templates", "Templates"]
810
- ["doc/packages", "Packages"]
@@ -27,6 +29,7 @@ Each feature addresses a different kind of reuse, composition, or abstraction to
2729
|--------------------------------------------|-----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
2830
| **Variables and Substitution (`${..}`)** | Insert dynamic values or evaluate expressions | Build labels, topics, IDs, or computed values |
2931
| **Conditionals (`!if`)** | Conditionally include or exclude YAML blocks | Enable or disable features when using packages or template flags |
32+
| **Loops (`!for`)** | Generate repeated YAML blocks from a list or map | Create multiple items, channels, or thing definitions from structured data |
3033
| **Include (`!include`)** | Insert the contents of another file | Reuse YAML across files; parameterize reusable blocks |
3134
| **Templates (`!insert`)** | Reuse YAML defined within the same file | Local parameterized blocks; reusable channel or item fragments |
3235
| **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 +40,7 @@ Each feature has a dedicated documentation page:
3740

3841
- [Variables and Substitution](doc/variables.md)
3942
- [Conditionals](doc/conditionals.md)
43+
- [Loops](doc/loops.md)
4044
- [Include](doc/include.md)
4145
- [Templates](doc/templates.md)
4246
- [Packages](doc/packages.md)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Changelog
2+
3+
All notable changes to the `YamlComposer` component will be documented in this file.
4+
5+
## [openHAB 5.3]
6+
7+
### New Features
8+
9+
- **[Key-Level `!if`](conditionals#keylevel-form):** Added a shorthand form for the `!if` tag to streamline conditional blocks.
10+
- **[Loop Support](loops):** Added `!for` directive for dynamic sequence iteration and templating.
11+
- **Jinjava Functions & Enumerate:** Added [range()](variables#range), and `enumerate` (as both a filter and function supporting lists and maps with `.key`/`.value` entry accessors) to be used in expressions and loops.
12+
- **[Ruby-Style Ranges](variables#ruby-style-range-syntax):** Added support for Ruby-style range syntax (`[1..5]` inclusive and `[1...5]` exclusive) as an alternative syntax for `range()`.
13+
- **[Inline `!var` Directive](variables#inline-var-directives):** Added `!var` directive for local variable declarations.
14+
Variables are scoped strictly to the active mapping level and its children, remaining hidden from parent and sibling contexts.
15+
16+
---
17+
18+
## [openHAB 5.2]
19+
20+
- Initial release.

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

Lines changed: 150 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,149 @@
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
1733

18-
## Basic Syntax
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.
1936

20-
The `!if` tag supports two forms: a **Mapping Form** for simple logic and a **Sequence Form** for multiple branches.
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 |
2145

22-
The `if:` and `elseif:` keys are treated as **implicit expressions**.
23-
You do not need to wrap the expression in `${...}`.
46+
## Key‑Level Form
2447

25-
### Mapping Form (Simple)
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 map underneath the selected tag is merged into the parent map.
52+
53+
Use this form when you want to conditionally merge map content into a parent 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 map is merged.
60+
- If the expression is falsy, the nested map 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+
:::
2666

27-
Use this for simple if/else decisions.
67+
### Simple Example
68+
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+
```
83+
84+
Result:
85+
86+
```yaml
87+
test:
88+
foo: bar
89+
items: a lot of items
90+
other: baz
91+
```
92+
93+
### Multi‑Branch Example
94+
95+
```yaml
96+
mode:
97+
!if "env == 'prod'":
98+
value: "production"
99+
!elseif "env == 'staging'":
100+
value: "staging"
101+
!elif "env == 'dev'": # !elseif, !elif, and !elsif can be used interchangeably
102+
value: "development"
103+
!else:
104+
value: "unknown"
105+
```
106+
107+
Result:
108+
109+
```yaml
110+
mode:
111+
value: "production"
112+
```
113+
114+
### Key Uniqueness
115+
116+
Each conditional tag is a map key.
117+
YAML requires each key to be unique.
118+
A duplicate‑key error occurs only when two conditional tags have identical key scalars.
119+
Inline comments may be used to make identical expressions unique.
120+
121+
```yaml
122+
test:
123+
!if "true # unique 1":
124+
foo: bar
125+
!if "true # unique 2":
126+
qux: quux
127+
!if "true # unique 3":
128+
corge: grault
129+
```
130+
131+
### Returning Simple Values
132+
133+
The _key‑level form_ is intended for returning maps and lists.
134+
Use inline expressions when you want to return a simple scalar value.
135+
Inline expressions do not require conditional tags.
136+
Inline expressions evaluate directly to a single value.
137+
138+
```yaml
139+
foo: ${"High" if 5 > 2 else "Low"}
140+
```
141+
142+
Alternatively the other two forms below also return a single value.
143+
144+
## Mapping Form
145+
146+
Use the mapping form for simple if/else decisions inside a single block.
28147
29148
```yaml
30149
example: !if
@@ -36,14 +155,15 @@ example: !if
36155
| Key | Description | Required |
37156
|:-------|:------------------------------------------------------------------------------------------------------------------------|:---------|
38157
| `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 |
158+
| `then` | The value to return if truthy. Can be a scalar, map, list, or any valid tag. | Yes |
159+
| `else` | The value to return if falsy. | No |
41160

42-
### Sequence Form (Multiple Branches)
161+
## Sequence Form
43162

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

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

49169
```yaml
@@ -57,9 +177,9 @@ environment_type: !if
57177

58178
## Expression Evaluation
59179

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

62-
### 1. Bare Expressions (Recommended)
182+
### Bare Expressions (Recommended)
63183

64184
The string is evaluated directly as an expression against the available variables.
65185

@@ -68,32 +188,26 @@ if: count > 10 and status == 'ALARM'
68188
```
69189

70190
::: tip
71-
The expression can be quoted when it contains characters that YAML would otherwise misinterpret, such as `:` or `#`.
191+
Quote expressions when they contain characters YAML would otherwise misinterpret, such as `:` or `#`.
72192
:::
73193

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

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

80199
```yaml
81200
variables:
82201
operator: ">"
83202
84203
test: !if
85204
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
89205
then: "High"
90206
```
91207

92208
## Truthiness Rules
93209

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**:
210+
Falsy values:
97211

98212
- `false`
99213
- `null`
@@ -102,21 +216,20 @@ The following values are considered **falsy**:
102216
- empty lists (`[]`)
103217
- empty maps (`{}`)
104218

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

108-
### Short-Circuiting (Lazy Evaluation)
221+
### ShortCircuiting (Lazy Evaluation)
109222

110223
Only the active branch is processed.
111224
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.
225+
Inactive branches do not load files and do not cause errors.
113226

114227
## Advanced Integration
115228

116229
### Nesting and Composition
117230

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

121234
```yaml
122235
status: !if
@@ -130,7 +243,7 @@ status: !if
130243

131244
### Conditional Merging (Mixins)
132245

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

135248
```yaml
136249
server_config:
@@ -157,8 +270,11 @@ network_settings: !if
157270
## Common Pitfalls
158271

159272
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'`.
273+
Quote string literals: `if: env == 'production'`.
161274
1. **Omitting `else`**: If no condition matches and there is no `else`, the result is `null`.
162275
1. **Invalid YAML**: Even inactive branches must be syntactically valid YAML.
276+
1. **Branch Ordering**: In the key‑level form, branches are evaluated in map order.
277+
The first truthy `!if` or `!elseif` wins.
278+
The `!else` branch applies only when no earlier branch matches.
163279

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

0 commit comments

Comments
 (0)