Skip to content

Commit 80816f9

Browse files
committed
remove pre-yaml string replacements, remove block-form var, require !else ~: syntax
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
1 parent 2bf79a8 commit 80816f9

9 files changed

Lines changed: 134 additions & 322 deletions

File tree

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Conditional tags are useful for selecting configuration blocks, enabling optiona
2323

2424
The conditional system supports three forms:
2525

26-
- **Key‑Level Form**: uses `!if`, `!elseif` (and aliases `!elsif`, `!elif`), and `!else` as map keys.
26+
- **Key‑Level Form**: uses `!if`, `!elseif` (and aliases `!elsif`, `!elif`), and `!else ~:` as map keys.
2727
- **Mapping Form**: uses `if:`, `then:`, and `else:` keys inside a single mapping.
2828
- **Sequence Form**: uses `if:`, `elseif:`, and `else:` entries inside a list.
2929

@@ -36,18 +36,18 @@ The table below summarizes the key differences so you can choose the right form
3636

3737
| Feature / Aspect | **Key‑Level Form** | **Mapping Form** | **Sequence Form** |
3838
|:-----------------------|:-----------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
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>` |
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>` |
4040
| **Behavior in Maps** | Merges nested key‑value pairs directly into the parent map | Resolves to a single value | Resolves to a single value |
4141
| **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`) |
42+
| **Multi‑Branching** | Supported via sibling keys (`!elseif`, `!else ~`) | Single condition (`if` / `then` / `else`) | Multi‑branch entries (`if`, `elseif`, `else`) |
4343
| **Unmatched Fallback** | Inactive branches are omitted entirely | Resolves to `null` | Resolves to `null` |
4444
| **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 |
4545

4646
## Key‑Level Form
4747

4848
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).
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).
5151
The nested map underneath the selected tag is merged into the parent map.
5252

5353
Use this form when you want to conditionally merge map content into a parent structure.
@@ -100,7 +100,7 @@ mode:
100100
value: "staging"
101101
!elif "env == 'dev'": # !elseif, !elif, and !elsif can be used interchangeably
102102
value: "development"
103-
!else:
103+
!else ~:
104104
value: "unknown"
105105
```
106106
@@ -275,6 +275,6 @@ network_settings: !if
275275
1. **Invalid YAML**: Even inactive branches must be syntactically valid YAML.
276276
1. **Branch Ordering**: In the key‑level form, branches are evaluated in map order.
277277
The first truthy `!if` or `!elseif` wins.
278-
The `!else` branch applies only when no earlier branch matches.
278+
The `!else ~` branch applies only when no earlier branch matches.
279279

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

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

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,35 @@ variables:
3535
3636
### Inline `!var` Directives
3737

38-
The `!var` directive allows you to declare or reassign variables **locally within a mapping node**, with visibility extending to all child nodes.
39-
Directives process sequentially and leave no output keys in the final composed data structure.
38+
The `!var` directive declares or reassigns variables **inline at the key level** within a mapping.
39+
Each directive updates the local variable scope immediately and produces **no output key** in the final composed structure.
4040

41-
::: tip Scope & Propagation Overview
41+
Variables defined this way are **local to the current mapping node** and automatically propagate to all of its descendants.
4242

43-
1. **Sequential Propagation:** A `!var` directive takes effect immediately for all subsequent entries—including keys, values, and nested child/descendant nodes—within the current mapping context.
44-
1. **Sub-Block Isolation:** Variables declared inside a nested child mapping remain confined to that specific branch. They propagate down to its descendants, but never leak upward to the parent or outward to adjacent sub-mappings.
43+
::: tip Scope & Propagation
44+
45+
1. **Immediate Effect:**
46+
Each `!var` directive is evaluated in order. Once declared, the variable is available to all subsequent keys, values, and nested mappings **within the same mapping node**.
47+
48+
1. **Mapping‑Node Boundaries:**
49+
Entering a nested mapping creates a new scope.
50+
Variables declared in the parent mapping remain visible to the child, but variables declared inside the child mapping do **not** propagate back to the parent.
51+
52+
1. **Sequential Evaluation:**
53+
Variables only apply to entries that appear **after** their declaration.
54+
Earlier entries in the same mapping cannot see variables declared later.
4555

4656
:::
4757

48-
`!var` supports single-property declarations (key-form), chained declarations, and block (map) declarations.
58+
#### `!var` Syntax
59+
60+
Declare a variable using the key‑level form:
4961

