config: Allow repeating the root key in /config files

If a non-default-name file in the config folder parses to a map with a
single top-level key matching the file's basename, unwrap it. This lets
TOML/YAML express slice-typed roots (cascade, permalinks), which can't
have a headless top-level array, and also lets users copy-paste docs
examples that include the root container (e.g. params.yaml with a
top-level params: block).

Fixes #12899
Fixes #14882

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bjørn Erik Pedersen
2026-05-13 13:14:06 +02:00
parent be4a0df3a2
commit ba5d812673
2 changed files with 150 additions and 1 deletions
+13 -1
View File
@@ -173,9 +173,11 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid
}
var keyPath []string
var unwrapKey string
if !DefaultConfigNamesSet[name] {
// Can be params.jp, menus.en etc.
name, lang := paths.FileAndExtNoDelimiter(name)
unwrapKey = name
keyPath = []string{name}
@@ -190,13 +192,23 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid
}
}
// TOML/YAML can't represent a headless top-level array, so allow a
// file to wrap its content under a single top-level key matching
// the basename (e.g. cascade.yaml with `cascade: [...]`).
var itemValue any = item
if unwrapKey != "" && len(item) == 1 {
if inner, ok := item[unwrapKey]; ok {
itemValue = inner
}
}
root := item
if len(keyPath) > 0 {
root = make(map[string]any)
m := root
for i, key := range keyPath {
if i >= len(keyPath)-1 {
m[key] = item
m[key] = itemValue
} else {
nm := make(map[string]any)
m[key] = nm