A2: Extend StructuredConfigVariablesStep to YAML + XML
Summary
Format-dispatch refactor of G1.3. Same operator-facing toggle (Squid.Action.JsonConfigVariables.Enabled + Targets glob) now handles .json/.json5, .yaml/.yml, .xml — dispatched per file by extension.
Zero new NuGet dependencies — YamlDotNet 16.3.0 already in csproj; XML via .NET native System.Xml.Linq.
Format dispatch matrix
| Extension | Format | Engine |
|---|---|---|
.json, .json5
|
JsonConfigFormat |
Existing JsonPathReplacer (G1.3 + H1) — unchanged |
.yaml, .yml
|
YamlConfigFormat |
YamlDotNet.RepresentationModel.YamlStream |
.xml |
XmlConfigFormat |
System.Xml.Linq.XDocument |
.config |
(none) | XDT territory (G1.2) — deliberately NOT handled here |
Shared semantic contract (single source of truth)
All formats route through ConfigVariableLookup.TryFind for variable resolution:
- Dot-form path (canonical) + colon-form (ASP.NET-Core IConfiguration idiom)
-
Squid.*self-namespace skip on dot form (H1-#1) - Colon-form escape hatch (
Squid:X:Y) for deliberate operator override
If JSON / YAML / XML ever drift apart on namespace handling, that's a regression — the shared helper makes drift mechanically impossible.
What's in the box
| Layer | File |
|---|---|
| Interface + shared result |
src/Squid.Calamari/Commands/StructuredConfig/IStructuredConfigFormat.cs (new) |
| Variable lookup (Squid.* guard + dot/colon) |
src/Squid.Calamari/Commands/StructuredConfig/ConfigVariableLookup.cs (new) |
| JSON adapter |
JsonConfigFormat.cs (new) |
| YAML |
YamlConfigFormat.cs (new) |
| XML |
XmlConfigFormat.cs (new) |
| Dispatcher |
StructuredConfigFormatRegistry.cs (new) |
| Step uses registry | StructuredConfigVariablesStep.cs |
| Tests (37 new) |
YamlConfigFormatTests.cs (13), XmlConfigFormatTests.cs (16), StructuredConfigFormatRegistryTests.cs (8) |
Why wire literal stays JsonConfigVariables
The toggle is still Squid.Action.JsonConfigVariables.Enabled — name is technically misleading (YAML+XML also fire now) but renaming would break the canonical surface shipped in #367 (A1) just three PRs ago. The behavioural change is "files matching Targets glob are dispatched by extension". Operators who set Targets=*.yaml already see YAML support — no UI / wire-literal change needed.
If you want the canonical name to evolve, that's a future PR with the same dual-name pattern A1 used (canonical + legacy).
Test plan
-
Squid.Calamari.Tests: 422/422 (was 374; +48) -
Squid.UnitTests: 5625/5625 (unchanged) -
Solution build: 0 errors -
Pre-merge audit GREEN on 7/7 invariants -
All G1.3 + H1 JsonPathReplacer tests pass unchanged — back-compat preserved -
Squid.* guard verified per format (regression catch: format-specific drift would only show up if the guard wasn't shared) -
.configfiles NOT handled by XmlConfigFormat — pinned byXmlConfigFormatTests.CanHandletheory -
xmlns:* declarations not addressable — pinned -
Staging: real .yaml+.xmldeploy with multi-document YAML
Non-breaking guarantee
| Surface | Status |
|---|---|
| Existing wire literals | Unchanged (canonical + legacy) |
Existing .json deploys |
Byte-identical behaviour (JsonConfigFormat is pure adapter) |
.config files |
Untouched by this step (XDT G1.2 still owns them) |
JsonPathReplacer public API |
Unchanged |
| Squid.UnitTests cross-project drift detectors | All pass |
| SquidWeb | Untouched |