50-
#### Single-Form (Key-Form) Syntax
62+
```yaml
63+
!var name: value
64+
```
5165

52-
Use `!var name: value` to declare a single local variable.
53-
Multiple sequential key-form `!var` directives evaluate progressively, allowing subsequent directives to reference previously declared variables.
66+
Multiple `!var` directives may appear sequentially. Later directives may reference variables defined earlier in the same mapping.
5467

5568
```yaml
5669
!var host: "localhost"
@@ -61,46 +74,36 @@ Multiple sequential key-form `!var` directives evaluate progressively, allowing
6174
endpoint: "${api_url}/users"
6275
```
6376

64-
**Resulting Output:**
77+
**Result:**
6578

6679
```yaml
6780
endpoint: "http://localhost:8080/v1/users"
6881
```
6982

70-
#### Block/Map Form Syntax
71-
72-
Use `!var:` followed by a mapping block to declare multiple variables in a single declaration.
73-
74-
```yaml
75-
!var:
76-
host: "192.168.1.50"
77-
port: 8080
78-
protocol: "https"
79-
80-
endpoint: "${protocol}://${host}:${port}/api"
83+
#### `!var` Scope Diagram
84+
85+
```text
86+
parent-map:
87+
├─ !var a: 1 ← defines `a` in this mapping
88+
├─ key1: ${a} ← sees `a`
89+
90+
├─ child-map: ← new mapping node (inherits `a`)
91+
│ ├─ key2: ${a} ← sees `a` but not `b`
92+
│ │ (because `b` is declared *after* this entry)
93+
│ ├─ !var b: 2 ← defines `b` only in this child mapping
94+
│ └─ key3: ${b} ← sees `b` (same mapping node, declared earlier)
95+
96+
└─ key4: ${a} ← sees `a` but not `b`
97+
(because `b` was declared inside child-map)
8198
```
8299
83-
#### Merge Keys Inside `!var` Blocks
100+
::: tip List Context Restriction
84101
85-
Block-form `!var` declarations support YAML merge keys (`<<:`). Merge keys are expanded first, populating default variables into scope before explicit map entries are evaluated. This allows explicit variable declarations to reference or override merged defaults:
102+
`!var` is valid **only inside mapping nodes**.
86103

87-
```yaml
88-
defaults: &defaults
89-
base_url: "[http://10.0.0.1](http://10.0.0.1)"
90-
timeout: 3000
91-
92-
service:
93-
!var:
94-
<<: *defaults
95-
timeout: 5000
96-
endpoint: "${base_url}:${timeout}"
97-
url: "${endpoint}"
98-
# Service URL resolves to: "[http://10.0.0.1:5000](http://10.0.0.1:5000)"
99-
```
104+
If used inside a list item (e.g., `- !var foo: bar`), the list element becomes a mapping containing the directive.
105+
If your list item must remain a scalar, declare the variable in the parent mapping instead.
100106

101-
::: tip List Context Restriction
102-
`!var` directives are supported only inside mapping contexts.
103-
Using `!var` inside a YAML list context (e.g., `- !var foo: bar`) logs a warning and is ignored.
104107
:::
105108

106109
## Variable Scoping & Isolation
@@ -573,7 +576,6 @@ foo: !sub:jinja "Hello {{ username }}!"
573576
## Common Pitfalls
574577

575578
1. **Unquoted Operators**: Expressions containing YAML‑significant characters such as `:` or `?` must be quoted; otherwise YAML interprets those characters as structural syntax and rejects the value.
576-
1. **Sub-Block Scope Boundaries**: `!var` declarations inside child mapping blocks, templates, or `!for` loops remain confined to that branch and never leak upward or outward to adjacent sub-mappings.
577579
1. **Reserved Names & System Variables**: System variables (`OPENHAB_CONF`, `__FILE__`, etc.) and Jinja keywords (`true`, `false`, `null`, `in`, `if`) cannot be overwritten.
578580
1. **`+` vs `~`**: Use `~` for strings to avoid type mismatch errors and use `+` for numbers or lists.
579581
1. **Jinja Blocks**: Block‑level Jinja constructs (e.g., `{% for %}`) are not supported. Use YAMLComposer’s own control‑flow tags, such as `!if`/`!elseif`/`!else` and `!for`.

bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/ComposerUtils.java

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313
package org.openhab.io.yamlcomposer.internal;
1414

15+
import java.io.ByteArrayInputStream;
1516
import java.io.IOException;
1617
import java.nio.charset.StandardCharsets;
1718
import java.nio.file.Files;
@@ -131,42 +132,8 @@ public ScalarResolver getScalarResolver() {
131132
* @throws IOException if an I/O error occurs
132133
*/
133134
static @Nullable Object loadYaml(byte[] fileBytes, Path sourcePath) throws IOException {
134-
String yamlContent = new String(fileBytes, StandardCharsets.UTF_8);
135-
yamlContent = normalizeTagOnlyKeys(yamlContent);
136135
Load loader = createYamlLoader(sourcePath.toString());
137-
return loader.loadFromString(yamlContent);
138-
}
139-
140-
private static String normalizeTagOnlyKeys(String yamlContent) {
141-
StringBuilder normalized = new StringBuilder(yamlContent.length());
142-
boolean inBlockScalar = false;
143-
int blockScalarIndent = -1;
144-
for (String line : yamlContent.split("(?<=\\n)|(?<=\\r\\n)", -1)) {
145-
String normalizedLine = line;
146-
String content = line.replaceFirst("[\\r\\n]+$", "");
147-
if (inBlockScalar && !content.isBlank() && indentation(content) <= blockScalarIndent) {
148-
inBlockScalar = false;
149-
blockScalarIndent = -1;
150-
}
151-
if (!inBlockScalar) {
152-
normalizedLine = content.replaceFirst("^(\\s*!(?:else|var)):(\\s*)(.*)$", "$1 ~:$2$3")
153-
+ line.substring(content.length());
154-
if (normalizedLine.matches("^\\s*.+?:\\s*[|>][0-9+-]*\\s*(?:#.*)?(?:\\r?\\n)?$")) {
155-
inBlockScalar = true;
156-
blockScalarIndent = indentation(content);
157-
}
158-
}
159-
normalized.append(normalizedLine);
160-
}
161-
return normalized.toString();
162-
}
163-
164-
private static int indentation(String line) {
165-
int result = 0;
166-
while (result < line.length() && (line.charAt(result) == ' ' || line.charAt(result) == '\t')) {
167-
result++;
168-
}
169-
return result;
136+
return loader.loadFromInputStream(new ByteArrayInputStream(fileBytes));
170137
}
171138

172139
/**

bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/YamlComposer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public YamlComposer(Path path, Map<String, @Nullable Object> variables, Set<Path
131131
this.recursiveTransformer.register(new IfProcessor(logger));
132132
this.recursiveTransformer.register(new ElseIfProcessor(logger));
133133
this.recursiveTransformer.register(new ElseProcessor());
134-
this.recursiveTransformer.register(new VarProcessor());
134+
this.recursiveTransformer.register(new VarProcessor(logger));
135135
this.recursiveTransformer.register(
136136
new IncludeProcessor(absolutePath.getParent(), newIncludeStack, includeCallback, includeCache, logger));
137137
this.recursiveTransformer.register(new InsertProcessor(templates, logger));

bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/core/DirectiveProcessor.java

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -238,22 +238,14 @@ public DirectiveProcessor(BufferedLogger logger) {
238238
public void processVarDirective(VarDirective varDirective, @Nullable Object oldVal,
239239
RecursiveTransformer transformer, Set<Class<? extends Placeholder>> allowedTypes,
240240
IdentityHashMap<Object, Object> visited) {
241-
switch (varDirective) {
242-
case VarDirective.SingleForm single -> {
243-
String varName = single.variableName();
244-
if (VariableLoader.isSpecialVariable(varName)) {
245-
logger.warn("{} Cannot redefine special variable '{}'.", single.sourceLocation(), varName);
246-
return;
247-
}
248-
Object resolvedVal = transformer.transform(oldVal, allowedTypes, visited);
249-
transformer.getVariables().put(varName, resolvedVal);
250-
}
251-
case VarDirective.MapForm mapForm -> {
252-
VariableLoader varLoader = new VariableLoader(transformer.getVariables(), transformer.getAbsolutePath(),
253-
transformer, logger);
254-
varLoader.extractVariables(oldVal, null, true);
255-
}
241+
String varName = varDirective.variableName();
242+
if (VariableLoader.isSpecialVariable(varName)) {
243+
logger.warn("{} Cannot redefine special variable '{}'.", varDirective.sourceLocation(), varName);
244+
return;
256245
}
246+
247+
Object resolvedVal = transformer.transform(oldVal, allowedTypes, visited);
248+
transformer.getVariables().put(varName, resolvedVal);
257249
}
258250

259251
private void processForDirective(ForDirective forDirective, @Nullable Object oldVal,

bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/directives/VarDirective.java

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,16 @@
2121
* in the current evaluation context (such as maps or list-control items) and produce no direct
2222
* data entries in the final output.
2323
* <p>
24-
* Supported syntaxes:
24+
* Supported syntax:
2525
* <ul>
26-
* <li><b>{@link SingleForm}:</b> {@code !var name: value} — assigns a single variable where the
27-
* name is defined in the tag scalar argument and the value is provided by the entry value.</li>
28-
* <li><b>{@link MapForm}:</b> {@code !var:\n k1: v1\n k2: v2} — assigns multiple
29-
* variables from a mapping block.</li>
26+
* <li>{@code !var name: value} — assigns a single variable where the variable name is defined in the
27+
* tag scalar argument and the value is provided by the mapping entry value.</li>
3028
* </ul>
3129
*
30+
* @param variableName the variable name declared in the tag scalar
31+
* @param sourceLocation the location in the source file for diagnostics
3232
* @author Jimmy Tanagra - Initial contribution
3333
*/
3434
@NonNullByDefault
35-
public sealed interface VarDirective extends Directive {
36-
37-
/**
38-
* Represents a single-variable declaration using the scalar tag form (e.g., {@code !var name: value}).
39-
*
40-
* @param variableName the variable name declared in the tag scalar
41-
* @param sourceLocation the location in the source file for diagnostics
42-
*/
43-
record SingleForm(String variableName, String sourceLocation) implements VarDirective {
44-
}
45-
46-
/**
47-
* Represents a multi-variable declaration using a mapping block (e.g., {@code !var:\n k: v}).
48-
*
49-
* @param sourceLocation the location in the source file for diagnostics
50-
*/
51-
record MapForm(String sourceLocation) implements VarDirective {
52-
}
35+
public record VarDirective(String variableName, String sourceLocation) implements Directive {
5336
}

bundles/org.openhab.io.yamlcomposer/src/main/java/org/openhab/io/yamlcomposer/internal/processors/VarProcessor.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import org.eclipse.jdt.annotation.NonNullByDefault;
1616
import org.eclipse.jdt.annotation.Nullable;
17+
import org.openhab.io.yamlcomposer.internal.BufferedLogger;
1718
import org.openhab.io.yamlcomposer.internal.core.RecursiveTransformer;
1819
import org.openhab.io.yamlcomposer.internal.directives.VarDirective;
1920
import org.openhab.io.yamlcomposer.internal.placeholders.VarPlaceholder;
@@ -26,23 +27,30 @@
2627
@NonNullByDefault
2728
public class VarProcessor implements PlaceholderProcessor<VarPlaceholder> {
2829

30+
private final BufferedLogger logger;
31+
32+
public VarProcessor(BufferedLogger logger) {
33+
this.logger = logger;
34+
}
35+
2936
@Override
3037
public Class<VarPlaceholder> getPlaceholderType() {
3138
return VarPlaceholder.class;
3239
}
3340

3441
@Override
3542
public @Nullable Object process(VarPlaceholder placeholder, RecursiveTransformer transformer) {
36-
Object val = placeholder.value();
3743
String sourceLocation = placeholder.sourceLocation();
3844

39-
if (val != null) {
40-
String variableName = String.valueOf(val).trim();
41-
if (!variableName.isEmpty() && !"~".equals(variableName)) {
42-
return new VarDirective.SingleForm(variableName, sourceLocation);
45+
if (placeholder.value() instanceof String strVal) {
46+
String variableName = strVal.trim();
47+
if (!variableName.isEmpty() && !"null".equalsIgnoreCase(variableName)) {
48+
return new VarDirective(variableName, sourceLocation);
4349
}
4450
}
4551

46-
return new VarDirective.MapForm(sourceLocation);
52+
logger.warn("{} Invalid !var directive. Expected a variable name scalar (e.g., '!var name: value').",
53+
sourceLocation);
54+
return null;
4755
}
4856
}

0 commit comments

Comments
 (0)